本周面试题一览:
  • 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)

在「执行上下文」的形成过程中,会使用「执行上下文栈」来管理执行上下文。

代码执行的过程:

  1. 创建 全局上下文 (global EC)
  2. 全局执行上下文 (caller) 逐行 自上而下 执行。遇到函数时,函数执行上下文 (callee) 被push到执行栈顶层
  3. 函数执行上下文被激活,成为 active EC, 开始执行函数中的代码,caller 被挂起
  4. 函数执行完后,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

这么说可能比较抽象,我画了一张图帮大家彻底理解他们之间的关系:

image

特殊的情况:Function 可以充当对象,也可以充当函数

console.log(Function.__proto__ === Function.prototype);
console.log(Object.__proto__ === Function.prototype);
console.log(Object.__proto__ === Function.__proto__ );

10. 谈谈你对闭包的理解

闭包属于一种特殊的作用域。它的定义可以理解为: 父函数被销毁 的情况下,返回出的子函数的[[scope]]中仍然保留着父级的单变量对象和作用域链,因此可以继续访问到父级的变量对象,这样的函数称为闭包。一句话解释就是:函数定义的作用域和函数执行的作用域,不在同一个作用域下。

  • 闭包会产生一个很经典的问题:

多个子函数的[[scope]]都是同时指向父级,是完全共享的。因此当父级的变量对象被修改时,所有子函数都受到影响。

  • 解决:
  1. 变量可以通过 函数参数的形式 传入,避免使用默认的[[scope]]向上查找
  2. 使用setTimeout包裹,通过第三个参数传入
  3. 使用 块级作用域,让变量成为自己上下文的属性,避免共享
  • 手写一个简单的闭包

下面例子中的 closure 就是一个闭包:

function func(){
    var a = 1,b = 2;
    function closure(){
        return a+b;
    }
    return closure;
}

关于

作者齐小神,前端程序媛一枚。

有点文艺,喜欢摄影。 虽然现在朝九晚五,埋头苦学, 但梦想是做女侠,扶贫济穷,仗剑走天涯。 希望有一天能改完 BUG 去实现自己的梦想。

公众号:大前端Space,不定时更新,欢迎来玩~

image


齐小神
69 声望6 粉丝

入世努力,出世潇洒。