乐趣区

关于前端:面试题锦2-|-谈谈你对闭包的理解

前言

大家都晓得,面试犹如考试,在考试之前,老师个别都会先给咱们 理清常识脉络 整顿温习纲要 ,依照纲要精准学习, 疾速达到应试状态 。所以,一本精炼的温习纲要对面试尤为重要。本 武林秘笈 从本周开始,每周会更新 3-5 个知识点,尽量的精简与提炼常识脉络,极为适宜利用碎片工夫来学习。心愿你和我一起,每周提高一点点~????

1.JS 的数据类型及存储形式的区别

在 ECMAScript 标准中,共定义了 8 种数据类型,分为 根本数据类型 援用数据类型(援用数据类型又称简单数据类型)两大类:

根本数据类型:String、Number、Boolean、Null、Undefined、Symbol(ES6 新增)、BigInt(ES10 新增)

援用数据类型:Object(Function、Array、Date、RegExp、Math… 都是 Object 类型的实例对象)

贮存形式的区别:

根本数据类型:变量名和值都贮存在栈内存中;

援用数据类型 :变量名贮存在 栈内存 中,值贮存在 堆内存 中,堆内存中会提供一个援用地址指向堆内存中的值,而这个援用地址是贮存在栈内存中的。

2. 罕用的判断 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]

3. 作用域、作用域链、执行上下文(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()
*/

4. 原型与原型链

  • 原型: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__);

5. 谈谈你对闭包的了解

闭包属于一种非凡的作用域。它的定义能够了解为: 父函数被销毁 的状况下,返回出的子函数的 [[scope]] 中依然保留着父级的单变量对象和作用域链,因而能够持续拜访到父级的变量对象,这样的函数称为闭包。一句话解释就是:函数定义的作用域和函数执行的作用域,不在同一个作用域下。

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

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

  • 解决:
  1. 变量能够通过 函数参数的模式 传入,防止应用默认的[[scope]] 向上查找
  2. 应用 setTimeout 包裹,通过第三个参数传入
  3. 应用 块级作用域,让变量成为本人上下文的属性,防止共享
  • 手写一个简略的闭包

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

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

对于

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

有点文艺,喜爱摄影。尽管当初朝九晚五,埋头苦学,但幻想是做女侠,扶贫济穷,仗剑走咫尺。心愿有一天能改完 BUG 去实现本人的幻想。

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

退出移动版