Javascript的作用域、执行上下文、This、闭包

109 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情

岁月如梭天天天,

寒来暑往年年年,

指点江山侃侃侃,

掘金文章发发发!

1.词法作用域和动态作用域

1.1 什么是作用域

  • 作⽤域是指程序源代码中定义变量的区域。
  • 作⽤域规定了如何查找变量,也就是确定当前执⾏代码对变量的访问权限。
  • JavaScript 采⽤词法作⽤域 (lexical scoping ),也就是静态作⽤域。

1.2 静态作⽤域和动态作⽤域

  • Javascript采用的是词法作用域(静态作用域),所以函数所在的作用域在定义时就决定了。
  • 而动态作用域是在函数调用时决定的。bash语言就是动态作用域。

2. 变量对象

2.1 什么是变量对象

  • 变量对象是执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。
  • 全局上下文和函数上下文的变量对象是不同的。

2.2 全局上下文

变量对象 - Variable object(VO)

  1. 全局对象是预定义的对象,作为 JavaScript 的全局函数和全局属性的占位符。通过使⽤全局对象, 可以访问所有其他所有预定义的对象、函数和属性。
  2. 在顶层 JavaScript 代码中,可以⽤关键字 this 引⽤全局对象。因为全局对象是作⽤域链的头,这意 味着所有⾮限定性的变量和函数名都会作为该对象的属性来查询。

全局对象Window就是全局上下文的变量对象。用 var 声明的全局变量会成为全局对象上的属性。

2.3 函数上下文

活动对象 - Activation object (AO)

活动对象和变量对象其实是⼀个东⻄,只是变量对象是规范上的或者说是引擎实现上的,不可在 JavaScript 环境中访问,只有到当进⼊⼀个执⾏上下⽂中,这个执⾏上下⽂的变量对象才会被激活,所 以才叫 activation object,⽽只有被激活的变量对象,也就是活动对象上的各种属性才能被访问。 活动对象是在进⼊函数上下⽂时刻被创建的,它通过函数的 arguments 属性初始化。arguments 属性值 是 Arguments 对象。

2.4 执行过程

执行上下文的代码分成两个阶段进行处理:分析和执行,可以理解为:

  1. 进入执行上下文
  2. 代码执行

2.4.1 进入执行上下文

变量对象包括:

  1. 函数的所有形式参数(如果是函数上下文)
    1. 由名称和对应值组成的一个变量对象的属性被创建;
    2. 没有实参,属性值设为 undefined;
  2. 函数声明
    1. 由名称和对应值(函数对象(function-object)组成一个变量对象的属性被创建;
    2. 如果变量对象已经存在相同名称的属性,则完全替换这个属性;
  3. 变量声明
    1. 由名称和对应值(undefined)组成一个变量对象的属性被创建;
    2. 如果变量名跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性;

例子:

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

进入执行上下文后,AO是:

AO = {
 arguments: {
 0: 1,
 length: 1
 },
 a: 1,
 b: undefined,
 c: reference to function c(){},
 d: undefined
}

2.4.2 代码执行

在执行阶段会按顺序执行,根据代码,修改变量对象的值。

上述例子执行之后的 AO :

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

变量创建过程总结:

  1. 全局上下⽂的变量对象初始化是全局对象;
  2. 函数上下⽂的变量对象初始化只包括 Arguments 对象;
  3. 在进⼊执⾏上下⽂时会给变量对象添加形参、函数声明、变量声明等初始的属性值;
  4. 在代码执⾏阶段,会再次修改变量对象的属性值;

3. This

3.1 Reference

reference 类型只存在于规范里的抽象类型,是为了更好地描述语言的底层行为逻辑的,并不存在与实际的js代码中。

Reference 的构成,由三个部分组成:

  • base value;
  • referenced name;
  • strict reference;

例子:

var foo = 1;

// 对应的Reference
var fooReference = {
    base:EnvirnomentRecord;
    name:'foo',
    strict:false
}


var foo = {
    bar:function(){
        return this
    }
}
foo.bar()
// bar 对应的Reference
var BarReference = {
   base:foo,
   propertyName:'bar',
   strict:false
}

规范内部提供了获取Reference组成部分的方法,GetValue和IsPropertyReference

  1. GetBase
  • 返回 base 的值。
  1. IsPropertyReference
  • 如果 base 的值是个对象,就返回 true。

3.2 确定this指向的判断逻辑

  1. 计算 MemberExpression 的结果赋值给 ref;
  2. 判断 ref 是不是一个 Reference 类型;
    1. 如果 ref 是 Reference,并且 IsPropertyReference(ref) 是true,那么 this 的值为 GetBase(ref);
    2. 如果 ref 是 Reference,并且base value 值是Environment Record,那么 this 的值为 ImplicitThisValue(ref);
    3. 如果 ref 不是 Reference,那么 this 的值为 undefined;

*注意:如果MemberExpression的结果中调用过了 GetValue 方法,那么返回的值就不是 Reference 类型。

var value = 1
var foo = {
  value:2,
  bar:function(){
    return this.value
  }
}

console.log(foo.bar()) // 2 
console.log((foo.bar)()) // 2 
// 这里因为由赋值运算符调用了GetValue(),所以this执行undefined,非严格模式下隐式转为window,所以值为1
console.log((foo.bar = foo.bar)()) // 1
console.log((false || foo.bar)()) // 1
console.log((foo.bar,foo.bar)()) // 1

4. 闭包

MDN 对闭包的定义为:

闭包是指那些能够访问自由变量的函数。

自由变量:

自由变量是指在函数中使用的,但既不是函数参数也不是函数局部变量的变量。

由此,闭包共有两部分组成:

闭包 = 函数 + 函数能够访问的自由变量。

闭包的两种说法

  1. 从理论⻆度:所有的函数。因为它们都在创建的时候就将上层上下⽂的数据保存起来了。哪怕是简 单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问⾃由变量,这个时候使⽤最外 层的作⽤域;
  2. 从实践⻆度:以下函数才算是闭包: a. 即使创建它的上下⽂已经销毁,它仍然存在(⽐如,内部函数从⽗函数中返回); b. 在代码中引⽤了⾃由变量;