龙空技术网

深入理解Js的This绑定(无需死记硬背,尾部有总结和面试题解析)

西安北大青鸟华美教育 268

前言:

如今同学们对“js取消绑定”都比较着重,看官们都需要知道一些“js取消绑定”的相关资讯。那么小编在网摘上收集了一些有关“js取消绑定””的相关资讯,希望朋友们能喜欢,姐妹们一起来学习一下吧!

深入理解Js的This绑定

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

让我们考虑下面代码:

varpeople={

name:"海洋饼干",

getName:function(){

console.log(this.name);

}

};

window.onload=function(){

xxx.onclick=people.getName;

};

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

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

varpeople={

Name:"海洋饼干",

getName:function(){

console.log(this.Name);

}

};

varbar=people.getName;

bar();//undefined

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

1.this

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

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

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

2.this绑定规则

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

2.1默认绑定

考虑下面代码:

functionfoo(){

vara=1;

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

}

vara=10;

foo();

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

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

2.2隐性绑定

代码说话:

functionfoo(){

console.log(this.a);

}

varobj={

a:10,

foo:foo

}

foo();//?

obj.foo();//?

答案:undefined10

foo()的这个写法熟悉吗,就是我们刚刚写的默认绑定,等价于打印window.a,故输出undefined,

下面obj.foo()这种大家应该经常写,这其实就是我们马上要讨论的隐性绑定。

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

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

2.3显性绑定

2.3.1隐性绑定的限制

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

2.3.2callapplybind

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

两个函数的区别:

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

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

例如:

functionfoo(a,b){

console.log(a+b);

}

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

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

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

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

functionfoo(){

console.log(this.a);

}

varobj={a:10};

foo=foo.bind(obj);

foo();//10

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

2.3.2显性绑定

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

functionfoo(){

console.log(this.a);

}

varobj={

a:10//去掉里面的foo

}

foo.call(obj);//10

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

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

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

2.4new绑定

2.4.1什么是new

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

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

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

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

创建一个新对象。

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

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

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

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

2.4.2new绑定

不哔哔,看代码:

functionfoo(){

this.a=10;

console.log(this);

}

foo();//window对象

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

varobj=newfoo();//foo{a:10}创建的新对象的默认名为函数名

//然后等价于foo{a:10};varobj=foo;

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

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

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

functionfoo(){

this.a=10;

returnnewString("捣蛋鬼");

}

varobj=newfoo();

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

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

2.5this绑定优先级

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

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

3.总结

1.如果函数被new修饰

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

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

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

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

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

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

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

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

4.面试题解析

1.

varx=10;

varobj={

x:20,

f:function(){

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

varfoo=function(){

console.log(this.x);

}

foo();//?

}

};

obj.f();

-----------------------答案---------------------

答案:2010

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

varx=10;

varobj={

x:20,

f:function(){

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

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

functionfoo(){

console.log(this.x);

}

foo();//10

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

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

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

}

};

obj.f();

2.

functionfoo(arg){

this.a=arg;

returnthis

};

vara=foo(1);

varb=foo(10);

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

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

-----------------------答案---------------------

答案:undefined10

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

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

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

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

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

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

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

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

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

3.

varx=10;

varobj={

x:20,

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

};

varbar=obj.f;

varobj2={

x:30,

f:obj.f

}

obj.f();

bar();

obj2.f();

-----------------------答案---------------------

答案:201030

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

varx=10;

varobj={

x:20,

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

};

varbar=obj.f;

varobj2={

x:30,

f:obj.f

}

obj.f();//20

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

bar();//10

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

obj2.f();//30

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

4.压轴题了

functionfoo(){

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

returnthis;

}

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

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

vargetName=function(){console.log(4);};

functiongetName(){console.log(5);}

foo.getName();//?

getName();//?

foo().getName();//?

getName();//?

newfoo.getName();//?

newfoo().getName();//?

newnewfoo().getName();//?

-----------------------答案---------------------

答案:2411233

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

functionfoo(){

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

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

returnthis;

}

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

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

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

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

vargetName=function(){console.log(4);};

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

functiongetName(){console.log(5);}

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

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

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

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

foo.getName();//2

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

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

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

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

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

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

getName();//4

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

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

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

foo().getName();//1

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

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

getName();//1

//刚刚上面的函数刚把window.getName设置为1,故同上输出1

newfoo.getName();//2

//new对一个函数进行构造调用,即foo.getName,构造调用也是调用啊

//该执行还是执行,然后返回一个新对象,输出2(虽然这里没有接收新

//创建的对象但是我们可以猜到,是一个函数名为foo.getName的对象

//且__proto__属性里有一个getName函数,是上面设置的3函数)

newfoo().getName();//3

//这里特别的地方就来了,new是对一个函数进行构造调用,它直接找到了离它

//最近的函数,foo(),并返回了应该新对象,等价于varobj=newfoo();

//obj.getName();这样就很清晰了,输出的是之前绑定到prototype上的

//那个getName3,因为使用new后会将函数的prototype继承给新对象

newnewfoo().getName();//3

//哈哈,这个看上去很吓人,让我们来分解一下:

//varobj=newfoo();

//varobj1=newobj.getName();

//好了,仔细看看,这不就是上两题的合体吗,obj有getName3,即输出3

//obj是一个函数名为foo的对象,obj1是一个函数名为obj.getName的对象

5.箭头函数的this绑定(2017.9.18更新)

箭头函数,一种特殊的函数,不使用function关键字,而是使用=>,学名胖箭头(2333),它和普通函数的区别:

箭头函数不使用我们上面介绍的四种绑定,而是完全根据外部作用域来决定this。(它的父级是使用我们的规则的哦)

箭头函数的this绑定无法被修改(这个特性非常爽(滑稽))

先看个代码巩固一下:

functionfoo(){

return()=>{

console.log(this.a);

}

}

foo.a=10;

//1.箭头函数关联父级作用域this

varbar=foo();//foo默认绑定

bar();//undefined哈哈,是不是有小伙伴想当然了

varbaz=foo.call(foo);//foo显性绑定

baz();//10

//2.箭头函数this不可修改

//这里我们使用上面的已经绑定了foo的baz

varobj={

a:999

}

baz.call(obj);//10

来来来,实战一下,还记得我们之前第一个例子吗,将它改成箭头函数的形式(可以彻底解决恶心的this绑定问题):

varpeople={

Name:"海洋饼干",

getName:function(){

console.log(this.Name);

}

};

varbar=people.getName;

bar();//undefined

====================修改后====================

varpeople={

Name:"海洋饼干",

getName:function(){

return()=>{

console.log(this.Name);

}

}

};

varbar=people.getName();//获得一个永远指向people的函数,不用想this了,岂不是美滋滋?

bar();//海洋饼干

可能会有人不解为什么在箭头函数外面再套一层,直接写不就行了吗,搞这么麻烦干嘛,其实这也是箭头函数很多人用不好的地方,来来来,饼干带你飞(可把我nb坏了,插会腰):

varobj={

that:this,

bar:function(){

return()=>{

console.log(this);

}

},

baz:()=>{

console.log(this);

}

}

console.log(obj.that);//window

obj.bar()();//obj

obj.baz();//window

我们先要搞清楚一点,obj的当前作用域是window,如obj.that===window。

如果不用function(function有自己的函数作用域)将其包裹起来,那么默认绑定的父级作用域就是window。

用function包裹的目的就是将箭头函数绑定到当前的对象上。函数的作用域是当前这个对象,然后箭头函数会自动绑定函数所在作用域的this,即obj。

美滋滋,溜了溜了

标签: #js取消绑定