Skip to content

Latest commit

 

History

History
956 lines (805 loc) · 28.5 KB

闭包和上下文.md

File metadata and controls

956 lines (805 loc) · 28.5 KB

@import "[TOC]" {cmd="toc" depthFrom=2 depthTo=6 orderedList=false}

作用域,上下文,闭包,this等相关概念

作用域

作用域是指程序源代码中定义变量的区域。

作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。

JavaScript 采用词法作用域(lexical scoping),也就是静态作用域

静态作用域与动态作用域

因为 JavaScript 采用的是词法作用域,函数的作用域在函数定义的时候就决定了。

而与词法作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的。

var value = 1;
function foo() { console.log(value); }

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

bar();  // 1
  • 静态作用域的执行流程: 执行 foo 函数,先从 foo 函数内部查找是否有局部变量 value,如果没有,就根据书写的位置,查找上面一层的代码,也就是 value 等于 1,所以结果会打印 1。

  • 动态作用域的执行流程: 执行 foo 函数,依然是从 foo 函数内部查找是否有局部变量 value。如果没有,就从调用函数的作用域,也就是 bar 函数内部查找 value 变量,所以结果会打印 2。

JavaScript采用的是静态作用域,所以这个例子的结果是 1。

动态作用域的例子:bash就是。

执行上下文栈

执行到一个函数的时候,首先会做一些准备工作,创建执行上下文栈(Execution context stack,ECS)来管理执行上下文。其实就是call stack?

例1

function fun3() { console.log('fun3'); }
function fun2() { fun3(); }
function fun1() { fun2(); }

fun1();

// 模拟ECS的执行流程:
ECStack = [globalContext];      // globalContext是永远都在最底下的


ECStack.push(<fun1> functionContext);   // 调用func1
ECStack.push(<fun2> functionContext);   // fun1中调用fun2,还要创建fun2的执行上下文
ECStack.push(<fun3> functionContext);   // 调用了fun3

ECStack.pop();    // fun3执行完毕
ECStack.pop();    // fun2执行完毕
ECStack.pop();    // fun1执行完毕

例2

下面两个例子的执行结果都是'local scope',因为f在创建的时候绑定了它的scope变量是'local scope',而不是动态去找的。但它们的调用栈的执行过程有差别。

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();

// 模拟调用栈
ECStack.push(<checkscope> functionContext);
ECStack.push(<f> functionContext);
ECStack.pop();
ECStack.pop();
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();

// 模拟调用栈
ECStack.push(<checkscope> functionContext);
ECStack.pop();
ECStack.push(<f> functionContext);
ECStack.pop();

变量对象

变量对象(Variable Object, VO)是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。

不同执行上下文下的变量对象稍有不同

  • 全局上下文下的变量对象叫全局对象(global object)
  • 函数上下文下的变量对象叫活动对象(Activation Object, AO)

全局对象 W3school的介绍:

全局对象是预定义的对象,作为 JavaScript 的全局函数和全局属性的占位符。通过使用全局对象,可以访问所有其他所有预定义的对象、函数和属性。 在顶层 JavaScript 代码中,可以用关键字 this 引用全局对象。因为全局对象是作用域链的头,这意味着所有非限定性的变量和函数名都会作为该对象的属性来查询。 例如,当JavaScript 代码引用 parseInt() 函数时,它引用的是全局对象的 parseInt 属性。全局对象是作用域链的头,还意味着在顶层 JavaScript 代码中声明的所有变量都将成为全局对象的属性。

// 可以通过 this 引用,在客户端 JavaScript 中,全局对象就是 Window 对象。
console.log(this);  // Window

// 全局对象是由 Object 构造函数实例化的一个对象。
console.log(this instanceof Object);    // true

// 预定义了一堆函数和属性。
console.log(Math.random === this.Math.random);

// 作为全局变量的宿主。
var a = 1;
console.log(a === this.a);    // true

// 客户端 JavaScript 中,全局对象有 window 属性指向自身。
var a = 1;
console.log(window === this);       // true
console.log(this.window === this);  // true
console.log(window.a === this.a);   // true

活动对象 AO 也叫变量对象。活动对象是在进入函数上下文时刻被创建的,它通过函数的 arguments 属性初始化。arguments 属性值是 Arguments 对象。

执行上下文的代码会分成两个阶段进行处理:

  • 进入执行上下文,初始化各种变量(所以会有变量提升)
  • 代码执行

例子:

function foo(a) {
  var b = 2;
  function c() {}
  var d = function() {};
  b = 3;
}

foo(1);

// 进入执行上下文
AO = {
  arguments: {
      0: 1,
      length: 1
  },
  a: 1,
  b: undefined,
  c: reference to function c() {},
  d: undefined
}

// 代码执行
AO = {
  arguments: {
      0: 1,
      length: 1
  },
  a: 1,
  b: 3,
  c: reference to function c(){},
  d: reference to FunctionExpression "d"
}

但是,如果函数上下文中的变量没有用var声明,是不会放到AO里面的。AO里面找不到,去全局对象里面找,也没有。

function foo() {
  console.log(a);
  a = 1;
}

foo();  // Uncaught ReferenceError: a is not defined

// 如果这样的话,能在全局对象里面找到(不是在AO里面)
function bar() {
  a = 1;
  console.log(a);
}
bar();  // 1

进入执行上下文时,首先会处理函数声明,其次会处理变量声明,如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。但是后面赋值以后,是可以覆盖函数声明的。

console.log(foo);   // f foo() {}
function foo() {
    console.log("foo");
}
var foo = 1;
console.log(foo);   // 1

作用域链

当查找变量的时候,会先从当前上下文的变量对象AO中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链

函数的作用域在函数定义的时候就决定了。函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中。可以理解 [[scope]] 就是所有父变量对象的层级链,但是注意:[[scope]] 并不代表完整的作用域链!

例子:

function foo() {
  function bar() {}
}

// 函数创建时,各自的[[scope]]是
foo.[[scope]] = [
  globalContext.VO
];

bar.[[scope]] = [
  fooContext.AO,
  globalContext.VO
];

整个作用域链的创建过程:

var scope = "global scope";
function checkscope(){
  var scope2 = 'local scope';
  return scope2;
}
checkscope();

/* ---------- 过程 ---------- */
// checkscope 函数被创建,保存作用域链到内部属性[[scope]]
checkscope.[[scope]] = [
  globalContext.VO
];

// checkscope执行之前,开始做准备工作
// 执行上下文context包括:变量对象(Variable object,VO), 作用域链(Scope chain), this
checkscopeContext = {
  AO: {
    arguments: {
      length: 0
    },
    scope2: undefined
  }
  Scope: checkscope.[[scope]],    // 注意这个checkscopeContext.Scope和checkscope.[[scope]]都是指向作用域链。
}

// 将当前活动对象AO放入checkscope作用域链顶端
checkscopeContext = {
  AO: {
    arguments: {
      length: 0
    },
    scope2: undefined
  },
  Scope: [AO, ...checkscope.[[scope]]]
}

// 准备工作做完,开始执行函数,随着函数的执行,修改 AO 的属性值
checkscopeContext = {
  AO: {
    arguments: {
      length: 0
    },
    scope2: 'local scope'
  },
  Scope: [AO, ...checkscope.[[scope]]]
}

// 函数执行完成,出栈……

注意: 函数定义时候的[[scope]]和函数执行时候的scope,前者作为函数的属性,后者作为函数执行上下文的属性。

在源代码中当你定义(书写)一个函数的时候(并未调用),js引擎也能根据你函数书写的位置,函数嵌套的位置,给你生成一个[[scope]],作为该函数的属性存在(这个属性属于函数的)。即使函数不调用,这也是为什么JS是基于词法作用域(静态作用域)。

然后进入函数执行阶段,生成执行上下文,执行上下文你可以宏观的看成一个对象,(包含vo,scope,this),此时,执行上下文里的scope和之前属于函数的那个[[scope]]不是同一个,执行上下文里的scope,是在之前函数的[[scope]]的基础上,又新增一个当前的AO对象构成的。

闭包

MDN 对闭包的定义为:

闭包是指那些能够访问自由变量的函数。自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。

所以,从技术(广义)的角度讲,所有的函数都是闭包。任何函数在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。

从实践(狭义)角度,以下函数才算是闭包:

  • 即使创建此函数的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
  • 在代码中引用了自由变量

闭包的实现方式: 是如何读取到已经被销毁的上下文中的自由变量呢?其实就是通过AO和作用域链。闭包的时候就算上下文已经销毁了,闭包的函数的作用域链里面还是存了之前上下文中的AO。

例子:非闭包的情况

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();  // 3    
data[1]();  // 3
data[2]();  // 3

// 函数调用之前,上下文和VO是
globalContext = {
  VO: {
    data: [...],
    i: 3
  }
}

data0Context = {
  Scope: [AO, globalContext.VO]   // data[0]()顺着作用域链找到了globalContext.VO.i = 3
}

使用闭包的情况

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = (function (i) {
    return function(){
      console.log(i);
    }
  })(i);
}

data[0]();  // 0
data[1]();  // 1
data[2]();  // 2

// 函数调用之前,上下文和VO是
globalContext = {
  VO: {
    data: [...],
    i: 3
  }
}

匿名函数Context = {
  AO: {
    arguments: {
      0: 0,
      length: 1
    },
    i: 0
  }
}

data0Context = {
  Scope: [AO, 匿名函数Context.AO, globalContext.VO]   // data[0]()顺着作用域链找到了匿名函数Context.AO.i = 0
}

闭包的一个具体例子:

function x() {
  var inside = 2;
  return function y() {
    console.log(inside)
  }
}
var f = x();
console.dir(f)    // f的[[Scopes]]存储了inside变量,注意如果y里面没用到外部变量,那Scopes里面就会完全没有Closure (x)
/*
  ƒ y() {
    ...
    [[Scopes]]: [
      Closure (x) { inside: 2 },
      Global { ... }
    ]
  }
*/

拓展:用作用域解释这个例子

// 对象内部定义的fn无法访问自己
var o = {
  fn: function (){
    console.log(fn);
  }
};
o.fn();   // ReferenceError: fn is not defined

// 外部定义的fn就可以访问自己
var fn = function (){
  console.log(fn);
};
fn();     // function (){ console.log(fn); };

解释

  • var的是在外部创建了一个fn变量,存在AO中,fn内部在内部寻找不到fn后向上作用域查找fn,就能找到。
  • 创建对象内部时,因为没有在函数作用域内创建fn,所以无法访问。作用域内只有o的定义。

this

首先说一下什么是JS里面的MemberExpression(是AST里面的吗),简单理解 MemberExpression 其实就是()左边的部分。

foo();     // MemberExpression 是 foo
foo()();   // MemberExpression 是 foo()
foo.bar(); // MemberExpression 是 foo.bar

如果MemberExpression的结果是一个reference,那this就指向这个MemberExpression的base。这个了解就可以了。

function foo() {
  console.log(this)
}

foo();    // MemberExpression 是 foo

// 这里的this就指向fooReference.base
var fooReference = {
    base: EnvironmentRecord,
    name: 'foo',
    strict: false
};

this的一些性质

非严格模式下, this 的值如果为 undefined,默认指向全局对象。

this的指向是调用时决定的,而不是创建时决定的。在函数执行过程中,this一旦被确定,就不可更改了,会报错。

(function () {
  this = {};  // SyntaxError: Invalid left-hand side in assignment
})();

this到底指向谁

基本上,理解this指向它的调用者,已经够用了。 找到this的一些总结:

  • 由new调用:绑定到新创建的对象
  • 由call或apply、bind调用:绑定到指定的对象
  • 由上下文对象调用:绑定到上下文对象
  • 箭头函数:继承外层函数调用的this绑定
  • 作为一个DOM事件处理函数: 指向触发事件的元素e.target
  • 默认:全局对象

具体来讲:

  • 全局上下文中直接调用:指向全局对象。 this等价于window对象,var === this. === winodw.
console.log(window === this); // true
var a = 1;
this.b = 2;
window.c = 3;
console.log(a + b + c); // 6
  • 函数上下文中直接调用:指向全局对象。
function foo(){
  console.log(this);
}
foo(); // Window
  • call, apply, bind:指向手动绑定的那个对象
var person = {
  name: "axuebin",
  age: 25
};
function say(job){
  console.log(this.name + ":" + this.age + " " + job);
}

say.call(person,"FE");    // axuebin:25 FE
say.apply(person,["FE"]); // axuebin:25 FE
say.bind(person)("FE");   // axuebin:25 FE
  • 箭头函数: 没有自己的this,都指向外层。箭头函数会捕获其所在上下文的this值,作为自己的this值。
function Person(name){
  this.name = name;
  this.say = () => {
    var name = "xb";
    return this.name;
  }
}
var person = new Person("axuebin");
console.log(person.say()); // axuebin
  • 作为对象的一个方法: 指向调用函数的对象。
var person = {
  name: "axuebin",
  getName: function(){
    return this.name;
  }
}
console.log(person.getName()); // axuebin

这里要特别注意,必须要person直接调用它,而不是拥有它。

var name = "外面";
var person = {
  name: "axuebin",
  getName: function(){
    return this.name;
  }
}
var getName = person.getName;
console.log(getName()); // 外面
  • 作为一个构造函数:指向正在构造的新对象。
function Person(name){
  this.name = name;
  this.age = 25;
  this.say = function(){
    console.log(this.name + ":" + this.age);
  }
}
var person = new Person("axuebin");
console.log(person.name); // axuebin
person.say(); // axuebin:25
  • 作为一个DOM事件处理函数: 指向触发事件的元素,也就是始事件处理程序所绑定到的DOM节点
var ele = document.getElementById("id");
ele.addEventListener("click", function(e){
  console.log(this === e.target);   // true
})
  • HTML标签内联事件处理函数: this指向所在的DOM元素
<button onclick="console.log(this);">Click Me</button>
  • jQuery: 在许多情况下JQuery的this都指向DOM元素节点。
$(".btn").on("click",function(){
  console.log(this); 
});

手写call / apply / bind

call: 主要的思路就是把要执行的fn挂到context上作为一个它的值,这样调用的时候就是类似context.fn(),this就会指向context。

apply: 跟call基本一样,区别就是不用spread ...args,而是直接args作为一个参数。

bind bind稍微有点区别,是返回一个函数。还要判断返回的fn是不是用new调用的,用new调用的时候,传递给bind的第一个参数是会被忽略掉的。

这里需要特别注意的是,bind完以后,怎么判断是不是用new调用的呢?new Fn()的时候,this是指向新创建的对象x,x.__proto__是指向Fn.prototype的,所以this instanceof Fn就是true。

function F() { console.log(this.__proto__ === F.prototype ) }
F()       // false
new F()   // true
/* --------------- call --------------- */
Function.prototype.myCall = function(context, ...args){
  const cxt = context || window;    // 第一个参数为null或者undefined时,context(this)指向全局对象window

  // 为了能以对象调用形式绑定this,将当前被调用的方法定义在cxt.fn上。用Symbol避免跟context里面其它的key重复
  const fn = Symbol() 
  cxt[fn] = this;       // this指向实际要调用的方法fn,因为用的时候是fn.call(),call是被fn调用的

  // 以对象调用形式调用fn,此时this指向cxt,也就是传入的需要绑定的this指向
  const res = cxt[fn](...args);
  
  delete cxt[fn];   // 删除该方法,不然会对传入对象造成污染(添加该方法)

  return res;
}

/* --------------- apply --------------- */
Function.prototype.myApply = function(context, args = []){
  const cxt = context || window;
  const fn = Symbol() 
  cxt[fn] = this; 
  const res = cxt[fn](...args);
  
  delete cxt[fn];
  return res;
}

/* --------------- bind --------------- */
Function.prototype.myBind = function (context, ...args) {
  const fn = this   // this表示当前函数,这里用到了闭包把当前函数存起来  

  return function newFn(...newArgs) {
    const iscalledByNew = this instanceof newFn;     // 判断是不是作为构造函数调用, new fn() 而不是 fn()
    const allArgs = [...args, ...newArgs];

    return iscalledByNew
      ? new fn(...allArgs)   // 新函数如果被当做构造函数调用,这里也要用同样的方法调用
      : fn.apply(context, allArgs)
  }
}

/* --------------- test --------------- */
const obj = {
  arg1: 'a1',
  test: function (arg2 = 'a2', arg3 = 'a3') {
    console.log(this.arg1, arg2, arg3)
  }
}

const newObj = {
  arg1: 'n1',
}

obj.test();                                  // a1 a2 a3   默认的三个参数

obj.test.myCall(newObj);                     // n1 a2 a3
obj.test.myApply(newObj);                    // n1 a2 a3
obj.test.myBind(newObj)();                   // n1 a2 a3
new (obj.test.myBind(newObj))()              // undefined a2 a3  当作为构造函数的时候,this是指向自己,所以this.arg1是undefined

obj.test.myCall(newObj, 'n2', 'n3');         // n1 n2 n3
obj.test.myApply(newObj, ['n2', 'n3']);      // n1 n2 n3
obj.test.myBind(newObj, 'n2', 'n3')();       // n1 n2 n3
new (obj.test.myBind(newObj, 'n2', 'n3'))()  // undefined n2 n3

obj.test.myCall(null, 'n2');                 // undefined n2 a3
obj.test.myApply(null, ['n2']);              // undefined n2 a3
obj.test.myBind(null, 'n2')();               // undefined n2 a3
new (obj.test.myBind(newObj, 'n2'))()        // undefined n2 a3

obj.test.myBind(null, 'n2')('n3');           // undefined n2 n3
new (obj.test.myBind(newObj, 'n2'))('n3')    // undefined n2 a3

各种面试题

综合题

function Foo() {
  getName = function () { console.log(1); };
  return this;
}
Foo.getName = function () { console.log(2);};
Foo.prototype.getName = function () { console.log(3);};
var getName = function () { console.log(4);};
function getName() { console.log(5);}

Foo.getName();            // 2 这就是一个静态函数,比较直接
getName();                // 4 函数表达式var只是变量提升声明,赋值在后面。function函数声明会全部变量提升,所以函数表达式后面的赋值会覆盖掉函数声明。
Foo().getName();          // 1 Foo()里面覆盖了外面的getName,然后返回的this指向window。最后等于是调用window.getName()
getName();                // 1 被覆盖了
new Foo.getName();        // 2 new X不带参数(没括号),所以X.aaa先执行,等于是new (Foo.getName)(), 调用的是静态方法
new Foo().getName();      // 3 new X()是带参数的(有括号),所以优先级高,等于是(new Foo()).getName(), Foo实例自己没有getName,所以找到了prototype里面的
new new Foo().getName();  // 3  new ((new Foo()).getName)(),还是调用了原型上的函数。

细节1:变量提升

  • 所有变量声明或函数声明都会被提升到当前函数的顶部。但是如果是表达式,就会被拆成两次执行,一次声明(被提升),一次赋值。

声明的提升:

console.log('x' in window);  // true
console.log(window.x);       // undefined (只提升了但是还没赋值)
var x;
x = 0;

// 实际上的执行是
var x;
console.log('x' in window);  // true
console.log(window.x);       // undefined (只提升了但是还没赋值)
x = 0;

表达式拆分:

console.log(x);   // function x(){}
var x = '覆盖';
function x() {}
console.log(x);   // 覆盖

// 实际上的执行是
var x;
function x(){}
console.log(x);   // function x(){}
x = '覆盖';
console.log(x);   // 覆盖

细节2:this

function Foo() {
  // getName会一直向上找,最终查找到window对象,若window对象中也没有getName属性,就在window对象中创建一个getName变量。
  getName = function () { console.log(1); };

  // 注意只有在new一个函数的时候,this才会指向本身。如果是直接调用这个函数,this是正常指向的(指向调用者),这个例子中指向外面的window。
  return this;
}

细节3:JS运算符优先级

汇总表 最上面的几个主要优先级:

  • 括号 (x)
  • 成员访问 x.y
  • 需计算的成员访问 x[y]
  • 带参数的new new x(y)
  • 函数调用 x(y)
  • 可选链 ?.
  • 不带参数的new new x

细节4:构造函数的返回值

js中构造函数可以有返回值也可以没有。

  • 没有返回值则按照其他语言一样返回实例化对象。
  • 若有返回值则检查其返回值是否为引用类型。如果是非引用类型,如基本类型(string,number,boolean,null,undefined)则与无返回值相同,实际返回其实例化对象。
  • 若返回值是引用类型,则实际返回值为这个引用类型。
function X() {}
console.log(new X());               // X {}

function Y() { return 1 }
console.log(new Y());               // Y {}

function Z() { return { z: 'z'} }   // { z: 'z' }
console.log(new Z());

各种this小题

var a = 20;
function fn() {
  console.log(this.a);
}
fn();   // 20
var a = 20;
function fn() {
  var a = 10;
  function foo() {
    console.log(this.a);
  }
  foo();
}
fn();   // 20

// 解释:在fn中,foo()还是单独调用的,并不是x.foo()的形式,所以foo的this还是指向window。
var a = 20;
var obj = {
  a: 10,
  c: this.a,
  fn: function () {
    return this.a;
  }
}

console.log(obj.c);     // 20   实际上等于console.log(this.a),跟函数调用不太一样
console.log(obj.fn());  // 10

// 解释:单独的{}不会形成新的作用域,因此obj.c中的this.a,由于并没有作用域的限制,它仍然处于全局作用域之中。所以这里的this其实是指向的window对象。
var a = 10;
function foo() {
  var a = 20;
  var obj = {
    a: 30,
    c: this.a,
    fn: function () {
      return this.a;
    }
  }
  return obj.c;
}
console.log(window.foo());  // 10
console.log(foo());         // 10,其实不就等于window.foo()嘛,foo就是window上的

'use strict'
console.log(foo());         // 注意这里就会报错:Cannot read property 'a' of undefined,因为等于是调用了undefined.foo()
// 经典例子,不多说了
var a = 20;
var foo = {
  a: 10,
  getA: function () {
    return this.a;
  }
}
console.log(foo.getA());  // 10

var test = foo.getA;
console.log(test());      // 20
//经典例子变体1
var a = 20;
function getA() {
  return this.a;
}
var foo = {
  a: 10,
  getA: getA
}
console.log(getA());      // 20
console.log(foo.getA());  // 10
function foo() {
  console.log(this.a)
}

function active(fn) {
  fn(); 
}

var a = 20;
var obj = {
  a: 10,
  getA: foo
}

obj.getA()          // 10
active(obj.getA);   // 20

// 解释:虽然active调用的是obj.getA,但这这是外部间接的。真正调用foo的是在active里面,直接fn(),所以还是直接调用,并没有x.fn()的形式。
function log() { console.log(this.a); }
const getA = { y: { z: log, a: 5 } };

var a = 20;
const obj = {
  a: 10,
  getA: getA
}

obj.getA.y.z();     // 5, this指向最近的一个调用的context,也就是y,this.a就是y.a

如何解决this丢失的问题:

var obj = {
  a: 20,
  getA: function () {
    setTimeout(function () { console.log(this.a); }, 1000)
  }
}

obj.getA();   // undefined

// 解决方法1:经典的that = this
var obj = {
  a: 20,
  getA: function () {
    var that = this;
    setTimeout(function () { console.log(that.a); }, 1000)
  }
}

// 解决方法2:用bind
var obj = {
  a: 20,
  getA: function () {
    setTimeout(function () { console.log(this.a); }.bind(this), 1000)
  }
}

各种闭包小题

function fun(n,o) {
  console.log(o)
  return {
    fun: function(m) {
      return fun(m,n);
    }
  };
}
var a = fun(0); a.fun(1); a.fun(2); a.fun(3); 
var b = fun(0).fun(1).fun(2).fun(3);
var c = fun(0).fun(1); c.fun(2); c.fun(3);

// undefined 0 0 0 
// undefined 0 1 2
// undefined 0 1 1

总结

  • JS是基于词法作用域的(静态作用域),查找变量是根据书写的上下文,而不是函数调用的上下文。
  • 变量对象VO和AO:
    • 进入函数上下文(执行准备)时刻被创建,用来保存这个函数的上下文中的变量。所以会有变量提升。
    • 函数执行的时候可能会更新里面的变量。
  • 作用域链:
    • 在函数创建的时候,就会生成一个[[scope]],指向了一个数组,存储了从它上一层AO一直到全局VO这样一条链(注意这里还没有创建自己的AO,AO是之后运行时才创建的)。这条作用域链是创建的时候就决定的,而不是动态生成的,所以是静态作用域。
    • 在函数运行的时候,会创建执行上下文Context,可以理解为一个大的对象,里面也有一个scope属性,会把自己本身的AO加到之前那条[[scope]]作用域链里面,形成一条完整的作用域链: [this.AO, ...[[scope]]]
  • 闭包
    • MDN的广义定义:闭包是指那些能够访问自由变量(外部作用域)的函数,所有函数都是闭包。
    • 实用的狭义定义:
      • 即使创建此函数的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
      • 在代码中引用了自由变量
    • 实现方式:
      • 简单来讲:函数在执行的时候会放到一个执行栈上,当函数执行完毕之后会从执行栈上移除。但是,堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员。
      • 详细来讲:就算创建它的上下文已经销毁了,闭包的函数的作用域链里面还是存了之前上下文中的AO。
    • 例子:
      • 节流防抖,柯里化

references