0%

Js执行上下文和作用域链以及this

Js执行流程:

编译阶段

变量提升:

是指在JavaScript代码执行过程中,JavaScript引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”。变量被提升后,会给变量设置默认值,这个默认值就是我们熟悉的undefined

执行部分的代码

经过编译后,生成两部分内容:执行上下文和可执行代码

执行上下文包括变量环境,词法环境,外部引用Outer(指向外部的执行上下文)和this,一般包括三种:全局执行上下文,函数执行上下文(调用函数,函数内代码被编译,创建函数上下文,函数执行结束,上下文销毁),eval(使用eval函数时,eval的代码会被编译,并创建执行上下文)

执行上下文会被js引擎压入调用栈中,执行完毕后,会把执行上下文弹出栈

执行阶段

Js引擎开始执行可执行代码,按照顺序一行行执行,当出现相同的变量和函数,会保存到执行上下文的变量环境中,一段代码如果定义了两个相同名字的函数,那么最终生效的是最后一个函数,而

作用域和作用域链以及词法作用域

作用域指在程序定义变量的区域,该位置决定了变量的生命周期,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期

ES6之前只有全局作用域和函数作用域

  • 全局作用域中的对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期。
  • 函数作用域就是在函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁。

ES6后多了一个块级作用域,let和const会创建块级作用域

变量提升造成的危害:

1.变量容易在不被察觉的情况在被覆盖

2.本应该被销毁的变量没被销毁:

1
2
3
4
5
6
function foo(){
for (var i = 0; i < 7; i++) {
}
console.log(i);
}
foo()

由于变量提升,变量i在创建执行上下文阶段被提升,当for循环结束,变量i没有被销毁

ES6解决变量提升带来的缺陷:

使用let和const支持块级作用域,块作用域中的变量会被放到执行上下文中的词法环境中,而不是变量环境,因此块级作用域中的变量不会出现变量提升的现象

词法作用域:

指作用域是由代码中函数声明的位置决定的,所以词法作用域是静态的作用域,通过它能够预测代码在执行过程中如何查找标识符

词法作用域由代码声明时的位置决定,所以整个词法作用域链顺序:foo函数作用域->bar函数作用域->main函数作用域->全局作用域

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function bar() {
var myName = " 极客世界 "
let test1 = 100
if (1) {
let myName = "Chrome 浏览器 "
console.log(test)
}
}
function foo() {
var myName = " 极客邦 "
let test = 2
{
let test = 3
bar()
}
}
var myName = " 极客时间 "
let myAge = 10
let test = 1
foo()

它的执行上下文栈如下:

先在执行上下文中的词法环境中查找->变量环境->外部作用域,最后在全局执行上下文的词法环境中找到test

从执行上下文角度看闭包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function foo() {
var myName = " 极客时间 "
let test1 = 1
const test2 = 2
var innerBar = {
getName:function(){
console.log(test1)
return myName
},
setName:function(newName){
myName = newName
}
}
return innerBar
}
var bar = foo()
bar.setName(" 极客邦 ")
bar.getName()
console.log(bar.getName())

根据词法作用域的规则,内部函数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
2
3
4
5
6
7
8
9
var myObj = {
name : " 极客时间 ",
showThis: function(){
console.log(this)
function bar(){console.log(this)}
bar()
}
}
myObj.showThis()

解决:

1.在外层函数中用一个变量self保存this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var myObj = {
name : " 极客时间 ",
showThis: function(){
console.log(this)
var self = this
function bar(){
self.name = " 极客邦 "
}
bar()
}
}
myObj.showThis()
console.log(myObj.name)
console.log(window.name)

2 内部函数使用箭头函数的形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var myObj = {
name : " 极客时间 ",
showThis: function(){
console.log(this)
var bar = ()=>{
this.name = " 极客邦 "
console.log(this)
}
bar()
}
}
myObj.showThis()
console.log(myObj.name)
console.log(window.name)

箭头函数不会创建自身的执行上下文,因此箭头函数中的this取决于它的作用域链中的上一个执行上下文中的this

2. 普通函数中的 this 默认指向全局对象 window

上面我们已经介绍过了,在默认情况下调用一个函数,其执行上下文中的 this 是默认指向全局对象 window 的。

不过这个设计也是一种缺陷,因为在实际工作中,我们并不希望函数执行上下文中的 this 默认指向全局对象,因为这样会打破数据的边界,造成一些误操作。如果要让函数执行上下文中的 this 指向某个对象,最好的方式是通过 call 方法来显示调用。

这个问题可以通过设置 JavaScript 的“严格模式”来解决。在严格模式下,默认执行一个函数,其函数的执行上下文中的 this 值是 undefined,这就解决上面的问题了