专栏名称: SegmentFault思否
SegmentFault (www.sf.gg)开发者社区,是中国年轻开发者喜爱的极客社区,我们为开发者提供最纯粹的技术交流和分享平台。
目录
相关文章推荐
程序员的那些事  ·  GPU:DeepSeek ... ·  昨天  
码农翻身  ·  中国的大模型怎么突然间就领先了? ·  昨天  
码农翻身  ·  漫画 | 为什么大家都愿意进入外企? ·  2 天前  
程序员小灰  ·  3个令人惊艳的DeepSeek项目,诞生了! ·  3 天前  
程序猿  ·  “我真的受够了Ubuntu!” ·  4 天前  
51好读  ›  专栏  ›  SegmentFault思否

深入理解 js this 绑定

SegmentFault思否  · 公众号  · 程序员  · 2017-09-20 11:16

正文

js 的 this 绑定问题,让多数新手懵逼,部分老手觉得恶心,这是因为this的绑定 ‘难以捉摸’,出错的时候还往往不知道为什么,相当反逻辑。

让我们考虑下面代码:

  1. var people = {

  2.    name : "海洋饼干",

  3.    getName : function(){

  4.        console.log(this.name);

  5.    }

  6. };

  7. window.onload = function(){

  8.    xxx.onclick =  people.getName;

  9. };

在平时搬砖时比较常见的this绑定问题,大家可能也写给或者遇到过,当xxx.onclick触发时,输出什么呢 ?

为了方便测试,我将代码简化:

  1. var people = {

  2.    Name: "海洋饼干",

  3.    getName : function(){

  4.        console.log(this.Name);

  5.    }

  6. };

  7. var bar = people.getName;

  8. bar();    // undefined

通过这个小例子带大家感受一下 this 恶心的地方,我最开始遇到这个问题的时候也是一脸懵逼,因为代码里的 this 在创建时指向非常明显啊,指向自己 people 对象,但是实际上指向 window 对象,这就是我马上要和大家说的 this 绑定规则

this

什么是this?在讨论this绑定前,我们得先搞清楚this代表什么。

  1. this是JavaScript的关键字之一。它是 对象 自动生成的一个内部对象,只能在 对象 内部使用。随着函数使用场合的不同,this的值会发生变化。

  2. this指向什么,完全取决于什么地方以什么方式调用,而不是创建时。(比较多人误解的地方)(它非常语义化,this在英文中的含义就是 这,这个 ,但这其实起到了一定的误导作用,因为this并不是一成不变的,并不一定一直指向当前 这个)

this 绑定规则

掌握了下面介绍的4种绑定的规则,那么你只要看到函数调用就可以判断 this 的指向了。

默认绑定

考虑下面代码:

  1. function foo(){

  2.    var a = 1 ;

  3.    console.log(this.a);    // 10

  4. }

  5. var a = 10;

  6. foo();

这种就是典型的默认绑定,我们看看foo调用的位置,”光杆司令“,像 这种直接使用而不带任何修饰的函数调用 ,就 默认且只能 应用 默认绑定。

那默认绑定到哪呢,一般是 window 上,严格模式下 是 undefined

隐性绑定

代码说话:

  1. function foo(){

  2.    console.log( this.a);

  3. }

  4. var obj = {

  5.    a : 10,

  6.    foo : foo

  7. }

  8. foo();                // ?

  9. obj.foo();            // ?

答案 : undefined 10

foo() 的这个写法熟悉吗,就是我们刚刚写的默认绑定,等价于打印 window.a ,故输出 undefined ,下面 obj.foo() 这种大家应该经常写,这其实就是我们马上要讨论的 隐性绑定 。

函数foo执行的时候有了上下文对象,即 obj 。这种情况下,函数里的this默认绑定为上下文对象,等价于打印 obj.a ,故输出 10

如果是链性的关系,比如 xx.yy.obj.foo(); , 上下文取函数的直接上级,即紧挨着的那个,或者说对象链的最后一个。

显性绑定

隐性绑定的限制

在我们刚刚的 隐性绑定中有一个致命的限制,就是上下文必须包含我们的函数 ,例: varobj={foo:foo} ,如果上下文不包含我们的函数用隐性绑定明显是要出错的,不可能每个对象都要加这个函数 ,那样的话扩展,维护性太差了,我们接下来聊的就是直接 给函数强制性绑定this。

call apply bind

这里我们就要用到 js 给我们提供的函数 call 和 apply,它们的作用都是改变函数的this指向,第一个参数都是 设置this对象。

两个函数的区别:

  1. call从第二个参数开始所有的参数都是 原函数的参数。

  2. apply只接受两个参数,且第二个参数必须是数组,这个数组代表原函数的参数列表。

例如:

  1. function foo(a,b){

  2.    console.log(a+b);

  3. }

  4. foo.call(null,'海洋','饼干');        // 海洋饼干  这里this指向不重要就写null了

  5. foo.apply(null, ['海洋','饼干'] );     // 海洋饼干

除了 call,apply函数以外,还有一个改变this的函数 bind ,它和call,apply都不同。

bind只有一个函数,且不会立刻执行,只是将一个值绑定到函数的this上,并将绑定好的函数返回。例:

  1. function foo(){

  2.    console.log(this.a);

  3. }

  4. var obj = { a : 10 };

  5. foo = foo.bind(obj);

  6. foo();                    // 10

(bind函数非常特别,下次和大家一起讨论它的源码)

显性绑定

开始正题,上代码,就用上面隐性绑定的例子 :

  1. function foo(){

  2.    console.log(this.a);

  3. }

  4. var obj = {

  5.    a : 10            //去掉里面的foo

  6. }

  7. foo.call(obj);        // 10

我们将隐性绑定例子中的 上下文对象 里的函数去掉了,显然现在不能用 上下文.函数 这种形式来调用函数,大家看代码里的显性绑定代码 foo.call(obj) ,看起来很怪,和我们之前所了解的函数调用不一样。

其实call 是 foo 上的一个函数,在改变this指向的同时执行这个函数。

(想要深入理解 [ call apply bindthis硬绑定,软绑定,箭头函数绑定 ] 等更多黑科技 的小伙伴欢迎关注我或本文的评论,最近我会单独做一期放到一起写一篇文章)(不想看的小伙伴不用担心,不影响对本文的理解)

new 绑定

什么是 new

学过面向对象的小伙伴对new肯定不陌生,js的new和传统的面向对象语言的new的作用都是创建一个新的对象,但是他们的机制完全不同。

创建一个新对象少不了一个概念,那就是 构造函数 ,传统的面向对象 构造函数 是类里的一种特殊函数,要创建对象时使用 new类名() 的形式去调用类中的构造函数,而js中就不一样了。

js中的只要用new修饰的 函数就是'构造函数',准确来说是 函数的 构造调用 ,因为在js中并不存在所谓的'构造函数'。

那么用new 做到函数的 构造调用 后,js帮我们做了什么工作呢:

  1. 创建一个新对象。

  2. 把这个新对象的 __proto__ 属性指向 原函数的 prototype 属性。(即继承原函数的原型)

  3. 将这个新对象绑定到 此函数的this上。

  4. 返回新对象,如果这个函数没有返回其他对象。

第三条就是我们下面要聊的new绑定

new 绑定

不哔哔,看代码:

  1. function foo(){

  2.    this.a = 10;

  3.    console.log(this);

  4. }

  5. foo();                    // window对象

  6. console.log(window.a);    // 10   默认绑定

  7. var obj = new foo();      // foo{ a : 10 }  创建的新对象的默认名为函数名

  8.                          // 然后等价于 foo { a : 10 };  var obj = foo;

  9. console.log(obj.a);       // 10    new绑定

使用new调用函数后,函数会 以自己的名字 命名 和 创建 一个新的对象,并返回。

特别注意: 如果原函数返回一个对象类型,那么将无法返回新对象,你将丢失绑定this的新对象,例:

  1. function foo(){

  2.    this.a = 10;

  3.    return new String("捣蛋鬼");

  4. }

  5. var obj = new foo();

  6. console.log(obj.a);       // undefined

  7. console.log(obj);         // "捣蛋鬼"

this 绑定优先级

过程是些无聊的代码测试,我直接写出优先级了(想看测试过程可以私信,我帮你写一份详细的测试代码)

new 绑定 > 显示绑定 > 隐式绑定 > 默认绑定

总结

1.如果函数被 new 修饰

this绑定的是新创建的对象,例:var bar = new foo(); 函数 foo 中的 this 就是一个叫foo的新创建的对象 , 然后将这个对象赋给bar , 这样的绑定方式叫 new 绑定 。

2.如果函数是使用 call,apply,bind 来调用的

this绑定的是 call,apply,bind 的第一个参数.例: foo.call(obj); , foo 中的 this 就是 obj , 这样的绑定方式叫 显性绑定 .

3.如果函数是在某个 上下文对象 下被调用

this绑定的是那个上下文对象,例 : var obj = { foo : foo }; obj.foo(); foo 中的 this 就是 obj . 这样的绑定方式叫 隐性绑定 .

4.如果都不是,即使用默认绑定

例:function foo(){...} foo() ,foo 中的 this 就是 window.(严格模式下默认绑定到undefined).

这样的绑定方式叫 默认绑定.

面试题解析

1.

  1. var x = 10;

  2. var obj = {

  3.    x: 20,

  4.    f: function(){

  5.        console.log( this.x);        // ?

  6.        var foo = function(){

  7.            console.log(this.x);    

  8.            }

  9.        foo();                      // ?

  10.    }

  11. };

  12. obj.f();

答案 : 20 10

解析 :考点 1. this默认绑定 2. this隐性绑定

  1. var x = 10 ;

  2. var obj = {

  3.    x: 20,

  4.    f: function(){

  5.        console.log(this.x);    // 20

  6.                                // 典型的隐性绑定,这里 f 的this指向上下文 obj ,即输出 20

  7.        function foo(){

  8.            console.log(this.x);

  9.            }

  10.        foo();       // 10

  11.                     //有些人在这个地方就想当然的觉得 foo 在函数 f 里,也在 f 里执行,

  12.                     //那 this 肯定是指向obj 啊 , 仔细看看我们说的this绑定规则 , 对应一下很容易

  13.                     //发现这种'光杆司令',是我们一开始就示范的默认绑定,这里this绑定的是window

  14.    }

  15. };

  16. obj.f();            

2.

  1. function foo(arg){

  2.    this.a = arg;

  3.    return this

  4. };

  5. var a = foo(1);

  6. var b = foo(10);

  7. console.log(a.a);    // ?

  8. console.log(b.a);    // ?

答案 : undefined 10

解析 :考点 1. 全局污染 2. this默认绑定

这道题很有意思,问题基本上都集中在第一undefined上,这其实是题目的小陷阱,但是追栈的过程绝对精彩。

让我们一步步分析这里发生了什么:

  1. foo(1)执行,应该不难看出是默认绑定吧 , this指向了window,函数里等价于 window . a = 1,return window;

  2. var a = foo(1) 等价于 window . a = window , 很多人都忽略了 *var a 就是window.a * ,将刚刚赋值的 1 替换掉了。

  3. 所以这里的 a 的值是 window , a . a 也是window , 即window . a = window ; window . a . a = window;

  4. foo(10) 和第一次一样,都是默认绑定,这个时候,将window.a 赋值成 10 ,注意这里是关键,原来window.a = window ,现在被赋值成了10,变成了值类型,所以现在 a.a = undefined。(验证这一点只需要将var b = foo(10);删掉,这里的 a.a 还是window)

  5. var b = foo(10); 等价于 window.b = window;

本题中所有变量的值,a = window.a = 10 , a.a = undefined , b = window , b.a = window.a = 10;

3.

  1. var x = 10;

  2. var obj = {

  3.    x: 20,

  4.    f: function(){ console.log(this.x); }

  5. };

  6. var bar = obj.f;

  7. var obj2 = {

  8.    x: 30,

  9.    f: obj.f

  10. }

  11. obj.f();

  12. bar();

  13. obj2.f();

答案:20 10 30

解析:传说中的送分题,考点,辨别this绑定

  1. var x = 10;

  2. var obj = {

  3.    x: 20,

  4.    f: function(){ console.log(this.x); }

  5. };

  6. var bar = obj.f;

  7. var obj2 = {

  8.    x: 30,

  9.    f: obj.f

  10. }

  11. obj.f();    // 20

  12.            //有上下文,this为obj,隐性绑定

  13. bar();      // 10

  14.            //'光杆司令' 默认绑定  ( obj.f 只是普通的赋值操作 )

  15. obj2.f();   //30

  16.            //不管 f 函数怎么折腾,this只和 执行位置和方式有关,即我们所说的绑定规则

4. 压轴题了

  1. function foo() {

  2.    getName = function () { console.log (1); };

  3.    return this;

  4. }

  5. foo.getName = function () { console.log(2);};

  6. foo.prototype.getName = function () { console.log(3);};

  7. var getName = function () { console.log(4);};

  8. function getName () { console.log(5);}

  9. foo.getName ();                // ?

  10. getName ();                    // ?

  11. foo().getName ();              // ?

  12. getName ();                    // ?

  13. new foo.getName ();            // ?

  14. new foo().getName ();          // ?

  15. new new foo().getName ();      // ?

答案:2 4 1 1 2 3 3

解析:考点 1. new绑定 2.隐性绑定 3. 默认绑定 4.变量污染(用词不一定准确)

  1. function foo() {

  2.    getName = function () { console.log (1); };

  3.             //这里的getName 将创建到全局window上

  4.    return this;

  5. }

  6. foo.getName = function () { console.log(2);};  

  7.        //这个getName和上面的不同,是直接添加到foo上的

  8. foo.prototype.getName = function () { console.log(3);};

  9.        // 这个getName直接添加到foo的原型上,在用new创建新对象时将直接添加到新对象上

  10. var getName = function () { console.log(4);};

  11.        // 和foo函数里的getName一样, 将创建到全局window上

  12. function getName () { console.log(5);}    

  13.        // 同上,但是这个函数不会被使用,因为函数声明的提升优先级最高,所以上面的函数表达式将永远替换

  14.        // 这个同名函数,除非在函数表达式赋值前去调用getName(),但是在本题中,函数调用都在函数表达式

  15.        // 之后,所以这个函数可以忽略了

  16.        // 通过上面对 getName的分析基本上答案已经出来了

  17. foo.getName ();                // 2

  18.                               // 下面为了方便,我就使用输出值来简称每个getName函数

  19.                               // 这里有小伙伴疑惑是在 2 和 3 之间,觉得应该是3 , 但其实直接设置

  20.                               // foo.prototype上的属性,对当前这个对象的属性是没有影响的,如果要使

  21.                               // 用的话,可以foo.prototype.getName() 这样调用 ,这里需要知道的是

  22.                               // 3 并不会覆盖 2,两者不冲突 ( 当你使用new 创建对象时,这里的

  23.                               // Prototype 将自动绑定到新对象上,即用new 构造调用的第二个作用)

  24. getName ();                    // 4

  25.                               // 这里涉及到函数提升的问题,不知道的小伙伴只需要知道 5 会被 4 覆盖,

  26.                               // 虽然 5 在 4 的下面,其实 js 并不是完全的自上而下,想要深入了解的

  27.                               // 小伙伴可以看文章最后的链接

  28. foo().getName ();              // 1

  29.                               // 这里的foo函数执行完成了两件事, 1. 将window.getName设置为1,

  30.                               // 2. 返回window , 故等价于 window.getName(); 输出 1

  31. getName ();                    







请到「今天看啥」查看全文