资讯专栏INFORMATION COLUMN

一次搞定this和闭包

Airy / 908人阅读

摘要:他的组成如下对的就是你关注的那个变量对象作用域链跟闭包相关由于是单线程的,一次只能发生一件事情,其他事情会放在指定上下文栈中排队。

闭包和this,是两个相当高频的考点,然而你有没有想过,实际上他们两个都跟同一个知识点相关?

有请我们的这篇文章的主角,执行上下文

执行上下文
执行上下文是什么

可以简单理解执行上下文是js代码执行的环境,当js执行一段可执行代码时,会创建对应的执行上下文。他的组成如下

executionContextObj = {
    this: 对的就是你关注的那个this,
    VO:变量对象,
    scopeChain: 作用域链,跟闭包相关
}

由于JS是单线程的,一次只能发生一件事情,其他事情会放在指定上下文栈中排队。js解释器在初始化执行代码时,会创建一个全局执行上下文到栈中,接着随着每次函数的调用都会创建并压入一个新的执行上下文栈。函数执行后,该执行上下文被弹出。

五个关键点:

单线程

同步执行

一个全局上下文

无限制函数上下文

每次函数调用创建新的上下文,包括调用自己

执行上下文建立的步奏

创建阶段

初始化作用域链

创建变量对象

创建arguments

扫描函数声明

扫描变量声明

求this

执行阶段

初始化变量和函数的引用

执行代码

this

在函数执行时,this 总是指向调用该函数的对象。要判断 this 的指向,其实就是判断 this 所在的函数属于谁。

指向调用对象
function foo() {
    console.log( this.a );
}

var obj = {
    a: 2,
    foo: foo
};

obj.foo(); // 2
指向全局对象
function foo() {
    console.log( this.a );
}

var a = 2;

foo(); // 2

注意

//接上

var bar = foo

a = 3
bar() // 3不是2

通过这个例子可以更加了解this是函数调用时才确定的

再绕一点

function foo() {
    console.log( this.a );
}

function doFoo(fn) {
    this.a = 4
    fn();
}

var obj = {
    a: 2,
    foo: foo
};

var a =3

doFoo( obj.foo ); // 4

function foo() {
    this.a = 1
    console.log( this.a );
}

function doFoo(fn) {
    this.a = 4
    fn();
}

var obj = {
    a: 2,
    foo: foo
};

var a =3

doFoo( obj.foo ); // 1

这是为什么呢?是因为优先读取foo中设置的a,类似作用域的原理吗?

通过打印foo和doFoo的this,可以知道,他们的this都是指向window的,他们的操作会修改window中的a的值。并不是优先读取foo中设置的a

因此如果把代码改成

function foo() {
    setTimeout(() => this.a = 1,0)
    console.log( this.a );
}

function doFoo(fn) {
    this.a = 4
    fn();
}

var obj = {
    a: 2,
    foo: foo
};

var a =3

doFoo( obj.foo ); // 4
setTimeout(obj.foo,0) // 1

上面的代码结果可以证实我们的猜测。

用new构造就指向新对象
a = 4
function A() {

    this.a = 3
    this.callA = function() {
        console.log(this.a)
    }
}

A() // 返回undefined, A().callA会报错。callA被保存在window上

var a = new A()

a.callA() // 3,callA在new A返回的对象里
apply/call/bind

大家应该都很熟悉,令this指向传递的第一个参数,如果第一个参数为null,undefined或是不传,则指向全局变量

a = 3
function foo() {
    console.log( this.a );
}

var obj = {
    a: 2
};

foo.call( obj ); // 2

foo.call( null ); // 3

foo.call( undefined ); // 3

foo.call(  ); // 3

var obj2 = {
    a: 5,
    foo
}

obj2.foo.call() // 3,不是5!

//bind返回一个新的函数
function foo(something) {
    console.log( this.a, something );
    return this.a + something;
}

var obj =
    a: 2
};

var bar = foo.bind( obj );

var b = bar( 3 ); // 2 3
console.log( b ); // 5
箭头函数

箭头函数比较特殊,没有自己的this,它使用封闭执行上下文(函数或是global)的 this 值。

var x=11;
var obj={
 x:22,
 say:()=>{
   console.log(this.x); //this指向window
 }
}
obj.say();// 11
obj.say.call({x:13}) // 11
x = 14
obj.say() // 14

//对比一下

var obj2={
 x:22,
 say() {
   console.log(this.x); //this指向obj2
 }
}
obj2.say();// 22
obj2.say.call({x:13}) // 13

事件监听函数

指向被绑定的dom元素

document.body.addEventListener("click",function(){
    console.log(this)
}
)

// 点击网页

// ...
HTML

HTML标签的属性中是可能写JS的,这种情况下this指代该HTML元素。

变量对象

变量对象是与执行上下文相关的数据作用域,存储了上下文中定义的变量和函数声明。

变量对象式一个抽象的概念,在不同的上下文中,表示不同的对象

全局执行上下文的变量对象

全局执行上下文中,变量对象就是全局对象。
在顶层js代码中,this指向全局对象,全局变量会作为该对象的属性来被查询。在浏览器中,window就是全局对象。

var a = 1
console.log(window.a) // 1
console.log(this.a) // 1
函数执行上下文的变量对象

函数上下文中,变量对象VO就是活动对象AO。

初始化时,带有arguments属性。
函数代码分成两个阶段执行

进入执行上下文时
此时变量对象包括

形参

函数声明,会替换已有变量对象

变量声明,不会替换形参和函数

函数执行

根据代码修改变量对象的值

举个例子

function test (a,c) {
  console.log(a, b, c, d) // 5 undefined [Function: c] undefined
  var b = 3;
  a = 4
  function c () {

  }
  var d = function () {

  }
  console.log(a, b, c, d) // 4 3 [Function: c] [Function: d]

  var c = 5
  console.log(a, b, c, d) // 4 3 5 [Function: d]

}

test(5,6)

来分析一下过程

1.创建执行上下文时

VO = {

arguments: {0:5},
a: 5,
b: undefined,
c: [Function], //函数C覆盖了参数c,但是变量声明c无法覆盖函数c的声明
d: undefined, // 函数表达式没有提升,在执行到对应语句之前为undefined

}

执行代码时

通过最后的console可以发现,函数声明可以被覆盖

作用域链

先了解一下作用域

作用域

变量与函数的可访问范围,控制着变量及函数的可见性与生命周期。分为全局作用域和局部作用域。

全局作用域:

在代码中任何地方都能访问到的对象拥有全局作用域,有以下几种:

在最外层定义的变量;

全局对象的属性

任何地方隐式定义的变量(未定义直接赋值的变量),在任何地方隐式定义的变量都会定义在全局作用域中,即不通过 var 声明直接赋值的变量。

局部作用域:

JavaScript的作用域是通过函数来定义的,在一个函数中定义的变量只对这个函数内部可见,称为函数(局部)作用域

作用域链

作用域链是一个对象列表,用以检索上下文代码中出现的标识符。
标识符可以理解为变量名称,参数,函数声明。

函数在定义的时候会把父级的变量对象AO/VO的集合保存在内部属性[[scope]]中,该集合称为作用域链。
自由变量指的是不在函数内部声明的变量。
当函数需要访问自由变量时,会顺着作用域链来查找数据。子对象会一级一级的向上查找父对象的变量,父对象的变量对子对象是可见的,反之不成立。
作用域链就是在所有内部环境中查找变量的链式表。

可以直接的说,JS采用了词法作用域(静态作用域),JS的函数运行在他们被定义的作用域中,而不是他们被执行的作用域。可以举一个例子说明:

var s = 3
function a () {
  console.log(s)
}

function b () {
  var s = 6
  a()
}

b() // 3,不是6

如果js采用动态作用域,打印出来的应该是6而不是3,这个例子说明了js是静态作用域。

函数作用域链的伪代码:

 
function foo() {
    function bar() {
        ...
    }
}

foo.[[scope]] = [
  globalContext.VO
];

bar.[[scope]] = [
    fooContext.AO,
    globalContext.VO
];

函数在运行激活的时候,会先复制[[scope]]属性创建作用域链,然后创建变量对象VO,然后将其加入到作用域链。

executionContextObj: {
    VO:{},
    scopeChain: [VO, [[scope]]]
}
闭包
闭包是什么

闭包按照mdn的定义是可以访问自由变量的函数。自由变量前面提到过,指的是不在函数内部声明的变量。

闭包的形式
function a() {
    var num = 1
    function b() {
        console.log(num++)
    }
    return b
}

var c1 = a()
c1() // "1"
c1() // "2"

var c2 = a()
c2() // "1"
c2() // "2"
闭包的过程

写的不是很严谨。可能省略了一些过程

运行函数a

创建函数a的VO,包括变量num和函数b

定义函数b的时候,会保存a的变量对象VO和全局变量对象到[[scope]]中

返回函数b,保存到c1

运行c1

创建c1的作用域链,该作用域链保存了a的变量对象VO

创建c1的VO

运行c1,这是发现需要访问变量num,在当前VO中不存在,于是通过作用域链进行访问,找到了保存在a的VO中的num,对它进行操作,num的值被设置成2

再次运行c1,重复第二步的操作,num的值设置成3

一些问题

通过上面的运行结果,我们可以观察到,c2所访问num变量跟c1访问的num变量不是同一个变量。我们可以修改一下代码,来确认自己的猜想

function a() {
    var x = {y : 4}
    function b() {
        return x
    }
    return b
}

var c1 = a()


var c2 = a()
c1 === c2()  // false

因此我们可以确定,闭包所访问的变量,是每次运行父函数都重新创建,互相独立的。
注意,同一个函数中创建的自由变量是可以在不同的闭包共享的

function a() {
    var x = 0
    function b() {
        console.log(x++)
    }
    function c() {
        console.log(x++)
    }
    
    return {
        b,
        c
    }
}

var r =  a()
r.b() // 0
r.c() // 1

补充一个查看作用域链和闭包的技巧
打开chrome控制台

console.dir(r.b)

f b() {
    [[Scopes]]: [
        {x:0}, 
        {type: "global", name: "", object: Window}
    ]    
}
最后

最后,我们再来总结一下执行上下文的过程,加深下印象

var scope = "global scope";
function checkscope(a){
    var scope2 = "local scope";
}

checkscope(5);
创建全局上下文执行栈

创建全局变量globalContext.VO.

创建checkscope函数

将全局变量VO保存为作用域链,设置到函数的内部属性[[scope]]

checkscope.[[scope]] = [
    globalContext.VO
];
执行checkscope函数

创建函数执行上下文,将checkscope函数执行上下文压入执行上下文栈

ECStack = [
    checkscopeContext,
    globalContext
];
函数执行上下文创建阶段

第一步是复制[[scope]],创建作用域链

checkscopeContext = {
    Scope: checkscope.[[scope]],
}

第二步是创建活动对象AO

checkscopeContext = {
    AO: {
        arguments: {
            0: 5
            length: 1
        },
        a: 5
        scope2: undefined
    },
    Scope: checkscope.[[scope]],
}

第三步是将活动对象AO放入作用域链顶端

checkscopeContext = {
    AO: {
        arguments: {
            0: 5
            length: 1
        },
        a: 5
        scope2: undefined
    },
    Scope:  [AO, checkscope.[[scope]]],
}

第四步,求出this,上下文创建阶段结束

这里的this等于window

进入函数执行阶段

随着函数执行,修改AO的值

    AO: {
        arguments: {
            0: 5
            length: 1
        },
        a: 5
        scope2: "local scope"
    },
函数执行完毕

函数上下文从执行上下文栈弹出

ECStack = [
    globalContext
];

文章写的比较长,涉及的范围也比较广,可能有不少的错误,希望大家可以指正。

本文章为前端进阶系列的一部分,
欢迎关注和star本博客或是关注我的github

参考

深入理解ES6箭头函数中的this

你不知道的JS上卷

JavaScript深入之执行上下文栈

理解JavaScript的作用域链

JavaScript深入之变量对象

深入理解JavaScript系列(12):变量对象(Variable Object)

了解JavaScript的执行上下文

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/94089.html

相关文章

  • 查漏补缺 - 收藏集 - 掘金

    摘要:酝酿许久之后,笔者准备接下来撰写前端面试题系列文章,内容涵盖浏览器框架分钟搞定常用基础知识前端掘金基础智商划重点在实际开发中,已经非常普及了。 这道题--致敬各位10年阿里的前端开发 - 掘金很巧合,我在认识了两位同是10年工作经验的阿里前端开发小伙伴,不但要向前辈学习,我有时候还会选择另一种方法逗逗他们,拿了网上一道经典面试题,可能我连去阿里面试的机会都没有,但是我感受到了一次面试1...

    YuboonaZhang 评论0 收藏0
  • Javascript currying柯里化详解

    摘要:面试题实现结果,题的核心就是问的的柯里化先说说什么是柯里化,看过许多关于柯里化的文章,始终搞不太清楚,例如柯里化是把接受多个参数的函数变换成接受一个单一参数最初函数的第一个参数的函数,并且返回接受余下的参数且返回结果的新函数的技术。 面试题:实现add(1)(2)(3) //结果 = 6,题的核心就是问的js的柯里化 先说说什么是柯里化,看过许多关于柯里化的文章,始终搞不太清楚,例如...

    mengbo 评论0 收藏0
  • 前端基础进阶(八):深入详解函数的柯里化

    摘要:函数被转化之后得到柯里化函数,能够处理的所有剩余参数。因此柯里化也被称为部分求值。那么函数的柯里化函数则可以如下因此下面的运算方式是等价的。而这里对于函数参数的自由处理,正是柯里化的核心所在。额外知识补充无限参数的柯里化。 showImg(https://segmentfault.com/img/remote/1460000008493346); 柯里化是函数的一个比较高级的应用,想要...

    kk_miles 评论0 收藏0
  • Java与groovy混编 —— 一种兼顾接口清晰实现敏捷的开发方式

    摘要:原文链接有大量平均水平左右的工人可被选择参与进来这意味着好招人有成熟的大量的程序库可供选择这意味着大多数项目都是既有程序库的拼装,标准化程度高而定制化场景少开发工具测试工具问题排查工具完善,成熟基本上没有团队愿意在时间紧任务重的项目情况 原文链接:http://pfmiles.github.io/blog/java-groovy-mixed/ 有大量平均水平左右的工人可被选择、参与...

    LittleLiByte 评论0 收藏0
  • javascript 闭包 ---- js进化之路

    摘要:闭包利用的,其实就是作用域嵌套情况下,内部作用域可以访问外部作用域这一特性。之所以要将闭包和垃圾回收策略联系在一起,是因为这涉及到闭包的一些重要特性,如变量暂时存储和内存泄漏。因为匿名函数回调的闭包实际引用的是变量,而非变量的值。 本文旨在总结在js学习过程中,对闭包的思考,理解,以及一些反思,如有不足,还请大家指教 闭包---closure 闭包是js中比较特殊的一个概念,其特殊之处...

    HtmlCssJs 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<