<<你不知道的JavaScript>>之this与绑定规则

前奏

读完书中的“关于this”这一章,发现其实作者有点啰嗦的感觉了。他并没有直接讲this到底是什么,只是列举了很容易用错this的例子,遗憾的是,我发现当中貌似有个错误的地方,看下面这段代码

function foo(){

  var a = 2;

  this.bar();

}

function bar(){

  console.log(this.a);

}

foo();

书中原话是:“首先,这段代码试图通过this.bar()来引用这个函数。这绝对是不可能成功的,我们之后会解释原因。调用bar()最自然的方法是省略前面的this,直接使用词法引用标识符”。可实际上,this.bar()是可以调用成功的,因为this代表的是当前函数的执行环境,而上述代码中的this指的就是全局环境了,那么全局环境调用自己作用域中的bar是完全可以的,而且自己经过代码验证也确实是可行的。不知道作者为什么会这样说呢?

不过在讲解this之前,还是先要说一下,this实际上是在函数调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。

调用位置

这里可能有些读者会产生疑问,函数的调用位置如何判断呢,或者我们之前根本也没想过这个问题,只是想当然的函数标识符 +()“就可以了。其实这里是有个叫做“函数调用栈”的说法,当你用F12打开调试工具后,点击source这一tab页时,在右侧列表中会看到“Call Stack ”的选项,这个就是所谓的调用栈,当你在函数调用中设置断点debugger时,就可以看到函数调用的顺序了。栈这个数据结构大家应该很熟悉了,和队列相比最明显的一个特点就是“先进后出”,如果还是不懂就自行查找资料了哈。

其实我们所说的调用位置,就是这个函数由谁调用,也就是调用环境(对象)是谁?这才是理解this绑定的关键所在,不过首先要了解一下this的绑定规则。

绑定规则

1)默认绑定

这种绑定就是指函数在没有任何修饰的函数引用情况下被调用的,看代码:

function foo(){

console.log(this.a);

}

var a = 2;

foo();  //输出结果为2

我们套合前面讲的调用位置,看看foo是在哪里调用的。很显然foo在全局环境中被调用,所以this指向的就是全局作用域了,那么 this.a 就是 2 喽。

2)隐式绑定

这种绑定是指函数的调用前面一般是有上下文对象的,也就是不是独立的调用,而是有修饰的调用。看下面代码:

function foo(){

console.log(this.a);

}

var bar = {

    a:2,

    foo:foo

}

var a = ‘hello’;

bar.foo();  //输出结果为 2

我们看到,foo在调用的时候前面有修饰符bar,foo作为引用属性被添加到obj中,在foo调用时就相当于是在bar中进行的,所以this指向了bar。

请读者注意了,到这里还没有结束,我们继续看下面一段代码

function foo(){

   console.log(this.a);

}

var bar = {

    a:2,

    foo:foo

}

var a = ‘hello’;

var test = bar.foo;

test(); //输出结果为 hello

这段代码跟之前的有什么区别呢?我们分析发现这里用 test 对 foo进行了引用,虽然表面上看foo这个函数是属于bar的,但实际情况不是这样的,bar只是对foo进行了属性引用,但是foo到底属于谁,还是看人家声明时的位置的。所谓的引用不过是起了别名,其实还是同一个人,就像你的大名和小名都是指的你自己一样。所以无论是 test 也好,还是 bar.foo 也罢,它们指向的都是foo自身了。不过此时你很可能会跟我说,既然这样那么输出的应该都是 2 啊。表面上好像你说的对,但是你似乎忘记了我们之前讲的最重要的“调用位置”这个判断规则了,你回头看下 test() 是不是等价于 foo() 了,this指向的就是全局作用域了,所以一定要看清楚调用环境了。

这里可以叫做this绑定丢失。就是我们本来是希望绑定在 bar 上的,但是this却指向了 全局作用域。

其实另一个丢失的例子,就是常见的回调函数,看下面代码

function foo(){

  console.log(this.a);

}

function dofoo(fn){

    fn();

}

var bar = {

    a:2,

    foo:foo

}

var a = ‘hello’;

dofoo(bar.foo);  //输出结果为hello

我们不用思考太多,就看bar.foo作为参数被传递,其实传递给dofoo的就是foo本身(前面提到的引用),所以对于foo的调用还是跟 foo()这句话一样的,this指向的当然还是全局作用域了。

那么,我们如何将this绑定在我们想要的对象或是函数上呢?这里先不直接回答,继续看另一种绑定方式。

3)显示绑定

可能有些同学已经猜到了,就是用call和apply,这两个又是什么鬼呢?其实你自己创建的函数都有着两个属性,这个与原型方法有关,后续再讨论。这两个方法其实功能差不多,第一个参数传递的都是一个对象,然后this会直接绑定在这个对象上,这种方式称之为显示绑定。

看下面这段代码:

function foo(){

  console.log(this.a);

}

var bar = {

    a:2,

    foo:foo

}

var a = ‘hello’;

foo.call(bar); //输出结果为2

我们定义了foo,foo便具有了call这个方法,然后把bar作为参数传递,那么this便指向了bar,所以调用的结果就是 2 了。

4)new绑定

传统的面向对象语言编程中,可以用 new 来初始化对象实例,这个时候通常会调用构造函数。如

something = new MyClass(…);

当然JavaScript中,也有new操作符,也会调用构造函数,你可能觉得这与其它编程语言类似,但实际上完全是两码事。

JavaScript中的构造函数,不属于任何一个类,也不会实例化一个类,甚至不能说是一种特殊的函数,只是用new 调用的普通函数而已。实际上:JavaScript中并不存在真正意义的“构造函数”,只有对于函数的“构造调用”。

使用new来调用构造函数,或者说是发生构造调用时,会自动执行下面的操作:

1.创建一个全新的对象;

2.这个对象会被执行原型链接;

3.这个新的对象会被绑定到函数调用的this上;

4.如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。

function foo(a){

this.a = a;

}

var bar = new foo();

console.log(bar.a);//输出结果为2

分析这段代码,利用new 调用foo时,会自动创建一个新的对象绑定到this上,因为foo中没有其他返回对象,所以就返回了这个新的对象bar。可以看到,new操作也可以改变函数调用时的this绑定情况。

经过对上面四种绑定规则的学习,我想同学们应该对this以及其绑定有了一个全新的认识,希望能够帮助大家。总结文章不易,希望读者朋友们多多支持哈。

    原文作者:弦五
    原文地址: https://www.jianshu.com/p/26de0cfa797e
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞