JS中一些重要概念

一、作用域

概念:作用域即为变量(或函数)的可见范围,限定了他们的生命周期。

ES5环境:函数作用域、全局作用域;

ES6环境:在ES5的基础上引入块级作用域,支持块级作用域、函数作用域、全局作用域;

 

上图代码举例说明:橙色范围是全局作用域、绿色是函数作用域、蓝色为块级作用域

实际上块级作用域的概念是包含函数/全局作用域的,只是为了便于区分,这里分开写。

 

二、执行上下文

概念:在JS代码执行之前会先进行编译(这里包括全局的代码以及函数内代码),这些代码只有在第一次执行时会进行编译,编译的结果是生成程序的 执行上下文可执行代码 两部分。执行上下文即为js代码的运行环境,包含执行期间用到的this、变量、函数、对象等。

对于ES5环境,只有在第一次执行函数时,会进行编译并且生成执行上下文,也就是只有函数(或全局)具有执行上下文。ES6中为了实现块级作用域,在执行上下文中加入了词法环境,将执行上下文分为了变量环境和词法环境,变量环境保持了ES5中的可执行上下文,即函数内可用变量信息(这里指非let/const声明的变量),词法环境则保存可执行上下文中let/const声明的可使用的变量信息。

也就是函数内的代码块是跟随函数一起进行编译的,编译后执行上下文已经生成,包含了变量环境以及词法环境,也就是在编译阶段就已经确认了函数内以及代码块内的可调用变量情况,只不过变量环境中的变量(及函数)已经创建,但词法环境中的变量(及函数)只有在执行到相应代码块时才会被创建。

1 function varTest() {
2   var x = 1;
3   if (true) {
4     let x = 2;  // 名称相同的变量!
5     console.log(x);  // 2
6   }
7   console.log(x);  // 1
8 }

以上代码在函数执行前的执行上下文如下图所示:

  当代码执行到if代码块时,函数执行上下文如下图所示:

既然变量环境中的变量(及函数)在编译阶段就已经随着执行上下文创建了,那么理论上在变量声明之前是可以使用它的,这也就引出了JS的变量提升特性。

 

三、变量提升

概念:JS代码中,可以在变量声明之前使用它,可以认为JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码的开头,这种行为被称为变量提升。

实际中,变量提升会给JS初学者带来一些困扰,因此ES6中新增的let、const声明变量不容许变量提升(但实际上还是有的!)。

(ES6环境下,变量提升可以总结为以下表格)

变量类型 提升范围 提升的内容 说明
var 函数作用域、全局作用域 创建、初始化(undefined) var声明的变量会在函数或全局作用域内进行提升,变量的创建、初始化会被提升到开头,赋值则保持在原来的声明位置。
let/const 块级作用域、函数作用域、全局作用域 创建、初始化(undefined) let/const是不容许变量提升的,也就是不能在声明前使用,但实际实现过程中确实是将创建、初始化提升到了开头,赋值保持在声明位置,只是JS引擎禁止了声明前进行访问,这也就是所谓的暂时死区。
function 块级作用域、函数作用域、全局作用域 创建、初始化(undefined)、赋值

function的提升不同于变量,可以称其为函数提升,函数提升不同于变量提升会将函数声明体整个提升至代码开头,相当于进行了赋值。也就是无论在代码什么位置声明,只要在作用域范围内均可访问执行该函数。

 

有一点需要注意:为了引入块级作用域并且向下兼容,在ES6中并没有改变var 而是新增了let/const来替代var,但并没有引入新关键字来替代function,这也使得function的变量提升会出现一些很难理解的现象。比如下面代码执行结果:

var a = 1;
{
  a = 99;
  function a(){}
  a = 30;
}
console.log(a);  //99(ES6环境)

为什么不是 1 呢,如果函数a 进行了变量提升,那么应该是提升到 { } 代码块的开头,也就是会创建一个局部变量a并指向一个内容为空的函数,接下来代码块中赋值99 以及 30 都是对局部变量就行的赋值,代码块执行结束后,局部变量a随之销毁,那么打印的应该是全局变量a的值,也就是1。但实际执行过程并非如此。

块级作用域中非函数/全局的代码块中的函数声明,不仅仅会在代码块中进行提升,而且会重复提升至代码块上级作用域中,但此次的提升只是创建和初始化的提升,并没有赋值,而赋值保持在了原来的声明位置,且赋值的内容是代码块中的相应变量值。

也就是说在非函数/全局的代码块中声明的函数,有如下特性:

(1)变量提升两次:一次提升至代码块开头,块级作用域的可执行上下文中创建了局部的函数变量,并进行了初始化和赋值。 另一次提升至代码块外部最近的函数或全局作用域,再外部作用域创建了同名的变量,并初始化为undefined。
(2)声明变赋值:既然只提升了创建和初始化到外部函数/全局作用域,那么赋值操作在哪里进行呢,答案是原来的声明位置,也就是在代码块的声明位置对 外部作用域的同名变量进行赋值,赋值的内容是块级作用域中的同名局部变量。

此时再回头看上面代码,就不难理解为何打印结果是99了:代码块中的 99 和 30 的赋值语句改变的是局部的变量a,而在function a(){}声明位置又将 值为99的局部变量a 赋值给了全局变量a,所以最后打印的全局变量a的结果变为了99而非1。

四、调用栈

概念:调用栈是管理函数调用关系的一种类似栈的数据结构。根据调用先后顺序等关系,将相应函数的执行上下文按照顺序压入调用栈中,执行结束后再弹出栈顶,这样就可以追踪函数的调用关系了。无论是内存空间还是调用深度,调用栈都是有极限的,也就是说函数是不能无限制的嵌套调用的。

执行上下文中的词法环境也是一种栈结构,记录了块级作用域内的let/const声明的变量信息,在代码块的执行开始,压入栈顶。并随着执行结束,再将栈顶的词法结构弹出,这样就实现了块级作用域。

 

五、作用域链

概念:在JS中,当前作用域是可以访问上级作用域的变量的,作用域链即是为了解决如何访问上级变量的问题。

在函数的执行上下文中,存在着一个outer指针,指向的就是上级作用域的执行上下文环境,沿着outer指针就可以找到上级作用域的上下文,从而访问上级作用域中变量。(全局执行上下文的outer指针为null)

在一个执行上下文内,变量的查找顺序则是先在词法环境中查找,没有则再去变量环境中找,均没有则沿着outer指针继续在上级执行上下文中查找,这就实现了如何在块级作用域中查找变量。

既然outer指针存放在执行上下文中,也就是其指向在代码的编译阶段就已经确定了,其是根据词法作用域确定的,是静态的,并不会因为代码的执行而动态变化,也就是说函数的声明位置就已经确定了它的执行上下文中outer的指向了,并不会因为函数的调用位置而发生改变。

var a = 1;
function b(){
  console.log(a);
}
function c(){
  var a = 2;
  b();
}
c();//打印结果:1

如以上代码打印的是全局变量a,而非函数c内局部变量a的值。

这是由于函数b执行过程中,发现自己的执行上下文中并不存在变量a,所以沿着作用域链进行查找,而outer指针指向的是全局执行上下文,所以找到了全局变量a,并打印。

 

六、闭包

概念:闭包是一块封闭和私有的内存空间,它属于一个已经执行完并销毁了的函数,它只能被在此函数内部声明的函数所访问,而这个内部声明的函数往往被赋值给了其他变量。

虽然闭包所在函数已经执行完毕,但闭包内变量,并不会随着函数执行结束而被JS引擎的垃圾回收机制处理掉,只有在被内部声明函数赋值的变量销毁时,对应的闭包才会被回收。

 

七、this

概念:this是执行上下文中的一个默认有的动态引用类型指针,与上述outer指针类似,但outer是用于实现作用域链,而this则用于实现调用环境(普遍的需求是:在对象内部的方法中使用内部的属性),且outer的值是在编译阶段由词法上下文决定的,是静态的。this的值则是动态变化的,与函数具体调用方式相关。

但是这里要特别注意,this和作用域链是两套没有任何关系的机制,要区分开,不可混淆。

类型:this随着执行上下文也分为全局执行上下文中的this、函数执行上下文中的this、eval执行上下文中的this

全局执行上下文中的this,指向的是全局对象globalThis,浏览器环境就是window对象

函数执行上下文的this,指向的是调用者,也就是谁调用的函数,this就指向谁,且是最近的调用者。例如:obj1.obj2.func(); // func中this指向的是obj1.obj2对象。如果没有调用者则this指向的是全局对象,严格模式下this为undefined

函数中this可以通过call、apply、bind等属性来更改。

运算符 new 也可以更改函数的this指向,使用new 调用函数,主要做了以下四步操作:

(1)首先创建一个新的空对象tempObj;

(2)将空对象tempObj的__proto__指向函数的prototype对象;

(3)调用函数的call方法,将函数的this指向空对象tempObj;

(4)执行函数

(5)如果函数有显式返回一个引用类型(即对象),则将该引用类型作为最后结果返回,否则返回tempObj;

例外:

箭头函数:ES6中的箭头函数的执行上下文中并没有this指针,这也就导致了箭头函数并不满足以上规则,箭头函数的this使用的是上层作用域中的this指针,相当于箭头函数中的this指针的值会沿着作用域链进行查找。

DOM事件处理函数:浏览器环境下,当函数被用作事件处理函数时,它的this指向的是触发事件的元素,但有一些非addEventListener添加的监听函数例外,如setTimeout等的回调函数this默认指向的是window。

 

八、原型链

JS中每个对象都有一个_ _proto_ _ 属性,指向创建该对象的构造函数的原型prototype,然后通过_ _proto_ _属性将对象连接起来,组成一个原型链,用来实现继承。

prototype是函数自带的一个对象属性,专门用于通过该函数构造的对象实现继承使用。

例外:

箭头函数:ES6中加入的箭头函数不仅没有this指针,同样也没有prototype,所以箭头函数是不可以用于构造函数使用的。

posted @ 2021-01-02 17:32  Aoobruce  阅读(142)  评论(0)    收藏  举报