阅读 127

《你所不知道的JavaScript》读书笔记(二):this指向问题

1. 前言

这篇文章是《你所不知道的JavaScript》读书笔记系列文章的第三篇文章。前两篇文章介绍了作用域和闭包的相关内容。在这篇文章中,我们来聊一聊JavaScript中一个比较坑的问题:this的指向问题。

2. 为什么使用this

在前面提到JavaScript的this机制是一个比较坑的机制,为什么呢?因为this的指向是在函数调用的时候才能确定的,开发者在进行函数定义的时候根本不知道this究竟会指向什么地方。这样会造成很多不可预测的行为,给开发人员带来了很大的不便。问题来了,既然this指向这么坑,那为什么还要有呢?为什么不把它直接砍掉算了,还要保留呢?在这里,我借用一下书中的表述来回答一下这个问题:

this提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将API设计得更加简洁并且易于复用。随着你的使用模式越来越复杂,显示传递上下文对象会让代码变得越来越混乱,使用this则不会这样。

3. 误解

在介绍this的指向问题之前,我们先来消除一下对this的几个误解:

3.1 this指向自身

我们通过一个例子来消除一下这个误解,下面贴代码:

function foo(num){
    console.log("foo:" + num)
    this.count++
}
foo.count = 0

var i
for(i = 0; i < 10; i++){
    foo( i );
}
console.log(foo.count)
复制代码

先来分析一下:这段代码中函数foo()被一个for循环调用了10次;按道理来讲this.count应该进行了十次自加,最后的foo.count的输出结果应该是10才对。然后,我们来跑一下这段代码:

image.png

结果显示,函数foo()确实被调用了10次,但是最后我们打印foo.count的时候,它的结果依然是0,这是什么原因呢?原因很简单foo()中的this.count++这条语句根本不是访问foo.count这个属性的,也就是说this不是指向函数自身。

3.2 this指向它的作用域

this不是指向函数自身,那么和函数相关的还有一个,就是函数作用域。那么,this是不是指向函数作用域呢?我们也通过一个例子来解释:

function foo() {
    var a = 2;
    this.bar();
}
function bar() {
    console.log( this.a );
}
foo();
复制代码

这段代码中,foo()试图通过this.bar()调用bar()函数。而且还试图使用this联通foo()bar()的作用域。假设,this的指向是函数作用域,那么这样通过this联通两个函数作用域的方式是可行的。在这段代码中,this假如指向的是函数的作用域,那么在作用域中查找a这个标识符显然是可以找到的。然而,这段代码的运行结果是:RefrenceError: a is not defined。显然,这里的this指向也不是函数作用域。

4. this绑定规则

this既不是指向函数本身,也不指向函数的作用域,那么它指向哪里呢?关于这个问题,我们继续参考书中的回答:

this是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。

4.1 调用位置

在理解this的绑定规则之前,应该先了解函数的调用位置。寻找调用位置就是寻找“函数被调用的位置”,最重要的是要分析调用栈。

function baz() {
    // 当前调用栈是: baz
    // 因此,当前调用位置是全局作用域
    console.log( "baz" );
    bar(); // <-- bar 的调用位置
}
function bar() {
    // 当前调用栈是 baz -> bar
    // 因此,当前调用位置在 baz 中
    console.log( "bar" );
    foo(); // <-- foo 的调用位置
}
function foo() {
    // 当前调用栈是 baz -> bar -> foo
    // 因此,当前调用位置在 bar 中
    console.log( "foo" );
}
baz(); // <-- baz 的调用位置
复制代码

上述代码认真分析了函数的调用位置。简单来说,函数的调用位置就是函数真正执行的地方。从字面上来看,一般会写成 functionName(parameter) 这样的形式。相信学过任何一门编程语言的朋友们都会知道,当函数调用的时候,会对当时的现场进行保留,压栈,然后跳转到函数内部执行,执行完毕之后,返回执行的结果,并且对调用前的现场进行还原。在这里,我们需要注意函数声明、函数表达式和函数调用三者之间的区别
在理解了调用位置之后,我们开始我们的主题:this的绑定规则。

4.2 绑定规则

  1. 默认绑定:在this没有使用其他绑定规则的时候,使用默认绑定,指向全局对象。
  2. 隐式绑定:当函数引用有上下文对象是,隐式绑定规则会把函数调用中的this绑定到这个上下文对象
  3. 显示绑定:利用call(),apply()以及bind()方法将this强制绑定到需要的对象上
  4. new绑定:
    使用new来调用函数时,会执行以下操作:
    1. 创造一个全新的对象
    2. 这个新对象会被执行[[Proptype]]连接
    3. 这个新对象会绑定到函数调用的this
    4. 如果函数没有返回其他对象,那么new表达式中的函数调用自动返回这个新对象

前面三个绑定规则都很好理解,我们重点关注第四个绑定规则。我们来看个例子:

function foo(a){
    this.a = a
}

var bar = new foo(2)
console.log(bar.a)
复制代码

这段代码的输出结果是2.

我们来分析一下:首先,var bar = new foo(2)这条语句通过new方法调用了函数foo().应该使用this的第四条绑定规则新对象会绑定到函数调用的this。在这段代码中,利用new产生的新对象是bar,执行的结果就应该是this.a = a等价于bar.a = a。因此,在打印bar.a的时候,得到的结果是2.

4.3 规则使用优先级

new绑定 -> 显式绑定 -> 隐式绑定 -> 默认绑定。

4.4 箭头函数

箭头函数不使用this的四种标准规则,而是根据外层(函数或全局)作用域来决定this.

5. 不一样的观点

我在知乎上看到的一篇文章中对this机制的其他看法,原文链接如下:是什么原因导致了 JS 中的 this 指向问题? 感兴趣的小伙伴们,请戳链接。

答案公布

for(var i=1; i<=5; i++){
    setTimeout( function (){
        console.log(i)
    }, i*1000)
}
复制代码

这段代码的执行结果是每隔一秒输出一个6。因为变量 i 的作用域是由var关键字声明的函数作用域,而这里没有函数,根据它声明的位置可以判断它是一个全局变量。然后,我们依次设置了五个定时器,定的时间分别是1s,2s,3s,4s,5s。因此,每隔一秒都会输出i。而此时的变量i经过循环之后,变成了6.因此,最终的结果是每隔1s输出一个6.

文章分类
前端
文章标签