本周面试题一览:
- JS 的数据类型及存储方式的区别
- 常用的判断 JS 数据类型的四种方法
- 作用域、作用域链、执行上下文(EC)、以执行上下文栈(ECS)
- 原型与原型链
- 谈谈你对闭包的理解
6.JS 的数据类型及存储方式的区别
在 ECMAScript 规范中,共定义了 8 种数据类型,分为基本数据类型和引用数据类型(引用数据类型又称复杂数据类型)两大类:
基本数据类型:String、Number、Boolean、Null、Undefined、Symbol(ES6 新增)、BigInt(ES10 新增)引用数据类型:Object(Function、Array、Date、RegExp、Math...都是Object类型的实例对象)
储存方式的区别:
基本数据类型:变量名和值都储存在栈内存中;引用数据类型:变量名储存在栈内存中,值储存在堆内存中,堆内存中会提供一个引用地址指向堆内存中的值,而这个引用地址是储存在栈内存中的。
7.常用的判断 JS 数据类型的四种方法
typeof
缺点:不能判断 Object 类型,也不能判断 Null 类型。
type null
返回结果为object
是因为 JavaScript 是用 32 位比特来存储值的,且是通过值的低1位或3位来识别类型的,object 的前三位表示是 000,null 为机器码空指针,32位表示全是0,它们俩的前三位一样,所以typeof null
也会打印出object
。
console.log(typeof "") // string
console.log(typeof 1) // number
console.log(typeof true) // boolean
console.log(typeof Symbol()) // symbol
console.log(typeof undefined) // undefined
console.log(typeof null) // object
console.log(typeof []) // object
console.log(typeof {}) // object
constructor
constructor
可以找到这个变量是通过谁构造出来的。
缺点:
- 不能判断 null 和 undefined,因为 null 和 undefined 是无效的对象,因此是不会有 constructor 存在,这两种类型的数据需要通过其他方式来判断。
- 函数的 constructor 是不稳定的,这个主要体现在自定义对象上,当开发者重写 prototype 后,原有的 constructor 引用会丢失,constructor 会默认为 Object
console.log(("").constructor == String) // true
console.log((1).constructor == Number) // true
console.log((true).constructor == Boolean); // true
console.log([].constructor == Array) // true
console.log(({}).constructor == Object) // true
instanceof
instanceof
用来判断A是否是B的实例,在这里需要特别注意的是:instanceof 检测的是原型。
缺点:instanceof 只能用来判断两个对象是否属于实例关系, 而不能判断一个对象实例具体属于哪种类型。
console.log([] instanceof Array) // true
console.log({} instanceof Object) // true
console.log([] instanceof Object) // true
console.log(new Date() instanceof Date) // true
Object.prototype.toString.call()
通过 .call 将传进来的对象作为 Object 原型上的 this,然后通过 toString 方法可以看到具体的类型。
优缺点:不能细分谁是谁的实例,但是判断这个变量的类型还是非常方便和靠谱的。
console.log(Object.prototype.toString.call()) // [object Undefined]
console.log(Object.prototype.toString.call("")) // [object String]
console.log(Object.prototype.toString.call(1)) // [object Number]
console.log(Object.prototype.toString.call(true)) // [object Boolean]
console.log(Object.prototype.toString.call(Symbol())) // [object Symbol]
console.log(Object.prototype.toString.call(null)) // [object Null]
console.log(Object.prototype.toString.call(new Function())) // [object Function]
console.log(Object.prototype.toString.call(new Date())) // [object Date]
console.log(Object.prototype.toString.call([])) // [object Array]
console.log(Object.prototype.toString.call(new RegExp())) // [object RegExp]
console.log(Object.prototype.toString.call(new Error())) // [object Error]
console.log(Object.prototype.toString.call(document)) // [object HTMLDocument]
console.log(Object.prototype.toString.call(window)) // [object Window]
8.作用域、作用域链、执行上下文(EC)、以执行上下文栈(ECS)
作用域
函数在定义时会产生两种作用域
:分为 全局作用域 和 局部作用域,所以 JS 的作用域是静态作用域
。
执行上下文(EC)
当 JS 在执行一段代码的时候,会产生执行上下文(EC)
;
- 「执行上下文」一共有三种类型:全局执行上下文、函数执行上下文、
eval
执行上下文。 - 「执行上下文」包含三个部分:变量对象(VO)、作用域链(词法作用域)、
this
指向。
活动对象 (AO): 当变量对象所处的上下文为 active EC 时,称为活动对象。
执行上下文栈(ECS)
在「执行上下文」的形成过程中,会使用「执行上下文栈」来管理执行上下文。
代码执行的过程:
- 创建 全局上下文 (global EC)
- 全局执行上下文 (caller) 逐行 自上而下 执行。遇到函数时,函数执行上下文 (callee) 被push到执行栈顶层
- 函数执行上下文被激活,成为 active EC, 开始执行函数中的代码,caller 被挂起
- 函数执行完后,callee 被pop移除出执行栈,控制权交还全局上下文 (caller),继续执行
例子:
function a() {
b();
}
function b() {
c();
}
function c() {
console.log("welcome");
}
/**
ECS = [
globalContext
]
ECS.push(functionAContext);
ECS.push(functionBContext);
ECS.push(functionCContext);
ECS.pop();
ECS.pop();
ECS.pop();
**/
作用域链
函数内部会保留一个[[scope]]
属性,它会保存所有的父级的变量对象。而且在函数执行的时候,会把自身的AO
对象加进去,所以执行的时候会先找自己的AO
属性,找不到的话会向上查找,这就是作用域链。
「作用域链」有两部分组成:
[[scope]]
属性: 指向父级变量对象和作用域链,也就是包含了父级的[[scope]]
和AO
。AO
: 自身活动对象
如此 [[scope]]
包含[[scope]]
,便自上而下形成一条「作用域链」。
例子:
var a = 1;
function sum() {
var b = 2;
return a+b;
}
sum();
/*
sum.[[scope]] = {
globalContext.VO
}
// 编译阶段:
sumContext = {
A0:{
arguments: {
length: 0
},
b: undefined
},
Scope: [A0, sum.[[scope]]]
}
// 执行阶段:
ESC = [
globalContext,
sumContext
]
A0:{
arguments: {
length: 0
},
b: 2
},
// 执行完:
ECS.pop()
*/
9.原型与原型链
- 原型:
prototype
,每一个函数都有一个prototype
属性; - 原型链:
__proto__
,每一个对象都有一个__proto__属性; - 构造函数:可以通过
new
来新建一个对象的函数。 - 实例:通过构造函数和
new
创建出来的对象,便是实例。实例通过__proto__
指向原型,通过constructor
指向构造函数。
例子:
function Animal() {
this.type = "哺乳类"
}
Animal.prototype.type = "哺乳"
// 实例
let animal = new Animal();
console.log(animal.__proto__.__proto__ === Object.prototype); //true
console.log(Animal.prototype.constructor == Animal); // true
console.log(Object.prototype.__proto__); // null
这么说可能比较抽象,我画了一张图帮大家彻底理解他们之间的关系:
特殊的情况:Function 可以充当对象,也可以充当函数
console.log(Function.__proto__ === Function.prototype);
console.log(Object.__proto__ === Function.prototype);
console.log(Object.__proto__ === Function.__proto__ );
10. 谈谈你对闭包的理解
闭包属于一种特殊的作用域。它的定义可以理解为: 父函数被销毁 的情况下,返回出的子函数的[[scope]]
中仍然保留着父级的单变量对象和作用域链,因此可以继续访问到父级的变量对象,这样的函数称为闭包。一句话解释就是:函数定义的作用域和函数执行的作用域,不在同一个作用域下。
- 闭包会产生一个很经典的问题:
多个子函数的[[scope]]
都是同时指向父级,是完全共享的。因此当父级的变量对象被修改时,所有子函数都受到影响。
- 解决:
- 变量可以通过
函数参数的形式
传入,避免使用默认的[[scope]]
向上查找 - 使用
setTimeout
包裹,通过第三个参数传入 - 使用
块级作用域
,让变量成为自己上下文的属性,避免共享
- 手写一个简单的闭包
下面例子中的 closure 就是一个闭包:
function func(){
var a = 1,b = 2;
function closure(){
return a+b;
}
return closure;
}
关于
作者齐小神,前端程序媛一枚。
有点文艺,喜欢摄影。 虽然现在朝九晚五,埋头苦学, 但梦想是做女侠,扶贫济穷,仗剑走天涯。 希望有一天能改完 BUG 去实现自己的梦想。
公众号:大前端Space,不定时更新,欢迎来玩~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。