JavaScript执行运行时 再解(二)

  • 闭包

  • 作用域链

  • 执行上下文

  • this

    68f50c00d475a7d6d8c7eef6a91b2152

闭包(closure)

闭包分为两个部分

  • 环境部分
    • 环境:函数的词法环境(执行上下文的一部分)
    • 标识符列表:函数中用到的未声明的变量
  • 标识符部分:函数体

执行上下文(处于环境当中)

  • lexical environment:词法环境,当获取变量或者 this 值的时候使用
  • variable environment:变量环境,当声明变量的时候使用
  • code evaluation:用于恢复代码执行的位置
  • Function:执行的任务是函数的时候使用,表示正在被执行的函数
  • ScriptOrModule:执行的任务是脚本或者模块的时候使用,表示正在执行的代码
  • Realm:使用的基础库和内置对象实例
  • Generator:仅生成器上下文有这个属性,表示当前的生成器

看这里:

var b = {}
let c = 1
this.a = 2

想要正确执行,需要知道:

  1. var 把 b 变量声明到了哪里
  2. b 表示哪个变量
  3. b 的原型是哪个对象
  4. let 把 c 声明到了哪里
  5. this 指向哪个对象

这些需要知道的信息就由上下文来给出。

徒手撕闭包

closure 算是 JavaScript 面试喜欢问的东西,是 一个难点,也是整个 JavaScript 的亮点之一。据说,很多高级应用都是依靠闭包来实现的。。。我还没看过这些高级应用的源代码。

  1. 变量作用域

    直接看三段代码:

    var n = 666;
    function fn() {
    	console.log(n);
    }
    fn(); // 666
    

    上面这个函数在函数内部直接读取了全局变量。

    function fn() {
    	var n = 666;
    }
    console.log(n); // error
    

    上面这个函数试图从global 读取 function 内部的局部变量。

    function fn2() {
      m = 666;
    }
    fn2()
    console.log(m)
    
    // fn2顺利输出 666
    // fn1执行失败
    // 这是因为,函数声明变量的时候,不用 var 命令,就声明成了全局变量
    
    function fn1() {
    	var n = 666;
    }
    fn();
    console.log(n);
    
  2. 如何从外部读取局部变量?(从函数外部拿函数内部的值)

    骚操作来了

    我们有过这种情况吧,反正我有。有时候需要获得函数内部的局部变量,以前还以为一切皆无可能,但是,现在我知道了闭包。

    在函数内部,再定义一个函数。

    function f1() {
    	var n = 666;
    	function f2() {
    		console.log(n); // 999
    	}
    }
    

    f2 在 f1的函数作用域里,这个时候 f1 的所有作用域变量(局部变量)都可以被 f2访问,反之则是不行的。JavaScript 特有的“链式作用域”,子对象会一级一级地向上查找父级对象的变量。

    所以,f2 可以读取 f1 中的局部变量,那么我们把 f2 return 回来,不就可以在 f1外部拿到它的内部变量了吗???

    function f1() {
    	var n = 666;
    	function f2() {
    		console.log(n);
    	}
    	return f2;
    }
    var result = f1();
    result(); // 666
    
  3. 简单理解闭包

    闭包就是定义在一个函数内部的函数(这么理解最简单),它能够把内部的函数所获取的函数内部的变量值返回出来,让外部环境获取函数内部的变量。

    闭包有什么用?
    1. 读取函数内部的变量
    2. 让这些变量的值始终保持在内存中

    show me the code

    function f1() {
    	var n = 666;
    	nAdd = () => n++;
    	function f2() {
    		console.log(n);
    	}
    	return f2;
    }
    var result = f1();
    // f2 就是一个闭包,把f1 中的全部变量丢到了外面的部分
    result() // 666
    nAdd();
    result(); // 667
    

    局部变量 n 一直存在于内存里面,没有被清除。

    为什么这样?f1 是f2 的父函数,而 f2 被赋给了一个全局变量,这就导致 f2 始终在内存里面。因此,不会被 JavaScript 的垃圾回收机制回收。

    看两道题
    1.   var name = "The Window";
      
        var object = {
          name : "My Object",
      
          getNameFunc : function(){
            return function(){
              return this.name;
            };
      
          }
      
        };
      
        alert(object.getNameFunc()()); // The window
      

      很明显,这里的 this 指向的是 global,函数里的 this 从 object 更改到了 global(对于浏览器来说是 window,对于 node 来说是 global)。

      如果要不改变 this 的话,写成箭头函数:

        var name = "The Window";
      
        var object = {
          name : "My Object",
      
          getNameFunc : function(){
      
            return () => this.name
      
          }
      
        };
      
        console.log(object.getNameFunc()());
      

      这样就输出了 my object

      或者,看 2,来个闭包

    2.   var name = "The Window";
      
        var object = {
          name : "My Object",
      
          getNameFunc : function(){
            var that = this;
            return function(){
              return that.name;
            };
      
          }
      
        };
      
        alert(object.getNameFunc()());// "My Object"
      

      我们来解释一下,在这段代码里,我们使用了 that(以前的老前辈喜欢用_this)来存储当前的 this 指向,在上面的代码里,that 指向了 getNameFuc,getNameFunc 运行环境的对象是 object,所以我们输出 that.name的时候,实际上就是输出了 object.name

    所以,哈哈哈。以后可以拿闭包来干活了,但是

    1. 闭包内存消耗很大,不能滥用,不然会造成网页性能问题。在退出之前,把不适用的局部变量全部删除。
    2. 闭包把变量弄到了函数外部,这就导致函数内部的变量容易遭到更改,这个时候,别瞎改伙计。

var 声明与赋值

var b = 1

通常我们认为它声明了变量 b,并且把变量 b 赋值为 1,var 声明作用域函数执行的作用域。也就是说,var 有变量提升,会穿透 for、if 等语句。(这肯定是个设计上的缺陷、失败)。

ES6 之前是只有 var,没有 let、const 的时代,那个时候诞生了一个技巧,IIFE(immediately invoked function expression)。它通过创建一个函数,来构造一个新的域,防止 var 的变量提升。

(function () {
	var a;
	// enter some code
}())
(function () {
	var a;
	// enter some code
})();

我个人写JavaScript 的 IIFE 时更喜欢使用第二种。

但是,括号有个缺点,就是上一行代码如果不加分号,括号会被解释为上一行最末尾的函数调用…(???我以前不知道这个的),产生完全不符合预期并且难以调试的行为,加号运算符也有这种问题。所以一些推荐不加分号的代码风格规范,会要求在括号前面加上分号。

我不喜欢写分号,少按几次键嘛,还是懒。

;(function() {
	var a;
	// enter some code
}())
;(function() {
  var a;
  // enter some code
})()

说真的写这么复杂的时候会不会被打

推荐的写法是这个样子

void function() {
	var a;
  // enter some code
}();

这样写避免了语法问题。同时,void 运算表示忽略后面函数的返回值,对于 IIFE 来说这样语义也更合理。

值得特别注意的是,有时候 var的特性会导致声明的变量和赋值的变量是两个 b,JavaScript 中有特例:

var b;
void function () {
	var env = {b: 1};
	b = 2;
	console.log("I'm function b: ", b);
	with(env) {
		var b = 3;
		console.log("I'm with b:", b);
	}
}();
console.log("Global b:", b);	

在这个例子中,我们利用立即执行的函数表达式(IIFE)构造了一个函数的执行环境,并且在里面使用了我们一开头的代码。

可以看到,在 Global function with三个环境中,b 的值都不一样。而在function 环境中,并没有出现 var b,这说明 with 内的 var b 作用到了 function这个环境中。这也是一些人坚决的反对在任何场景下使用 this 的原因。

let

let 是 ES6 开始引入的新的变量声明模式,比起 var 的诸多弊病,let 做了非常明确的梳理和规定。

为了实现 let,JavaScript 在运行时引入了块级作用域。也就是说,在 let 出现之前,JavaScript 的 if for 等语句皆不产生作用域。

我简单统计了下,以下语句会产生 let 使用的作用域:

  • for
  • if
  • switch
  • try/catch/finally

能用 let 就用 let ,能用 const 就用 const,用 var 一定要在文件最开头声明好,别给自己、别给他人挖坑。

Realm

前端进阶训练营里面没有看懂这是个啥东西,那就再看一看

首先,这个词翻译不成中文,翻译过来没用,就像 hadoop。

我们看这个代码

var b = {}

在 ES2016 之前的版本中,标准中甚少提及{}的原型问题。但在实际的前端开发中,通过 iframe 等方式创建多 window 环境并非罕见的操作,所以,这才促成了新概念 Realm 的引入。

Realm 中包含一组完整的内置对象,而且是复制关系。

对不同 Realm 中的对象操作,会有一些需要格外注意的问题,比如 instanceOf 几乎是失效的。

以下代码展示了在浏览器环境中获取来自两个 Realm 的对象,它们跟本土的 Object 做 instanceOf 时会产生差异:

var iframe = document.createElement('iframe')
document.documentElement.appendChild(iframe)
iframe.src="javascript:var b = {};"

var b1 = iframe.contentWindow.b;
var b2 = {};

console.log(typeof b1, typeof b2); //object object

console.log(b1 instanceof Object, b2 instanceof Object); //false true

可以看到,由于 b1、 b2 由同样的代码“ {} ”在不同的 Realm 中执行,所以表现出了不同的行为。

原文地址:https://www.cnblogs.com/ssaylo/p/13156234.html