Js执行流程:
编译阶段
变量提升:
是指在JavaScript代码执行过程中,JavaScript引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”。变量被提升后,会给变量设置默认值,这个默认值就是我们熟悉的undefined。
执行部分的代码
经过编译后,生成两部分内容:执行上下文和可执行代码
执行上下文包括变量环境,词法环境,外部引用Outer(指向外部的执行上下文)和this,一般包括三种:全局执行上下文,函数执行上下文(调用函数,函数内代码被编译,创建函数上下文,函数执行结束,上下文销毁),eval(使用eval函数时,eval的代码会被编译,并创建执行上下文)
执行上下文会被js引擎压入调用栈中,执行完毕后,会把执行上下文弹出栈
执行阶段
Js引擎开始执行可执行代码,按照顺序一行行执行,当出现相同的变量和函数,会保存到执行上下文的变量环境中,一段代码如果定义了两个相同名字的函数,那么最终生效的是最后一个函数,而
作用域和作用域链以及词法作用域
作用域指在程序定义变量的区域,该位置决定了变量的生命周期,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期
ES6之前只有全局作用域和函数作用域
- 全局作用域中的对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期。
- 函数作用域就是在函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁。
ES6后多了一个块级作用域,let和const会创建块级作用域
变量提升造成的危害:
1.变量容易在不被察觉的情况在被覆盖
2.本应该被销毁的变量没被销毁:
1 | function foo(){ |
由于变量提升,变量i在创建执行上下文阶段被提升,当for循环结束,变量i没有被销毁
ES6解决变量提升带来的缺陷:
使用let和const支持块级作用域,块作用域中的变量会被放到执行上下文中的词法环境中,而不是变量环境,因此块级作用域中的变量不会出现变量提升的现象
词法作用域:
指作用域是由代码中函数声明的位置决定的,所以词法作用域是静态的作用域,通过它能够预测代码在执行过程中如何查找标识符
词法作用域由代码声明时的位置决定,所以整个词法作用域链顺序:foo函数作用域->bar函数作用域->main函数作用域->全局作用域
例子:
1 | function bar() { |
它的执行上下文栈如下:
先在执行上下文中的词法环境中查找->变量环境->外部作用域,最后在全局执行上下文的词法环境中找到test
从执行上下文角度看闭包:
1 | function foo() { |
根据词法作用域的规则,内部函数getName和setName总是可以访问到外部函数foo中的变量,左移当InnerBar对象返回给全局变量bar后,虽然foo函数已经执行结束,但是getName和setName函数依然可以使用foo函数中的变量myName和test1,当foo函数执行完成后,整个调用栈状态如下:
闭包定义:在JavaScript中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束,但是内部函数引用外部函数的变量依然保存在内存中,我们把这些变量的集合称为闭包。
闭包回收
如果引用闭包的函数是全局变量,那么闭包会一直存在直到页面关闭;但如果这个闭包以后不再使用的话,就会造成内存泄漏。
如果引用闭包的函数是个局部变量,等函数销毁后,在下次 JavaScript 引擎执行垃圾回收时,判断闭包这块内容如果已经不再被使用了,那么 JavaScript 引擎的垃圾回收器就会回收这块内存。
所以在使用闭包的时候,你要尽量注意一个原则:如果该闭包会一直使用,那么它可以作为全局变量而存在;但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它成为一个局部变量。
在执行上下文的视角讲this
作用域链和this是两套不同的系统,
this 是和执行上下文绑定的,也就是说每个执行上下文中都有一个 this。执行上下文主要分为三种——全局执行上下文、函数执行上下文和 eval 执行上下文,所以对应的 this 也只有这三种——全局执行上下文中的 this、函数中的 this 和 eval 中的 this。
全局执行上下文中的 this
全局执行上下文中的 this 是指向 window 对象的。这也是 this 和作用域链的唯一交点,作用域链的最底端包含了 window 对象,全局执行上下文中的 this 也是指向 window 对象
#函数执行上下文中的 this
1. 通过函数的 call 方法设置
2. 通过对象调用方法设置
3. 通过构造函数中设置
this 的设计缺陷以及应对方案
1. 嵌套函数中的 this 不会从外层函数中继承
我认为这是一个严重的设计错误,并影响了后来的很多开发者,让他们“前赴后继”迷失在该错误中。我们还是结合下面这样一段代码来分析下:
1 | var myObj = { |
解决:
1.在外层函数中用一个变量self保存this
1 | var myObj = { |
2 内部函数使用箭头函数的形式
1 | var myObj = { |
箭头函数不会创建自身的执行上下文,因此箭头函数中的this取决于它的作用域链中的上一个执行上下文中的this
2. 普通函数中的 this 默认指向全局对象 window
上面我们已经介绍过了,在默认情况下调用一个函数,其执行上下文中的 this 是默认指向全局对象 window 的。
不过这个设计也是一种缺陷,因为在实际工作中,我们并不希望函数执行上下文中的 this 默认指向全局对象,因为这样会打破数据的边界,造成一些误操作。如果要让函数执行上下文中的 this 指向某个对象,最好的方式是通过 call 方法来显示调用。
这个问题可以通过设置 JavaScript 的“严格模式”来解决。在严格模式下,默认执行一个函数,其函数的执行上下文中的 this 值是 undefined,这就解决上面的问题了