YDKJS-this关键字的指向确定

Begin

这是一篇老掉牙的写javascript中this指向的文章。暴露是小白了...

其实挺简单的,找一下调用点在哪里,看看适用la个规则,看看是不是特例,就知道了。嗯...

让我们开始吧!

找调用点

this指向哪个对象,不取决函数如何被声明,取决于函数如何被调用。这被称为调用点。借助chrome的开发者工具,找到调用栈,从上往下的第二个记录就是真正的调用点。(如果不能用辅助工具就自己找了~)

看一些例子:

切换到Sources,在foo函数内打一个断点,刷新页面重新执行,右侧栏Call Stack第一个记录foo说明foo函数在执行,第二个记录是真正的调用点,双击它!

可以看见跳转到foo();这条语句,函数的调用点,在右侧栏可以看到具体在index.js文件的第7行。

上面例子得到的函数调用点是一目了然的,也有情况得到的结果是间接的:

这里foo函数的调用点却是bar(),需要找一下bar是什么,var bar = obj.foo

同样的找一下var bar = foo.bind(obj)

总结来说,使用chrome开发者工具call stack找调用点的办法,大多数情况得到的结果是直接的,少数情况得到的结果是间接的,需要再找一下,比如上面两个例子中的bar

规则+特例

找到调用点后,注意力转移到调用点如何确定this指向。上高中那会儿,化学老师对某一类型的题目老是强调:记住大多数的规律,加上少数的特例,你就记全了

有四个规则

默认绑定规则:独立函数调用(我称之为函数裸奔调用),函数内容在strict mode下?this=undefined:this=window。看些例子:

function foo() {
	console.log( this.a );
}

var a = 2;
foo(); // 2   裸奔调用~
复制代码
//demo1
function Bar() {
	"use strict";
	console.log( this.a );
}

var a = 2;
Bar(); // TypeError: `this` is `undefined`  裸奔调用~

//demo2
'use strict';
function test() {
  console.log(this);
};

test();// undefined  裸奔调用~

//demo3
function foo() {
	console.log( this.a );
}

var a = 2;

(function(){
	"use strict";	//调用点在严格模式下而不是函数内容在严格模式下,this指向window
	foo(); // 2
})();
复制代码

隐含绑定规则:形如obj.fn,调用点用obj对象来引用函数,this指向obj。看些例子:

function foo() {
	console.log( this.a );
}

var obj = {
	a: 2,
	foo: foo
};

obj.foo(); // 2
复制代码
//只有对象属性引用链最后一层影响调用点
//结果是42而不是2
function foo() {
	console.log( this.a );
}

var obj2 = {
	a: 42,
	foo: foo
};

var obj1 = {
	a: 2,
	obj2: obj2
};

obj1.obj2.foo(); // 42
复制代码

明确绑定规则:在调用点强行指定this指向。比如使用call()apply()bind()和某些API提供一个可选参数指定环境。看些例子:

//使用call
function foo() {
	console.log( this.a );
}

var obj = {
	a: 2
};

foo.call( obj ); // 2
复制代码
//forEach参数指定
function foo(el) {
	console.log( el, this.id );
}

var obj = {
	id: "awesome"
};

// 使用 `obj` 作为 `this` 来调用 `foo(..)`
[1, 2, 3].forEach( foo, obj ); // 1 awesome  2 awesome  3 awesome
复制代码

new绑定规则:使用new 的函数的构造器调用。看个例子,以这个例子解释一下new调用的四个步骤:

  1. 一个全新的对象会凭空创建(就是被构建):obj
  2. 这个新构建的对象会被接入原形链obj.__proto__=foo.prototype
  3. 这个新构建的对象被设置为函数调用的 this 绑定 :foo函数内this指向obj
  4. 除非函数返回一个它自己的其他 对象,否则这个被 new 调用的函数将 自动 返回这个新构建的对象 :foo函数没有返回新对象,bar = obj
function foo(a) {
	this.a = a;
}

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

调用点适用多种规则,规则有优先顺序

  1. new绑定、明确绑定>隐含绑定>默认绑定

    比如new obj.foo()obj.foo(obj1)是按照new绑定和明确绑定节奏走的

  2. new绑定>硬绑定(new绑定无法和call、apply一起使用)

    应用是一种“柯里化”,预先设置函数的部分或所有参数

    function foo(p1,p2) {
    	this.val = p1 + p2;
    }
    
    // 在这里使用 `null` 是因为在这种场景下我们不关心 `this` 的硬绑定
    // 而且反正它将会被 `new` 调用覆盖掉!
    var bar = foo.bind( null, "p1" );
    var baz = new bar( "p2" );
    baz.val; // p1p2
    复制代码
特例
  1. 隐含丢失(函数赋值、函数作为参数传递,创建函数的间接引用):隐含绑定规则无效,回到默认绑定的规则。看些例子:

    function foo() {
    	console.log( this.a );
    }
    
    var obj = {
    	a: 2,
    	foo: foo
    };
    
    var bar = obj.foo; // 函数间接引用!
    
    var a = "oops, global"; // `a` 也是一个全局对象的属性
    bar(); // "oops, global"
    复制代码
    function foo() {
    	console.log( this.a );
    }
    
    var obj = {
    	a: 2,
    	foo: foo
    };
    
    var a = "oops, global"; // `a` 也是一个全局对象的属性
    
    setTimeout( obj.foo, 100 ); // "oops, global"
    复制代码
    • 函数作为参数传递,如果想函数执行时保留this指向,使用硬绑定。在上例中,foo.bind(obj)代替obj.foo
  2. null或者undefined作为applycall或者bindthis指定值,回到默认绑定的规则。前面提到的柯里化是这一条特例的应用,可惜被new调用覆盖了。再看一个old way参数展开的例子:

    function foo(a,b){
      console.log(a+b);
    }
    
    //参数展开
    foo.apply(null,[1,2]);	//等价于es6的 foo(...[1,2])
    复制代码
    • 如果函数在严格模式下,指定null或者undefined就是null或者undefined

      var a = 2;
      
      function foo(){
          "use strict";
          console.log(this.a);
      }
      
      foo.apply(null);    //Uncaught TypeError: Cannot read property 'a' of null
      复制代码
    • 回到默认绑定规则考虑到污染全局对象,传一个空对象替换nullundefined

      function foo(a,b){...}
      
      //空对象,也被称为DMZ空对象
      var ø = Object.create( null );
                        
      foo.apply(ø,[2,3])
      复制代码
  3. !!!箭头函数:封闭它的(函数或全局)作用域this是什么,箭头函数this就指向什么。看些例子:

    function foo() {
      // 返回一个箭头函数
    	return (a) => {
        // 这里的 `this` 是词法上从 `foo()` 采用的
    		console.log( this.a );
    	};
    }
    
    var obj1 = {
    	a: 2
    };
    
    var obj2 = {
    	a: 3
    };
    
    var bar = foo.call( obj1 );
    bar.call( obj2 ); // 2, 不是3!
    复制代码
    function foo() {
    	setTimeout(() => {
    		// 这里的 `this` 是词法上从 `foo()` 采用
    		console.log( this.a );
    	},100);
    }
    
    var obj = {
    	a: 2
    };
    
    foo.call( obj ); // 2
    复制代码

    箭头函数的本质是词法作用域(和调用点决定的机制不一样)。上面例子在ES6之前,常这样写:

    function foo() {
    	var self = this; // 词法上捕获 `this`
    	setTimeout( function(){
    		console.log( self.a );
    	}, 100 );
    }
    
    var obj = {
    	a: 2
    };
    
    foo.call( obj ); // 2
    复制代码

其他

最后记录一个有意思的demo和一点想法。

有意思的demo能做的是:默认绑定规则适用时,this指向window,现在想自己指定默认值,称为软绑定。看代码:

//softBind方法
if (!Function.prototype.softBind) {
	Function.prototype.softBind = function(obj) {
		var fn = this,
			curried = [].slice.call( arguments, 1 ),
			bound = function bound() {
				return fn.apply(
					(!this ||
						(typeof window !== "undefined" &&
							this === window) ||
						(typeof global !== "undefined" &&
							this === global)
					) ? obj : this,
					curried.concat.apply( curried, arguments )
				);
			};
		bound.prototype = Object.create( fn.prototype );	
		return bound;
	};
}

//使用
function foo() {
   console.log("name: " + this.name);
}

var obj = { name: "obj" },
    obj2 = { name: "obj2" },
    obj3 = { name: "obj3" };

var fooOBJ = foo.softBind( obj );

fooOBJ(); // name: obj

obj2.foo = foo.softBind(obj);
obj2.foo(); // name: obj2   <---- 看!!!

fooOBJ.call( obj3 ); // name: obj3   <---- 看!

setTimeout( obj2.foo, 10 ); // name: obj   <---- 退回到软绑定
复制代码

其中curried = [].slice.call( arguments, 1 )curried.concat.apply( curried, arguments )两行代码实现了柯里化(bind函数也有),还用到了闭包。

一点想法是:既然this是在函数调用的时候被确定,那能不能假想函数如何被调用,来写函数?

补充:手动实现applycallbind

  1. 使用隐含绑定改变this指向
  2. 使用ES6扩展符解决参数传递问题
  3. 细节处理:检测传入对象是否是特殊的nullundefinedstringnumber或者boolean;删除隐含绑定添加的属性
Function.prototype.calldiy = function(target, ...args) {
  //检测target不同情况
  if(target === null || target === undefined){
    target = window;
  }else{
    target = Object(target);
  }
  target.foo = this;
  let temp = target.foo(...args);
  delete target.foo;  //删除添加的foo属性
  return temp;
}

Function.prototype.applydiy = function(target, args) {
  //检测target不同情况
  if(target === null || target === undefined){
    target = window;
  }else{
    target = Object(target);
  }
  target.foo = this;
  let temp = target.foo(...args);
  delete target.foo;  //删除添加的foo属性
  return temp;
}

Function.prototype.binddiy = function(target, ...args1) {
  // let fn = this;
  // return function(...args2) {
  //   fn.applydiy(target, args1.concat(args2));
  // }
  return (...args2) => this.applydiy(target, args1.concat(args2));
}
复制代码
End

文章为个人总结,不妥之处还请雅正。

转载请注明出处。

参考文献:

你不懂JS:this与对象原型 第二章:this豁然开朗

JavaScript中的this

this apply call bind

JavaScript中的this

手动实现call,apply,bind

转载于:https://juejin.im/post/5b64630ee51d4534b85860bf

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值