《你不知道的JS》中this总结
前言
在学习this的时候看过一些关于this这部分的文章。大部分都是零零散散的看完了就放在收藏夹里面。最近再去收藏夹里面找来看的时候发现之前那篇写的很好的this文章却不见了。这篇文章是我重新看了《你不知道的js》后对this这部分内容的总结,更多内容请参照《你不知道的javascript》。
误解
在没有看《你不知道的js》之前我也是根据他的英文翻译以为this就是指函数本身或者是函数的作用域 。但是后来学习了一系列知识之后打破我之前的一些认知。
误解一:this指向自身
function foo(num){
this.count++
console.log(this)//window对象,
console.log(this.count) //输入了4次NaN, count 为undefined
}
foo.count=0;
for(let i=0;i<10;i++){
if(i>5){
foo(i);
}
}
console.log(foo.count)//依然为0
【反逻辑】: 如果this指向自身,那么this==foo函数对象,那么this.count==foo.count==0;但是实际结果却是上面的情况。说明this不指向自身。
【打印出window对象】:发现foo上确实有count属性。但是在window全局变量上面也有一个count.
误解二:指向它的作用域
第二种误解是,this指向函数的作用域。在有些情况下其实是正确的。但是实际上,this在任何情况下都不指向函数的词法作用域
function foo(){
var a = 2;
console.log(this.a)//undefine
}
foo();
【反逻辑】如果this指向函数的作用域,那么输出的应该是2。但是输出的却是undefined。
实际上this指向
实际上this取决于调用的位置。因为他的指向是调用的时候被绑定的。 书中提到有四种this绑定规则。
默认绑定
独立函数的调用,调用函数的时候前面没有加修饰符,这种情况默认this指向window(注意如果是在严格模式下this指向undefined)。
function foo(){
var a = 2;
var b =1 ;
bar();
console.log(this.a)//window
}
var b = 23;
function bar(){
console.log(b,"bbbbbbbb")//23
console.log(this) //window
}
foo();
【逻辑】foo() 函数和bar()函数在调用时前面没有任何修饰符。尽管bar函数的调用栈在foo()函数中。但是输出的值却是23. 说明bar函数的this指向也是window.
隐式绑定
这种规则需要考虑的是调用位置是否有上下文对象。或者说是否被某个对象拥有或者包含。
function foo(){
console.log(this.a)
}
var obj = {
a:2,
foo:foo
}
obj.foo();//2
【逻辑】这种方式调用函数,隐式规则会把函数调用中的this绑定到这个函数上下文对象中。
【注意】只有最后一层在调用位置中起作用。
function foo(){
console.log(this.a)//42
}
var obj2 = {
a:42,
foo:foo
}
var obj1 = {
a:2,
obj2: obj2
}
obj1.obj2.foo(); //这只有最后一层的obj2起作用
【隐式调用的其他注意问题】
- 情况一: 调用函数时给调用函数一个别名会丢失隐式绑定
function foo(){
console.log(this.a)//全局对象
}
var obj = {
a:"对象a的属性",
foo:foo
}
var bar = obj.foo;//函数别名
var a = "全局对象";
bar();
这种情况相当于用的是默认绑定的规则。输出的是“全局对象”。
- 情况二: 传入回调函数
function foo(){
console.log(this.a)//全局对象属性
}
function doFoo(fn){
fn();
}
var obj={
a:2,
foo:foo
}
var a = "全局对象属性"
doFoo(obj.foo)
这种情况类似于情况一。
显示绑定(call, apply)方法
在某个对象上面强制调用某个函数。
function foo(){
console.log(this.a)//
}
var obj = {
a:2
}
var a = "全局"
foo.call(obj)
【注意】如果传入了一个原始值(字符串类型,boolean类型,数字类型)来当做this绑定对象,这个传入值会转化为他们的对象形式
function foo(){
console.log(this)
}
var obj = {
a:2
}
var a = "全局"
foo.call(true)
foo.call("233")
foo.call(1)
但是显示绑定并不能解决之前的丢失绑定的问题!!
1.硬绑定
硬绑定是一种显示的强制绑定。 原理:Function.prototype.bind()会创建一个新的包装函数。这个函数会忽略它当前的this绑定。(无论绑定的对象是什么),并把我们提供的对象绑定到this上
function foo(something) {
console.log(this.a, something);
return this.a + something;
}
var obj = {
a: 2
}
var bar = function () {
console.log(arguments)
return foo.apply(obj, arguments)
}
// var b = bar(3);
// b()
// console.log(bar(3))
bar(3)//2 3
new绑定.
在javascript中 只是被new操作符调用的普通函数。
使用new来构造函数的时候,会自动执行下面的操作。
- 创建一个全新的对象
- 这个新对象会被执行[[Prototype]]连接
- 这个新对象会绑定到函数调用的this
- 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象
- 普通绑定 此时的this指向window
function foo(a){
this.a = a;
console.log(this)//指向window
}
foo(2);
- new绑定 此时的this指向foo函数对象
function foo(a){
this.a = a;
console.log(this)//函数foo
}
var bar = new foo(2);
优先级比较
- 显示绑定和隐式绑定比较
function foo(){
console.log(this.a)
}
var obj1 = {
a:2,
foo:foo
}
var obj2 = {
a:3,
foo:foo
}
obj1.foo.call(obj2)//3
obj2.foo.call(obj1)//2
obj1.foo.call(obj2) 调用方式中前半部分是隐式调用,后半部分是显示调用。但是输出的却是显示调用中的值。 说明显示调用的优先级大于隐式调用。
- new绑定和隐式绑定比较
function foo(something){
console.log(this)
this.a = something
}
var obj1 = {
foo:foo
}
obj1.foo(4)
function foo(something){
console.log(this)//foo
this.a = something
}
var obj1 = {
foo:foo
}
var bar = new obj1.foo(4)
后面这一段代码中既有new绑定,又有隐式绑定,但是打印出来的this却是foo函数。说明new绑定优先级大于隐式绑定
- new绑定和显示绑定
function foo(something){
this.a = something;
}
var obj1 = {};
//硬绑定部分
var bar = foo.bind(obj1);
bar(2);
console.log(obj1.a)//2
//new 绑定
var baz = new bar(3);
console.log(obj1.a);//2
console.log(baz.a);//3
【解释】依据硬绑定的原理,bar被绑定到了obj1上,并且后面无法再更改bar的绑定。所以在硬绑定阶段输出的是2. 执行到new绑定的时候。 如果new绑定没有改变bar函数的绑定的话,输出的obj1.a应该是3,但是输出的依然是2. 说明new绑定已经改变了bar的绑定。打印出新产生的对象,发现新对象多了一个a属性。
判断this步骤
1、函数是否在new中调用?如果是的话this绑定的是新创建的对象
2、函数是否通过call、apply或者硬绑定调用,如果是的话this为传入的对象
3、函数是否在某个上下文对象中调用(隐私绑定)
4、默认绑定
绑定例外的情况
1、将null 或者undefined作为this的绑定对象传入call、apply或者bind. 此时会使用默认绑定。
function foo(){
console.log(this.a)
}
var a = 2;
foo.call(null); //2
会传入null的情况
- 情况一 : 展开一个数组
- 情况二 : 函数柯里化
function foo(a,b){
console.log("a:"+a+",b:"+b)
}
//把数组展开成参数
foo.apply(null,[2,3]);
//使用bind进行柯里化
var bar = foo.bind(null,2);
bar(3)
【注意】直接传入空对象可能会导致一些问题,通常的做法是传入一个特殊的空对象
function foo(a,b){
console.log("a:"+a+",b:"+b)
}
var b = Object.create(null);
//把数组展开成参数
foo.apply(b,[2,3]);
//使用bind进行柯里化
var bar = foo.bind(b,2)
bar(3)
2、间接引用
function foo(){
console.log(this.a)
}
var a = 2;
var o={
a:3,
foo:foo
}
var p = {a:4}
o.foo();
(p.foo = o.foo)()//等价于 foo()
foo()
console.log(o.foo)//foo函数
3、软绑定
原理:通过软绑定,我们希望this在默认情况下不再指向全局对象(非严格模式)或 undefined (严格模式),而是指向两者之外的一个对象(这点和硬绑定的效果相同),但是同时又保留了隐式绑定和显式绑定在之后可以修改this指向的能力。
1 if(!Function.prototype.softBind){
2 Function.prototype.softBind=function(obj){
3 var fn=this;
4 var args=Array.prototype.slice.call(arguments,1);
5 var bound=function(){
6 return fn.apply(
7 (!this||this===(window||global))?obj:this,
8 args.concat.apply(args,arguments)
9 );
10 };
11 bound.prototype=Object.create(fn.prototype);
12 return bound;
13 };
14 }
1 function foo(){
2 console.log("name: "+this.name);
3 }
4
5 var obj1={name:"obj1"},
6 obj2={name:"obj2"},
7 obj3={name:"obj3"};
8
9 var fooOBJ=foo.softBind(obj1);
10 fooOBJ();//"name: obj1" 在这里软绑定生效了,成功修改了this的指向,将this绑定到了obj1上
11
12 obj2.foo=foo.softBind(obj1);
13 obj2.foo();//"name: obj2" 隐式绑定修改了foo()函数this的绑定
14
15 fooOBJ.call(obj3);//"name: obj3" 显示绑定修改了foo()函数的this绑定
16
17 setTimeout(obj2.foo,1000);//"name: obj1"
18 /*回调函数相当于一个隐式的传参,如果没有软绑定的话,这里将会应用默认绑定将this绑定到全局环 境上,但有软绑定,这里this还是指向obj1*/
4、箭头函数
es6中的箭头函数无法使用上面的这几种规则。因为箭头函数并不是使用function关键字定义的。而是使用=>操作符定义的。他的this是根据外层(函数或者全局)作用域来决定的.
function foo(){
return (a)=>{
console.log(this.a)
}
}
var obj1={
a:2
}
var obj2 = {
a:3
}
var bar = foo.call(obj1);
bar.call(obj2)//2
foo()函数绑定了obj1对象。bar函数是foo返回的箭头函数。如果bar是按照显示绑定规则那么输出的应该是3,但是实际却输出的为2.