文章目录
前言
这一部分是JavaScript中比较难以理解但又非常重要的一部分,他包含变量提升,this的指向,实现let的块区间,闭包,作用域链等,只有理解了这一部分,我们才能更深一步的认识到JavaScript的运行过程,才能从根源去认识和解决问题。曾经很多的地方只知道要这么做却不知道为什么这么做,在学习这部分后才明白,并且能理解之前做法的一些不好,可以优化的的地方。执行上下文
定义
执行执行上下文是JavaScript执行一段代码时的运行环境,比如调用一个函数,就会进入这个函数的执行上下文,确定该函数在执行期间用到的诸如this、变量、对象以及函数等。
- JavaScript执行代码时,会先编译创建上下文,并执行(赋值,加减等操作)。
- 在编译全局代码时,会创建全局上下文,在整个生命周期中只有一个噢,且处于栈底。
- 编译函数内的代码时,会创建函数上下文,正在运行的会处于栈顶,一般在调用完成后就会出栈(销毁)
- 如下图。执行上下文存在于调用栈(也叫执行上下文调用栈)中,每一个上下文都有它的变量环境,词法环境,this,outer。
1.执行栈的实际过程
下面我们看一个案例,我们用一段代码来看看具体在调用栈的过程
var a = 10;
var b = 20;
function add(a , b) {
return a+b;
}
add(a,b)
第一步
- 首先创建全局的执行山下文,并且压入栈,在这里还没有对其进行赋值,所以a/b的值都为undefined。
- 全局执行上下文压入栈后,JavaScript引擎就会开始执行代码,就会进行赋值操作。
第二步
- 在这里执行到函数 add ,就会创建函数add的函数执行上下文,并且把该上下文压入栈,因为方法add里面没有变量,就不必赋值,在该执行栈运算得到结果,就会返回。
第三步
- 在上面完成后会弹出函数add的执行栈(被垃圾回收机制回收),变为下图,到这里整个过程就完成了。
所以执行上下文其实就是跟踪每一个函数的执行的机构,是当前代码运行的环境,全局的处于最底层,函数的处于相应的函数上下文,通过作用域链实现连接
2.栈溢出
- 补充说一点为啥会有栈溢出,因为当我们不断创建函数方法时,并没有对没用的进行释放,不断的增加函数执行上下文,栈空间满了。
3.预解析
- JavaScript引擎在运行js代码的时候分为两步:预解析和代码执行
- 预解析: JavaScript引擎会把js里面所有的 var 还有 function提升到当前作用域的最前面(也叫我们前面的声明,同时还有产生的怪异操作,变量预解析(变量提升)与函数预解析(函数提升))
- 代码执行:按照代码顺序从上往下执行(对应我们的赋值部分,但不止该操作)
变量提升
在上面我们可以看到每一个执行栈里面还有四个部分,接下来我们先来看一下变量环境这一部分
- 我在前面写过的关于变量详细说明的文章,可以去看一下噢,在上面说道的上下文我们知道在创建上下文的同时会变量环境会包含变量,方法,但值为undefined,创建完成到执行部分才回赋值。现在为了个好的理解变量提升,我们联合下面代码和上面的内容来探索变量提升。
var = a = "你好"
//等价与下面写法
var a; //声明
a = "你好" //赋值
变量提升的定义
- 是指在JavaScript代码执行过程中,JavaScript引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”。变量被提升后,会给变量设置默认值,这个默认值就是我们熟悉的undefined。
我们先来看一个例子
![add();
console.log(number);
var number = 10;
function add() {
console.log(' 啦啦啦啦啦')
}
console.log(number);](https://img-blog.csdnimg.cn/20210505161110656.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDE4MTE4MA==,size_16,color_FFFFFF,t_70)
知道这里输出的结果吗?
下面我们通过一张图来说明变量提升的过程吧。
过程
- 首先开辟栈空间,创建执行上下文,给代码进行执行的环境
- 编译创建全局上下文,把var number = undefined 和函数 add() 放入到变量环境。
- 全局上下文代码执行,调用方法add()输出“啦啦啦啦啦”,因为这时还没有给number赋值,所以接下来输出的是undefined ,之所以没有标错,就是变量提升的结果,再后面执行赋值后输出的就是10了。
注意
函数和变量一样存在变量提升,并且函数的优先级高于变量,且不会发生同名覆盖,但变量赋值后会覆盖
//先声明变量a,再到函数a
var a ;
function a() {}
console.log(a) // 输出ƒ a() {}
// 给变量赋值为3
function a() {}
var a = 3;
console.log(a) // 输出3
小试牛刀
console.log(a);
console.log(add());
var a = 10;
function add() {
console.log(b);
var b = 20;
console.log(b);
c = 30;
}
add()
console.log(a);
console.log(b);
console.log(c);
你能知道上面的输出结果吗?
作用域
定义
- 作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。作用域具有一套规则来确保当前代码对变量函数等的访问权限。
- 类似引擎来问作用域,问:作用域大哥那个变量a有没有? 答:有的,刚刚编译器来声明了
- 在ES5之前,还没有Let和Const两个关键词,所以就只有函数作用域和全局作用域。但在这之后,Let和Const两个关键词的引入带来了块级作用域。
- 全局作用域:该对象在全局都能访问到,它的生命周期和页面的等同。
- 函数作用域:只能在该函数里访问到,生命周期随函数结束而结束销毁。
- JavaScript引擎:从开始到结尾,负责整个JavaScript程序的编译和执行过程。
- 编译器:语法分析和代码生成等
- 作用域:负责收集并维护由所有声明的变量和函数组成的一系列查询,而且有一定的规则,确定当前执行的代码对这些标识符的访问权限。
1.作用域和上下文的区别
其实这两者还挺相像的,很多人甚至把他们以为同一体,但我们要注意区分两者,实际上是不同的最大的区别在于动态和静态,作用域是静态的,创建好之后是不会在改变的,但上下文是一种环境,调用时创建,结束释放。前者在定义时产生,后者在创建时产生。
2.JavaScript如何实现块级作用域
块级作用域(私有作用域)
确保了外界不能访问到该区域内的变量,防止了污染全局,常由 if 或 if else 以及 { }, 还有用到 let 和 const 这两个关键词。块级作用域不可跨域访问,并且会有“暂时性死区”(声明前不可访问,这也就是 let 和 const 的特性)
下面我们通过下面的一段代码,分析它的上下文,来理解JavaScript如何实现块级作用域,同时我们会讲到我们之前看到词法环境。
function add (a) {
let b = 20;
var c = 30;
{
let b = 40
}
return a+b+c
}
- 首先编译阶段,我们上面说过,变量和函数会声明到变量环境中,但在这里,let 和 const不是声明到变量环境,而是放到词法环境。
- 再下来会对执行到代码块,会对前面的进行赋值了,并且把代码块里的 let b 加到词法环境,在这里词法环境是一个栈结构的没回把代码块压入栈,当处理完闭,就会把该代码块弹出栈。
- 在函数作用域的变量的访问过程是从词法环境开始访问到变量环境的(所以上面的代码中,访问变量C的过程如下)
3.LHS和RHS查询
4.作用域链
定义
作用域链其实就是作用域的集合,当内部找不到时会一步一步往上找
- 在查找变量时,会先从当前的内部,在到上一级,最后全局
- 我们定义变量最好定义在局部变量中,因为全局变量在最末端,会导致效率低。
- 每一个作用域里都有一个作用域的退出机制我们把它命名为 outer
var a = 10;
var b = 20;
function add1() {
var b = 30
add2()
add3()
function add2() {
var a = 20
console.log( 1 , a) //20
console.log( 2 ,b) // 30
}
}
function add3() {
console.log(3 ,b) // 20
}
add1()
你猜对每一个输出的答案了吗?全对了吗?恭喜你,那我们下面的内容你就能跟轻松啦,如果错了?让我猜猜,是不是错在第三个输出的结果是20,而不是30是吧?没关系,这正是我们这里的重点!!认真听咯!!
上面出错的同学,你是否以为函数 add3() 指向函数 add1() 呢?但实际上不是的,他是指向全局的。那么为什么add2()的是指向add1()呢?我们是如何确认作用链的指向呢?其实这里是根据词法作用域确认的。
作用域链与原型链的区别:
- 当访问一个变量时,解释器会先在当前作用域查找标识符,如果没有找到就去父作用域找,作用域链顶端是全局对象window,如果window都没有这个变量则报错。
- 当在对象上访问某属性时,首选i会查找当前对象,如果没有就顺着原型链往上找,原型链顶端是null,如果全程都没找到则返一个undefined,而不是报错。
5.词法作用域
定义
- 词法作用域就是指作用域是由代码中函数声明的位置来决定的,也就是你写的代码位置。因为这个原因词法作用域是静态的,处理代码时会保持作用域不变,并且我们能借此预测出变量的声明和赋值位置
- 可以通过eval()和 with 来修改(欺骗词法作用域,但不建议这样,会降低效率)
下面我们通过下面的代码来分析词法作用域
var a = 10;
var b = 20;
function add1() {
var b = 30
add2()
add3()
function add2() {
var a = 20
console.log( 1 , a) //20
console.log( 2 ,b) // 30
}
}
function add3() {
console.log(3 ,b) // 20
}
下面我们通过两幅图,来充分的展示上面代码的结构
6.闭包
定义
- 闭包是由函数以及声明该函数的词法环境组合而成的。由上面的词法作用域,我们知道闭包也就是在一个内层函数中你可以访问到其外层函数的作用域,同时每当创建一个函数,闭包就会在函数创建的同时被创建出来。
- 当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包
function add() {
var a = 10;
function show() {
console.log(a)
}
return show
}
var adds = add()//这行相当于var adds = show()
adds()
在这里调用add(),结束了,返回show(),按道理add()已经结束了,局部变量a的值应该访问不了,但实际上是可以的,这是为啥呢?就是我们前面说的闭包,下面通过一张图你就能很好的理解了。(注意闭包在栈空间存放的是一个访问地址,实际存放在堆空间)
1.探索闭包存放的位置
我们创建一个非常大的 变量a 和 变量b 我们可以发现在堆空间里发现 变量a ,那么还有个问题,所有 函数add()里的变量都会存放到闭包吗?我们通过下面的的图也能看出,只有被内部函数引用的变量才会加入到闭包里。
function add() {
var a = new Array(1000000).join('a');
var b = new Array(1000000).join('b');
function show() {
console.log(a)
}
return show
}
var adds = add()
adds()
执行后在对空间里的内存。
2.闭包的回收
因为闭包的性质,在根据垃圾回收的原则,只要被引用的就不会被回收,这样就很容易导致内存泄露。我们应该注意对闭包的使用。
- 闭包被一个全局的函数引用,他就不会被回收直到页面被关闭,这样要是一直没有被引用就会内存泄露了。
- 闭包被一个全局的函数引用的话,他会随函数的销毁而不在被引用,直到垃圾回收机制回收。
所以我们原则上:闭包会一直使用的就创建为全局变量,反之为局部变量。
this
这个玩意有时候非常非常麻烦,我们有时甚至不知道他指向到哪个部分。下面我们来尽量梳理它的规律和变化。
为啥需要this
我们知道JavaScript是根据词法作用域生成的,词法作用域是静态的,这就决定了,我们创建好后,就无法更改了。就没办法就对象内部的方法中使用对象内部的属性这个普遍的需求。
如何决定this的调用对象
第一步:调用位置
我们需要分析出关于该对象的调用位置,通过这个才能正确的分析出
这个我们可以通过 console.log() 来输出调用过程
function f() {
console.trace();
}
function d() {
f()
}
const b = function () {
d();
};
const c = function () {
d()
};
function a() {
b();
c();
}
a()
调用路径的输出如下图
第二步:绑定规则
下面我们分析一下this的绑定规则,能进一步理解如何分析this的指向。
1.默认绑定(独立函数绑定)
function add() {
console.log(this.a)
}
var a = 2;
add() ;//2
这里的this指向的是全局环境,在这里 this.a 也就等于 var a = 2。为什么呢?因为函数add是没有加修饰的(如:a.add() 等通过对象调用什么的)什么都没有影响的,他的this就不会受到其它的影响,保持默认的指向全局。也是我们最基本常用的。
- 比如这里,一个vue项目里,我们在方法getadmin() 中 curPage:this.curPage 就是让方法中的 curPage 指向data里,全局的 curPage了,这里就是我们用到的默认绑定
2.隐式绑定(通过对象绑定)
function add() {
console.log(this.a)
}
var obj {
var a:2;
add:add;
}
obj.add() ;//2
这里的this指向的对象obj,在这里 this.a 也就等于 obj.a。为什么呢?因为函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。注(多个对象时只对一个起作用 如 obj1.obj2.add() 只会指向obj2)
3.隐式丢失
这个是在隐式绑定下发生不符合我们的预计的情况,
function add() {
console.log(this.a)
}
var obj {
var a:2;
add:add;
}
var aa = obj.add() ;//2
aa() //undefined
var a = 22;
aa() //22
在这里 aa 实际上只是函数add()的引用地址(一切都是对象),所以实际上是一个不带任何修饰的函数调用,因此应用了默认绑定。在第一次调用时是先变量提升,未赋值所以为 undefined ,第二次赋值后为去全局的 22 ,不是obj里面的 2 ;
4.参数传递
function add() {
console.log(this.a)
}
function add1(fn) {
fn();
}
var obj {
var a:2;
add:add;
}
var a = 22;
add1(obj.add) //22
参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果和上一个例子一样。
5.箭头函数里的this
箭头函数里没有this
6.new绑定
function add(a) {
this.a = a
}
var aa = new add(2)
console.log(aa.a)
使用new来调用foo(…)时,我们会构造一个新对象并把它绑定到foo(…)调用中的this上。new是最后一种可以影响函数调用时this绑定行为的方法,我们称之为new绑定
7.显式绑定(call/bind/apply)
这三个方法就是用来改变this的指向函数的。
这三者的区别
- all、apply与bind都用于改变this绑定,但call、apply在改变this指向的同时还会执行函数,而bind在改变this后是返回一个全新的boundFcuntion绑定函数
- 2.bind属于硬绑定,返回的 boundFunction 的 this 指向无法再次通过bind、apply或 call 修改;call与apply的绑定只适用当前调用,调用完就没了,下次要用还得再次绑。
- call与apply功能完全相同,唯一不同的是call方法传递函数调用形参是以散列形式,而apply方法的形参是一个数组。在传参的情况下,call的性能要高于apply,因为apply在执行时还要多一步解析数组。
这几个函数还挺重要的,也必须能了解原理手写出来,我们在下篇来详细介绍。
8.总结this
- 看 this 在哪个做作用域中?
- 全局 —> this = window
- funcion 函数体中。
- 看函数执行。
- 哪一个函数被执行?再看方法体中 this 指向谁?
- 是否有call apply bind ,有—> 参一是谁 this 就指向谁
- 是否是事件处理函数; 是----> 触发事件元素
- 调用函数是否是new 是构造函数,this 指向new 实例的对象
- 都不满足。谁调用指向谁。
总结
这里的内容很多,并且难以理解,但我们还是要尽量去明白,才便于我们开发的过程遇到问题时能分析出问题的所在和原因。