JavaScript执行运行时 再解(二)
-
闭包
-
作用域链
-
执行上下文
-
this
闭包(closure)
闭包分为两个部分
- 环境部分
- 环境:函数的词法环境(执行上下文的一部分)
- 标识符列表:函数中用到的未声明的变量
- 标识符部分:函数体
执行上下文(处于环境当中)
- lexical environment:词法环境,当获取变量或者 this 值的时候使用
- variable environment:变量环境,当声明变量的时候使用
- code evaluation:用于恢复代码执行的位置
- Function:执行的任务是函数的时候使用,表示正在被执行的函数
- ScriptOrModule:执行的任务是脚本或者模块的时候使用,表示正在执行的代码
- Realm:使用的基础库和内置对象实例
- Generator:仅生成器上下文有这个属性,表示当前的生成器
看这里:
var b = {}
let c = 1
this.a = 2
想要正确执行,需要知道:
- var 把 b 变量声明到了哪里
- b 表示哪个变量
- b 的原型是哪个对象
- let 把 c 声明到了哪里
- this 指向哪个对象
这些需要知道的信息就由上下文来给出。
徒手撕闭包
closure 算是 JavaScript 面试喜欢问的东西,是 一个难点,也是整个 JavaScript 的亮点之一。据说,很多高级应用都是依靠闭包来实现的。。。我还没看过这些高级应用的源代码。
-
变量作用域
直接看三段代码:
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);
-
如何从外部读取局部变量?(从函数外部拿函数内部的值)
骚操作来了
我们有过这种情况吧,反正我有。有时候需要获得函数内部的局部变量,以前还以为一切皆无可能,但是,现在我知道了闭包。
在函数内部,再定义一个函数。
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
-
简单理解闭包
闭包就是定义在一个函数内部的函数(这么理解最简单),它能够把内部的函数所获取的函数内部的变量值返回出来,让外部环境获取函数内部的变量。
闭包有什么用?
- 读取函数内部的变量
- 让这些变量的值始终保持在内存中
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 的垃圾回收机制回收。
看两道题
-
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,来个闭包
-
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
所以,哈哈哈。以后可以拿闭包来干活了,但是
- 闭包内存消耗很大,不能滥用,不然会造成网页性能问题。在退出之前,把不适用的局部变量全部删除。
- 闭包把变量弄到了函数外部,这就导致函数内部的变量容易遭到更改,这个时候,别瞎改伙计。
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 中执行,所以表现出了不同的行为。