JavaScript基础:执行上下文与作用域链

778 阅读11分钟

执行上下文

什么是执行上下文

执行上下文就是当前代码的执行环境,每当js代码在运行的时候,它都是在执行上下文中运行。

执行上下文栈

执行栈就是其他语言中的调用栈,是一种LIFO数据结构的栈,它被用来存储代码运行时创建的执行上下文。
当js引擎第一次遇到要执行的代码的时候,首先会创建一个全局的执行上下文并压入当前执行栈,每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈顶,js引擎执行栈顶的函数,当该函数执行完毕,执行上下文从栈中弹出,控制流程到达下一个上下文。

执行上下文的三种类型

image.png

全局执行上下文
js引擎解析js代码最开始遇到的就是全局代码,初始化的时候会向调用栈中压入一个全局执行的上下文,只有当整个应用程序结束的时候才会清空执行栈,栈底永远都是全局执行上下文。
全局执行上下文只有一个,全局执行环境是最外围的一个执行环境,在浏览器的全局对象是 window, this指向这个对象。

函数执行上下文
可以有无数个,函数被调用的时候才会被创建。每次调用函数都会创建一个新的执行上下文。

eval和with执行上下文
很少使用

执行上下文的三个重要属性

对于每一个执行上下文都含有三个重要属性:变量对象,作用域链,this。

image.png

变量对象:
每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。我们编写的代码无法访问,只有解析器在处理数据时在后台使用。
在执行上下文中用活动对象表示变量对象,只有当进入一个执行环境时,这个执行上下文的变量对象才会被激活,此时成为活动对象,只有活动对象上的属性才能被访问

作用域链:
当代码在一个环境中执行时,会创建变量对象的一个作用域链。 作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。

this

执行上下文的生命周期

image.png

创建阶段
a. 创建变量对象: 初始化函数参数 -> 函数声明 -> 变量声明
b. 创建作用域链: 函数的作用域在函数定义的时候就确定了。作用域链本身包含变量对象,当查找变量时,会先从当前上下文中的变量对象中查找,如果没有找到,就会从父级执行上下文的变量对象中查找,一直找到全局执行上下文的变量对象
c. 确定this的指向

执行阶段
执行变量赋值,代码执行。

回收阶段
执行上下文出栈被垃圾回收机制进行回收。

作用域和作用域链

什么是作用域

指变量的可访问性和可见性。也就是说整个程序中哪些部分可以访问这个变量。

作用域决定这个变量的生命周期及其可见性。 当我们创建了一个函数或者 {} 块,就会生成一个新的作用域。
需要注意的是,通过 var 创建的变量只有函数作用域,而通过 let 和 const 创建的变量既有函数作用域,也有块作用域。

作用域的价值

  1. 安全,变量只能在特定的区域被访问,可以避免程序在其他位置意外修改变量。
  2. 减轻变量命名压力。

作用域的类型

image.png

全局作用域
全局作用域下声明的变量可以在程序的任意位置访问。任何不在函数中或大括号中声明的变量都在全局作用域下。

函数作用域
函数作用域也叫局部作用域,如果一个变量是在函数内部声明的它就在一个函数作用域下面。这些变量只能在函数内部访问,不能在函数以外去访问。

块级作用域:\

使用一个变量时,首先js引擎会在当前作用域寻找该变量,如果没找到就会到它的上层作用域查找,直到找到该变量或已到全局作用域。
如果在全局作用域仍找不到,会在全局范围内隐式声明该变量(非严格模式)或直接报错。

this 问题

this与执行上下文绑定,每个执行上下文都有一个this。

this分三类:

  • 全局执行上下文中的this
  • 函数中的this
  • eval中的this

函数的this是在调用时绑定的,this的取值完全取决于函数的调用位置

this指向确认规则

1、如果一个函数中有this,但是它没有被上一级的对象所调用,那么this指向的就是window(严格模式下为undefined)

2、如果一个函数中有this,这个函数有被上一级的对象所调用,那么this指向的就是上一级的对象。

3、如果一个函数中有this,这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,this指向的也只是它上一级的对象

var o = {
    a:10,
    b:{
        a:12,
        fn:function(){
            console.log(this.a); //12
        }
    }
}
o.b.fn();

4、 this永远指向的是最后调用它的对象

var o = {
    a:10,
    b:{
        a:12,
        fn:function(){
            console.log(this.a); //undefined
            console.log(this); //window
        }
    }
}
var j = o.b.fn;
j();

构造函数中的this

new关键字可以改变this的指向 (详见new一个新对象的过程发生了什么?)

function Fn(){
    this.user = "追梦子";
}
var a = new Fn();
console.log(a.user); //追梦子

this碰到return时的指向

如果返回值是一个对象,那么this指向的就是那个返回的对象;(该规则对null无效

如果返回值不是一个对象,那么this还是指向函数的实例。

function fn(params) {
    this.user = 'leemo'
    return {}
}
var a = new fn
console.log(a.user) // undefined

function fn(params) {
    this.user = 'leemo'
    return { user: 'ltc'}
}
var a = new fn
console.log(a.user) // ltc
 
function fn2(params) {
    this.user = 'leemo'
    return function(){}
}
var b = new fn2
console.log(b.user) // undefined
 
function fn3(){
    this.user = 'leemo'
    return undefined
}
var c = new fn3
console.log(c.user) // leemo

延时函数的this

超时调用(setTimeout回调)的代码都是在全局作用域环境中执行的,因此(setTimeout回调)函数中this的值在非严格模式下指向window对象,在严格模式下是undefined

function foo() {
    console.log("id1:", this.id);
    console.log("this1:", this);
    setTimeout(function() {
        console.log("id2:", this.id);
        console.log("this2:", this);
    }, 0);
}

var id = 21;

foo();

// Chrome
// id1: 21
// this1: window
// id2: 21
// this2: window

foo.call({id: 42});

// Chrome
// id1: 42
// this1: {id: 42}
// id2: 21
// this2: window

箭头函数的this

箭头函数最大的特色就是没有自己的this、arguments、super、new.target,且箭头函数没有原型对象prototype,不能用作构造函数(new一个箭头函数会报错)。

因为没有自己的this,所以箭头函数中的this其实指的是包含函数中的this

无论是点调用,还是call调用,都无法改变箭头函数中的this

function foo() {
    console.log("id1:", this.id);
    console.log("this1:", this);
    setTimeout(() => {
        console.log("id2:", this.id);
        console.log("this2:", this);
    }, 0);
}

var id = 21;

    foo();

// Chrome
// id1: 21
// this1: window
// id2: 21
// this2: window

foo.call({id: 42});

// Chrome
// id1: 42
// this1: {id: 42}
// id2: 42
// this2: {id: 42}

因为箭头函数(setTimeout回调)没有自己的this,导致其内部的this引用了外层代码块的this,即foo函数的this

注意:在定义阶段(调用函数前),foo函数的this的值并不确定,但箭头函数的this自定义阶段开始就指向foo函数的this

又因为使用call方法改变了foo函数运行(调用)时其函数体内this的指向(重新指向对象{id: 42})从而使箭头函数中this的指向发生变化,最后输出了例子所示结果。

改变this的指向

  1. 使用 new
  2. 使用 bind
function fn(){
    console.log(this.name);
};
var obj={
    name:'jack',
};
var b=fn.bind(obj);
b();
  1. 使用call()apply() 方法

他们也可以用来调用函数,这两个方法都接受一个对象作为参数,用来指定本次调用时函数中this的指向。

call方法使用的语法规则
函数名称.call(obj,arg1,arg2...argN);
参数说明:
obj:函数内this要指向的对象,
arg1,arg2...argN :参数列表,参数与参数之间使用一个逗号隔开

function fn(name){
    this.name=name;
    this.fn1=function(){
        console.log(this.name);
    }
};
var obj={};
fn.call(obj,'jack');
console.log(obj.name);
obj.fn1();

apply方法使用的语法规则
函数名称.apply(obj,[arg1,arg2...,argN])
参数说明:
obj :this要指向的对象
[arg1,arg2...argN] : 参数列表,要求格式为数组

call和apply的作用一致,区别仅仅在函数实参参数传递的方式上
这个两个方法的最大作用基本就是用来强制指定函数调用时this的指向

function fn(name,age){
    this.name=name;
    this.age=age;
    this.fn1=function(){
        console.log(this.name);
    }
};
var obj={};
fn.apply(obj,['jack',18]);
console.log(obj.age);
obj.fn1();

扩展问题

词法作用域和动态作用域的理解和区别

JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。函数的作用域在函数定义的时候就决定了。 而与词法作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的

var value = 1;

function foo() {
    console.log(value);
}

function bar() {
    var value = 2;
    foo();
}

bar();

如果是词法作用域 输出 1,如果是动态作用域 输出2

通过词法作用域理解闭包

核心就是函数的作用域在定义时就已经确定了,如果理解了这句话,也就理解了闭包。 在全局作用域中“定义”一个函数到时候,只会创建包含全局作用域的作用域链。
只有“执行”该函数的时候,才会复制创建时的作用域,并将当前函数的局部作用域放在作用域链的顶端 。

JavaScript是采用词法作用域的,这就意味着函数的执行依赖于函数定义的时候所产生(而不是函数调用的时候产生的)的变量作用域。为了去实现这种词法作用域,JavaScript函数对象的内部状态不仅包含函数逻辑的代码,除此之外还包含当前作用域链的引用。 函数对象可以通过这个作用域链相互关联起来,如此,函数体内部的变量都可以保存在函数的作用域内,这在计算机的文献中被称之为闭包。

从技术的角度去讲,所有的JavaScript函数都是闭包:他们都是对象,他们都有一个关联到他们的作用域链。绝大多数函数在调用的时候使用的作用域链和他们在定义的时候的作用域链是相同的,但是这并不影响闭包。

当调用函数的时候闭包所指向的作用域链和定义函数时的作用域链不是同一个作用域链的时候,闭包become interesting。这种interesting的事情往往发生在这样的情况下: 当一个函数嵌套了另外的一个函数,外部的函数将内部嵌套的这个函数作为对象返回。一大批强大的编程技术都利用了这类嵌套的函数闭包,当然,javascript也是这样。可能你第一次碰见闭包觉得比较难以理解,但是去明白闭包然后去非常自如的使用它是非常重要的。

通俗点说,在程序语言范畴内的闭包是指函数把其的变量作用域也包含在这个函数的作用域内,形成一个所谓的“闭包”,这样的话外部的函数就无法去访问内部变量。所以按照第二段所说的,严格意义上所有的函数都是闭包。

需要注意的是:我们常常所说的闭包指的是让外部函数访问到内部的变量,也就是说,按照一般的做法,是使内部函数返回一个函数,然后操作其中的变量。这样做的话一是可以读取函数内部的变量,二是可以让这些变量的值始终保存在内存中。