作用域和作用域链

作用域

定义:是名字与实体的绑定保持有效的那部分计算机程序(规定可访问变量范围的一套规则)
全局:1、全局对象下的变量 2、最外层声明的变量和方法
这里有一个注意细节:全局声明的变量,使用 var 声明的变量,会被挂载到全局对象 window 之下。而使用 let/const 声明的变量,并不会修改 window 对象,而是被挂载到一个新的对象 Script 之下。

  1. var a = 100
  2. const b = 20
  3. function test() {
  4. return a + b;
  5. }
  6. console.log(window.b)
  7. console.log(window.a)
  8. console.dir(test)

函数:函数声明和函数表达式可以让{ }具备作用域,称之为函数作用域。函数作用域中声明的变量与方法,只能被下层子作用域访问,不能被其他不相关的作用域访问。 es6之前没有块级作用域的存在,想要让某段代码拥有独立作用域,之前的做法就是用自执行函数来模拟,本质上还是函数作用域。 (示例:(function(){console.log(1)})() 、 !function(){console.log(1)}() )
块级作用域:
let/const 声明最核心的特点:它声明的变量,能够被任何{ }约束。这也就是我们常说的块级作用域。

函数作用域和块级作用域的经典代码:

  1. var arr = [1, 2, 3, 4, 5];
  2. for (var i = 0; i < arr.length; i++) {
  3. setTimeout(()=>{
  4. console.log(i)
  5. })
  6. }
  7. console.log(i);

作用域链

高级程序设计4:作用域链其实是一个包含指针的列表,每个指针分别指向一个变量对象,但是物理上不包含响应的对象。
在上一篇文章:函数-执行环境中已经了解到作用域范围的部分信息在预解析阶段就已经确定。下面我们用chrome的调试工具来验证一下:

  1. function foo() {
  2. let a = 1;
  3. let b = 2;
  4. let c = 3
  5. function bar() {
  6. let d = c
  7. function inner() {
  8. return a;
  9. }
  10. console.dir(inner)
  11. }
  12. console.dir(bar)
  13. bar()
  14. }
  15. foo()

对于 bar 和 inner 来说,他们的 [[Scopes]] 属性仅存在内部访问的 foo 函数作用域中的变量,因此这样的结果是符合优化原则的,那些没有被访问的变量声明不会解析到作用域链中。
下面是es标准介绍环境的一段话:
image.png
在当前函数中,要寻找到变量的值是从哪里来的,就首先会从自己的环境中查找,如果没有找到,则会去保存的外部环境中查找。这里需要注意的是,作用域链本身就是存在于函数对象中的一个属性 [[Scopes]],该属性是在代码预解析阶段就已经确认好的。而经常说的一层一层的往上查找,并不是执行时查找的,而是预解析阶段就查找好存储的。

这里还有个local对象,这个在下面的执行上下文章节中会说到。

闭包

定义

维基百科:闭包(Closure),又称词法闭包或函数闭包,是在支持头等函数的编程语言中实现词法绑定的一种技术。
MDN:一个函数和对其词法环境的引用捆绑在一起,这样的组合就是闭包。
高程:引用了另一个函数作用域中变量的函数。

不管是哪里给的闭包解释都是同一个结构:函数体 + 上层环境

生命周期

闭包的生命周期没有规定的概念,单纯是我拿来帮助理解闭包的一个说法,主要想说明闭包从产生到销毁的过程。

产生:闭包是基于词法作用域的规则产生,通过闭包可以让函数内部可以访问函数外部的声明,而函数的 [[Scopes]] 属性,是在解析阶段确认。所以闭包在代码解析时就能确定。(也有人说是函数创建阶段,其实也是对的,因为我们前面说过解析过程检测到函数的创建就进行预解析,预解析的过程之后Scopes属性中就存在了Closure)
存储:闭包对象「Closure」的引用存在于自身的 [[Scopes]] 属性中。也就是说,只要函数体在内存中持久存在,闭包就会持久存在。
回收:通过前面我们知道,在预解析阶段,函数声明会创建一个函数体,并在代码中持久存在。这里持久存在的意义就是为了随时访问。当不需要被访问了函数体自然就会被回收,而闭包和函数体就是同时回收的。

  1. // 情况1:
  2. function foo() {
  3. let a = 10;
  4. let b = 20;
  5. function bar() {
  6. a = a + 1;
  7. console.log(a)
  8. const c = 30;
  9. return a + b + c;
  10. }
  11. console.dir(bar)
  12. return bar
  13. }
  14. // 函数作为返回值的应用:此时实际调用的是 bar 函数
  15. foo()()
  16. foo()()
  17. foo()()

当函数 foo 执行,会创建函数体 bar,并作为 foo 的返回值。foo 调用完毕,则对应创建的执行上下文会被回收,此时 bar 作为 foo 执行上下文的一部分,自然也会被回收。那么保存在 bar.[[Scopes]] 上的闭包对象,自然也会被回收。
因此,多次执行 foo()(),实际上是在创建多个不同的 foo 执行上下文,中间与 bar 创建的闭包对象,始终都没有被保存下来,会随着 foo 的上下文一同被回收。因此,多次执行 foo()(),实际上创建了不同的闭包对象,他们也不会被保留下来,相互之间也不会受到影响。

总结

  • 闭包的产生主要是函数内部访问了上层作用域的声明
  • 函数体的[[Scopes]]中存在了一个真实的Closure对象方便理解闭包
  • 闭包是在代码解析阶段,根据词法作用域的规则产生的
  • 闭包对象可以被垃圾回收机制回收

    适用场景

    单例模式 vuex

执行上下文

生命周期:
image.png
在之前比较常见的理论中,执行上下文有三个属性 变量对象、作用域链、this 。大家有兴趣可以去看一下 冴羽的文章
但是在es5标准中,将变量对象、活动变量、作用域链统称为词法环境,且我们在前面说到作用域链的时候有说过,作用域链在解析(parser)阶段就已经创建。

所以执行上下文创建阶段更合理的解释是:初始化词法环境、确认this指向。

初始化词法环境

这个过程通过debug看一下local对象
<<<<<————————————————————-
增加(变量对象方向)
变量对象其实是老概念,但是我考虑了一下感觉local对象上的部分内容确实和变量对象很像,毕竟是换了个名字,确实应该再说一下变量对象,你也可以理解为初始化词法环境的一部分。
变量对象本身其实就是执行上下文里有一个对象用来放执行上下文中可访问的内容(类似local对象不包含this)。
它的过程有三步骤

  • 建立arguments对象。检查当前上下文中的参数,建立该对象下的属性与属性值。
  • 检查当前上下文的函数声明,也就是使用function关键字声明的函数。在变量对象中以函数名建立一个属性,属性值为指向该函数所在内存地址的引用。
  • 检查当前上下文中的变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性,属性值为undefined。

上面这个步骤说是变量对象的创建也行,说是词法环境的几个步骤也没问题,只是换了个名字。
另外于变量对象对应的还有个活动对象,其实是进入上下文后,也就是开始执行,变量对象就变成活动对象了,一个是静态的一个是可改变可访问的动态的。就是一个关机的电脑和一个已经开机的电脑的区别。
————————————————————————————>>>>
此过程引申出的一个概念:变量提升

var: console.log(a) // undefined var a = 20
let/const/class : console.log(a) // Uncaught ReferenceError: Cannot access ‘a’ before initialization let a = 20
function: console.log(foo) // ƒ foo() {} function foo() {}
使用 function 关键字声明的函数,在变量提升中的体现与 var/let/const 都不一样。function 声明的函数,在初始化时,就会直接赋值指向对应的函数体。
function 不同的原因在于:function提升是刻意的,为了解决问题而提升的
Dmitry 的文章中 Brendan Eich给出的答案
讨论到最后的总结:
image.png
变量提升是人为实现的问题,而函数提升在当初设计时是有目的的。

this

this是一个特殊的对象,该对象动态且隐式地传递给上下文,我们可以将其视为隐式的额外参数,可以访问但不能更改。
this 的指向,是在执行上下文的创建过程中,被确定的,由此可知一个函数的this指向变得很灵活。
根据前文中我们把上下文分为全局执行上下文和函数执行上下文,所以我在这里也先说一下全局上下文创建时的this指向
全局上下文是一个比较特殊的存在,在全局上下文中,this 指向全局对象本身。这个结论本身也不复杂。
函数中的this
如果调用者函数,被某一个对象所拥有,那么该函数在调用时,内部的this指向该对象。如果函数独立调用,那么该函数内部的this,则指向undefined。但是当在非严格模式中,当this指向undefined时,它会被默认指向全局对象。

  1. // "use strict"
  2. var a = 20;
  3. let obj = {
  4. a: 10,
  5. getA: function () {
  6. console.log(this.a)
  7. }
  8. }
  9. let obj1 = {
  10. a: 40
  11. }
  12. obj.getA() // 10
  13. let test = obj.getA
  14. test() // 20
  15. obj.getA.call(obj1) // 40 显示绑定
  1. function Person(age) {
  2. this.name = 'Tom';
  3. this.age = age;
  4. }
  5. var p = new Person(20);
  6. console.log(p.name) // Tom new绑定

箭头函数中的this
箭头函数中的this是不能被call或apply改变的,并且永远指向函数声明时所在的词法作用域。

  1. var a =10
  2. f1=()=> {
  3. console.log(this.a)
  4. }
  5. let test ={
  6. a : 20,
  7. f2: ()=> {
  8. console.log(this.a)
  9. }
  10. }
  11. let obj = {
  12. a: 30
  13. }
  14. f1.call(obj) // 10 call无法改变箭头函数的指向
  15. test.f2() // 10 不指向test,而是指向外层调用者

原理可借鉴: http://dmitrysoshnikov.com/ecmascript/javascript-the-core-2nd-edition/#this
https://github.com/mqyqingfeng/Blog/issues/7

原理简述: MemberExpression 获取ref —>判断ref是不是Reference类型 —> 1、如果 ref 是 Reference,并且 IsPropertyReference(ref) 是 true, 那么 this 的值为 GetBase(ref) 2、IsPropertyReference(ref) 是false 返回ImplicitThisValue(ref),计算结果是undefind 3、不是Reference直接返回undefind。