一名【合格】前端工程师的自检清单 - 答案版(做用域和闭包)

终于到做用域和闭包这一块了,这一块应该是JavaScript语言里面最难以学习和理解的了.javascript

也许在平常编码中会常常接触到做用域和闭包,可是对于其原理和产生一系列的问题得不到一个深度的了解.做为一个[合格]的前端工程师,这一块的知识是必定要夯实的.前端

我在整理这一块的答案时,也从新理解了一遍做用域和闭包的知识,感受对于本身来讲,这又是一个提高,但愿这篇文章的答案可以和你们共勉.java

原文地址: 一名【合格】前端工程师的自检清单npm

1.理解词法做用域和动态做用域

  • 词法做用域: 词法做用域(也就是静态做用域)就是定义在词法阶段的做用域,是由写代码时将变量和块做用域写在哪里来决定的,所以当词法分析器处理代码时会保持做用域不变;不管函数在哪里被调用,也不管它如何被调用,它的词法做用域都只由函数被声明时所处的位置决定.编程

  • 动态做用域: 动态做用域并不关心函数和做用域是如何声明以及在任何处声明的,只关心它们从何处调用.换句话说,做用域链是基于调用栈的,而不是代码中的做用域嵌套.设计模式

  • JavaScript采用词法做用域.promise

举个例子:浏览器

var value = 1;

function foo() {
    console.log(value);
}

function bar() {
    var value = 2;
    foo();
}

bar();

// 结果是 ???
复制代码

假设JavaScript采用静态做用域,让咱们分析下执行过程:前端工程师

执行 foo 函数,先从 foo 函数内部查找是否有局部变量 value,若是没有,就根据书写的位置,查找上面一层的代码,也就是 value 等于 1,因此结果会打印 1.闭包

假设JavaScript采用动态做用域,让咱们分析下执行过程:

执行 foo 函数,依然是从 foo 函数内部查找是否有局部变量 value.若是没有,就从调用函数的做用域,也就是 bar 函数内部查找 value 变量,因此结果会打印2.

前面咱们已经说了,JavaScript采用的是静态做用域,因此这个例子的结果是1.

2.理解JavaScript的做用域和做用域链

  • 做用域就是一个独立的地盘,让变量不会外泄、暴露出去.

让咱们用一段简单的代码来理解一下:

function fn() {
    var innerVar = "内部变量";
}
fn();//要先执行这个函数,不然根本不知道里面是啥
console.log(innerVar); // Uncaught ReferenceError: innerVar is not defined
复制代码

从上面的例子能够体会到做用域的概念,变量 innerVar在全局做用域没有声明,因此在全局做用域下取值会报错.

ES6以前JavaScript没有块级做用域,只有全局做用域和函数做用域,能够经过letconst来实现.

  • 做用域链
  1. 什么是自由变量

首先认识一下什么叫作自由变量 .以下代码中,console.log(a)要获得a变量,可是在当前的做用域中没有定义 a(可对比一下b).当前做用域没有定义的变量,这成为自由变量.自由变量的值如何获得 -- 向父级做用域寻找(注意:这种说法并不严谨,下文会重点解释).

var a = 100
function fn() {
    var b = 200
    console.log(a) // 这里的a在这里就是一个自由变量
    console.log(b)
}
fn()
复制代码
  1. 什么是做用域链

若是父级也没呢?再一层一层向上寻找,直到找到全局做用域仍是没找到,就宣布放弃.这种一层一层的关系,就是做用域链.

var a = 100
function fn() {
    var b = 200
    function fn2() {
        var c = 300
        console.log(a) // 自由变量,顺做用域链向父做用域找
        console.log(b) // 自由变量,顺做用域链向父做用域找
        console.log(c) // 本做用域的变量
    }
    fn2()
}
fn()
复制代码
  1. 关于自由变量的取值

关于自由变量的值,上文提到要到父做用域中取,其实有时候这种解释会产生歧义.

var x = 10
function fn() {
  console.log(x)
}
function show(f) {
  var x = 20
  (function() {
    f() //10,而不是20
  })()
}
show(fn)
复制代码

在 fn 函数中,取自由变量x的值时,要到哪一个做用域中取?

要到建立fn函数的那个做用域中取,不管fn函数将在哪里调用.

因此,不要再用以上说法了.相比而言,用这句话描述会更加贴切:要到建立这个函数的那个域. 做用域中取值,这里强调的是“建立”,而不是“调用”,其实这就是所谓的静态做用域.

再看一个例子:

var a = 10
function fn() {
  var b = 20
  function bar() {
    console.log(a + b) //30
  }
  return bar
}
var x = fn(),
  b = 200
x() //bar()
复制代码

fn()返回的是bar函数,赋值给x.执行x(),即执行bar函数代码.取b的值时,直接在fn做用域取出.取a的值时,试图在fn做用域取,可是取不到,只能转向建立fn的那个做用域中去查找,结果找到了,因此最后的结果是30.

3.理解JavaScript的执行上下文栈,能够应用堆栈信息快速定位问题

执行上下文总共有三种类型:

  • 全局执行上下文: 这是默认的、最基础的执行上下文。不在任何函数中的代码都位于全局执行上下文中。它作了两件事:
  1. 建立一个全局对象,在浏览器中这个全局对象就是 window 对象。
  2. this 指针指向这个全局对象。一个程序中只能存在一个全局执行上下文。
  • 函数执行上下文: 每次调用函数时,都会为该函数建立一个新的执行上下文。每一个函数都拥有本身的执行上下文,可是只有在函数被调用的时候才会被建立。一个程序中能够存在任意数量的函数执行上下文。每当一个新的执行上下文被建立,它都会按照特定的顺序执行一系列步骤。

  • Eval函数执行上下文: 运行在eval 函数中的代码也得到了本身的执行上下文,但因为eval函数不建议使用,因此在这里再也不讨论。

执行栈,在其余编程语言中也被叫作调用栈,具备 LIFO(后进先出)结构,用于存储在代码执行期间建立的全部执行上下文。 当 JavaScript 引擎首次读取你的脚本时,它会建立一个全局执行上下文并将其推入当前的执行栈。每当发生一个函数调用,引擎都会为该函数建立一个新的执行上下文并将其推到当前执行栈的顶端。 引擎会运行执行上下文在执行栈顶端的函数,当此函数运行完成后,其对应的执行上下文将会从执行栈中弹出,上下文控制权将移到当前执行栈的下一个执行上下文。

4.this的原理以及几种不一样使用场景的取值

this 既不指向函数自身,也不指函数的词法做用域。若是仅经过 this 的英文解释,太容易产生误导了。它实际是在函数被调用时才发生的绑定,也就是说 this 具体指向什么,取决于你是怎么调用的函数。

this 的 4 种绑定规则分别是:默认绑定隐式绑定显式绑定new 绑定。优先级从低到高。

  1. 默认绑定:

什么叫默认绑定,即没有其余绑定规则存在时的默认规则。这也是函数调用中最经常使用的规则。 来看这段代码:

function foo() { 
    console.log( this.a );
}
var a = 2;
foo(); //打印的是什么?
复制代码

foo() 打印的结果是2。

由于foo()是直接调用的(独立函数调用),没有应用其余的绑定规则,这里进行了默认绑定,将全局对象绑定 this 上,因此 this.a 就解析成了全局变量中的 a ,即2。

注意:在严格模式下(strict mode),全局对象将没法使用默认绑定,即执行会报undefined的错误

function foo() { 
 "use strict";
    console.log(this.a);
}
var a = 2; 
foo(); // Uncaught TypeError: Cannot read property 'a' of undefined
复制代码
  1. 隐式绑定:

除了直接对函数进行调用外,有些状况是,函数的调用是在某个对象上触发的,即调用位置上存在上下文对象。

function foo() { 
    console.log(this.a);
}
var a = 2;
var obj = { 
    a: 3,
    foo: foo 
};
obj.foo(); // ?
复制代码

obj.foo() 打印的结果是3。

这里foo函数被当作引用属性,被添加到obj对象上。这里的调用过程是这样的: 获取obj.foo属性 -> 根据引用关系找到foo函数,执行调用 因此这里对foo的调用存在上下文对象objthis进行了隐式绑定,即this绑定到了obj上,因此this.a被解析成了obj.a,即3。

  • 多层调用链
function foo() { 
    console.log(this.a);
}
var a = 2;
var obj1 = { 
    a: 4,
    foo: foo 
};
var obj2 = { 
    a: 3,
    obj1: obj1
};
obj2.obj1.foo(); //? 
复制代码

obj2.obj1.foo() 打印的结果是3。

一样,咱们看下函数的调用过程: 先获取obj1.obj2 -> 经过引用获取到obj2对象,再访问 obj2.foo -> 最后执行foo函数调用 这里调用链不仅一层,存在obj1obj2两个对象,那么隐式绑定具体会绑哪一个对象。这里原则是获取最后一层调用的上下文对象,即obj2,因此结果显然是4(obj2.a)。

  • 隐式丢失(函数别名)

注意:这里存在一个陷阱,你们在分析调用过程时,要特别当心

先看个代码:

function foo() { 
    console.log(this.a);
}
var a = 2;
var obj = { 
    a: 3,
    foo: foo 
};
var bar = obj.foo;
bar(); //? 
复制代码

bar() 打印的结果是2。

为何会这样,obj.foo 赋值给bar,那调用bar()为何没有触发隐式绑定,使用的是默认绑定呢。 这里有个概念要理解清楚,obj.foo 是引用属性,赋值给bar的实际上就是foo函数(即:bar指向foo自己)。

那么,实际的调用关系是:经过bar找到foo函数,进行调用。整个调用过程并无obj的参数,因此是默认绑定,全局属性a

  • 隐式丢失(回调函数)
function foo() { 
    console.log(this.a);
}
var a = 2;
var obj = { 
    a: 3,
    foo: foo 
};
setTimeout(obj.foo, 100); // ? 
复制代码

打印的结果是2。

一样的道理,虽然参传是obj.foo,由于是引用关系,因此传参实际上传的就是foo对象自己的引用。对于setTimeout的调用,仍是 setTimeout -> 获取参数中foo的引用参数 -> 执行 foo 函数,中间没有obj的参与。这里依旧进行的是默认绑定。

  1. 显式绑定:

相对隐式绑定,this值在调用过程当中会动态变化,但是咱们就想绑定指定的对象,这时就用到了显式绑定。

显式绑定主要是经过改变对象的prototype关联对象,这里不展开讲。具体使用上,能够经过这两个方法callapply来实现(大多数函数及本身建立的函数默认都提供这两个方法)。

callapply是一样的做用,区别只是其余参数的设置上.

function foo() { 
    console.log(this.a);
}
var a = 2;
var obj1 = { 
    a: 3,
};
var obj2 = { 
    a: 4,
};
foo.call(obj1); // ?
foo.call(obj2); // ? 
复制代码

打印的结果是3, 4。

这里由于显示的申明了要绑定的对象,因此this就被绑定到了obj上,打印的结果天然就是obj1.aobj2.a

  • 硬绑定:
function foo() { 
    console.log(this.a);
}
var a = 2;
var obj1 = { 
    a: 3,
};
var obj2 = { 
    a: 4,
};
var bar = function(){
    foo.call(obj1);
}
setTimeout(bar, 100); // 3
bar.call(obj2); // 这是多少
复制代码

前面两个(函数别名、回调函数)打印3,由于显示绑定了,没什么问题。

最后一个打印是3。

这里须要注意下,虽然bar被显示绑定到obj2上,对于barfunction(){…} 中的this确实被绑定到了obj2,而foo由于经过foo.call(obj1)已经显示绑定了obj1,因此在foo函数内,this指向的是obj1,不会由于bar函数内指向obj2而改变自身。因此打印的是obj1.a(即3)。

  1. new绑定:

js中的new操做符,和其余语言中(如JAVA)的new机制是不同的。js中,它就是一个普通函数调用,只是被new修饰了而已。

使用new来调用函数,会自动执行以下操做:

  1. 建立一个空的简单JavaScript对象(即{});
  2. 连接该对象(即设置该对象的构造函数)到另外一个对象 ;
  3. 将步骤1新建立的对象做为this的上下文 ;
  4. 若是该函数没有返回对象,则返回this

从第三点能够看出,this指向的就是对象自己。 看个代码:

function foo(a) { 
    this.a = a;
}
var a = 2;
var bar1 = new foo(3);
console.log(bar1.a); // ?
var bar2 = new foo(4);
console.log(bar2.a); // ?
复制代码

最后一个打印是3, 4。

由于每次调用生成的是全新的对象,该对象又会自动绑定到this上,因此答案显而易见。

最后要注意箭头函数,它的this绑定取决于外层(函数或全局)做用域。

5.闭包的实现原理和做用,能够列举几个开发中闭包的实际应用

  • 闭包的概念:指有权访问另外一个函数做用域中的变量的函数,通常状况就是在一个函数中包含另外一个函数。

  • 闭包的做用:访问函数内部变量、保持函数在环境中一直存在,不会被垃圾回收机制处理.

  • 闭包的优势:

    1. 方便调用上下文中声明的局部变量
    2. 逻辑紧密,能够在一个函数中再建立个函数,避免了传参的问题
  • 闭包的缺点: 由于使用闭包,可使函数在执行完后不被销毁,保留在内存中,若是大量使用闭包就会形成内存泄露,内存消耗很大

防抖和节流就是典型的闭包实际应用,还有IIFE也是一个闭包

6.理解堆栈溢出和内存泄漏的原理,如何防止

  • 内存泄露:是指申请的内存执行完后没有及时的清理或者销毁,占用空闲内存,内存泄露过多的话,就会致使后面的程序申请不到内存。所以内存泄露会致使内部内存溢出

  • 堆栈溢出:是指内存空间已经被申请完,没有足够的内存提供了

  • 常见的内存泄露的缘由

    1. 全局变量引发的内存泄露
    2. 闭包
    3. 没有被清除的计时器
  • 解决方法

    1. 减小没必要要的全局变量
    2. 减小闭包的使用(由于闭包会致使内存泄露)
    3. 避免死循环的发生

7.如何处理循环的异步操做

  1. 如何确保循环的全部异步操做完成以后执行某个其余操做
  • 方法一:设置一个flag,在每一个异步操做中对flag进行检测
let flag = 0;
for(let i = 0; i < len; i++) {
    flag++;
    Database.save_method().exec().then((data) => {
        if(flag === len) {
            // your code
        }
    })
}
复制代码
  • 方法二:将全部的循环放在一个promise中,使用then处理
new Promise(function(resolve){
    resolve()
}).then(()=> {
    for(let i = 0; i < len; i++) {
        Database.save_method().exec()
    }
}).then(() => {
    // your code
})
复制代码
  1. 循环中的下一步操做依赖于前一步的操做,如何解决
  • 方法一:使用递归,在异步操做完成以后调用下一次异步操做
function loop(i){
    i++;
    Database.save_method().exec().then(() => {
        loop(i)
    })
}
复制代码
  • 方法二:使用asyncawait(注意: 不能在forEach中使用await)
async function loop() {
    for(let i = 0; i < len; i++) {
        await Database.save_method().exec();
    }
}
复制代码

8.理解模块化解决的实际问题,可列举几个模块化方案并理解其中原理

  1. Module模式

在模块化规范造成以前,JS开发者使用Module设计模式来解决JS全局做用域的污染问题。Module模式最初被定义为一种在传统软件工程中为类提供私有和公有封装的方法。在JavaScript中,Module模式使用匿名函数自调用 (闭包)来封装,经过自定义暴露行为来区分私有成员和公有成员。

let myModule = (function (window) {
    let moduleName = 'module'  // private
    // public
    function setModuleName(name) {
      moduleName = name
    }
    // public
    function getModuleName() {
      return moduleName
    }
    return { setModuleName, getModuleName }  // 暴露行为
})(window)
复制代码
  1. CommonJS

CommonJS主要用在Node开发上,每一个文件就是一个模块,没个文件都有本身的一个做用域。经过module.exports暴露public成员。例如:

// 文件名:x.js
let x = 1;
function add() {
  x += 1;
  return x;
}
module.exports.x = x;
module.exports.add = add;
复制代码

此外,CommonJS经过require()引入模块依赖,require函数能够引入Node的内置模块、自定义模块和npm等第三方模块。

// 文件名:main.js
let xm = require('./x.js');
console.log(xm.x);  // 1
console.log(xm.add());  // 2
console.log(xm.x);   // 1
复制代码
  1. AMD
// 定义AMD规范的模块
define([function() {
  return 模块
})
复制代码

区别于CommonJSAMD规范的被依赖模块是异步加载的,而定义的模块是被看成回调函数来执行的,依赖于require.js模块管理工具库。固然,AMD规范不是采用匿名函数自调用的方式来封装,咱们依然能够利用闭包的原理来实现模块的私有成员和公有成员:

define(['module1', 'module2'], function(m1, m2) {
  let x = 1;
  function add() {
    x += 1;
    return x;
  }
  return { add };
})
复制代码
  1. CMD

CMDSeaJS 在推广过程当中对模块定义的规范化产出。AMD 推崇依赖前置,CMD 推崇依赖就近。

define(function(require, exports, module) {
  // 同步加载模块
  var a = require('./a');
  a.doSomething();
  // 异步加载一个模块,在加载完成时,执行回调
  require.async(['./b'], function(b) {
    b.doSomething();
  });
  // 对外暴露成员
  exports.doSomething = function() {};
});
// 使用模块
seajs.use('path');
复制代码

CMD集成了CommonJSAMD的特色,支持同步和异步加载模块。CMD加载完某个依赖模块后并不执行,只是下载而已,在全部依赖模块加载完成后进入主逻辑,遇到require语句的时候才执行对应的模块,这样模块的执行顺序和书写顺序是彻底一致的。所以,在CMDrequire函数同步加载模块时没有HTTP请求过程。

  1. ES6 module

ES6的模块化已经不是规范了,而是JS语言的特性。随着ES6的推出,AMDCMD也随之成为了历史。ES6模块与模块化规范相比,有两大特色:

  • 模块化规范输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • 模块化规范是运行时加载,ES6 模块是编译时输出接口。

模块化规范输出的是一个对象,该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,ES6 module 是一个多对象输出,多对象加载的模型。从原理上来讲,模块化规范是匿名函数自调用的封装,而ES6 module则是用匿名函数自调用去调用输出的成员。

结语

由于时间和篇幅有限,因此每一项列举的答案都不算特别详细.

若是有这须要的同窗欢迎给我留言,我能够另开文章,详细讲一讲其中具体的部分.

固然也能够多看看《JavaScript高级程序设计》,基础才是重中之重啊.

系列连接:

相关文章
相关标签/搜索