上一篇文章js中this指向全面解析——四种绑定规则讲了this指向哪里,
今天就来说一说四种绑定规则也有例外的情况,还有关于ES6箭头函数中this的问题
本文讲述两个内容:绑定例外、this词法
绑定例外
一、被忽略的this
null或者undefined 作为this的绑定对象传入call、apply或者bind,这些值在调用时会被忽略,实际应用的是默认绑定规则:(不懂默认绑定可以看上一篇文章)
function foo() {
console.log( this.a );
}
var a = 2;
foo.call( null ); // 2
什么时候会把null或者undefined传给this呢?
如果函数并不关心this,但是仍然需要传入一个占位置,那么可以传入
介绍一种非常有用的方法,bind(…)可以对参数进行柯里化(预先设置一些参数)
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 把数组“展开”成参数
foo.apply( null, [2, 3] ); // a:2, b:3
// 使用 bind(..) 进行柯里化
var bar = foo.bind( null, 2 );
bar( 3 ); // a:2, b:3
然而,总是使用 null 来忽略 this 绑定可能产生一些副作用。如果某个函数确实使用了this(比如第三方库中的一个函数),那默认绑定规则会把 this 绑定到全局对象(在浏览 器中这个对象是 window),这将导致不可预计的后果(比如修改全局对象)。
更安全的this
更安全的做法,创建一个“DMZ”对象——一个空的非委托的对象
创建一个空对象最简单的方法是Object.create(null),可以用任何名字来命名DMZ对象
object.create(null)和{ }很像,但是不会创建Object.prototype 这个委托,所以它比 {}“更空”
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 我们的 DMZ 空对象
var ø = Object.create( null );
// 把数组展开成参数
foo.apply( ø, [2, 3] ); // a:2, b:3
// 使用 bind(..) 进行柯里化
var bar = foo.bind( ø, 2 );
bar( 3 ); // a:2, b:3
使用变量名ø可以让函数变得更加“安全”,提高代码可读性
二、间接引用
函数的间接引用时,调用这个函数会应用默认绑定规则
间接引用最容易在赋值时发生:
function foo() {
console.log( this.a );
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
(p.foo = o.foo)(); // 2
(p.foo = o.foo)();
这句语句实际上调用的是foo(),不是p.foo 或者 o.foo 。
所以这里应用的是默认绑定
三、软绑定
硬绑定可以把this强制绑定到指定的对象(除了使用new时但是硬绑定之后无法用隐式绑定或显式绑定的方法修改this,大大降低了函数的灵活性。
可以通过软绑定 的方法,该方法实现了和硬绑定相同的效果,同时保留隐式绑定或者显式绑定修改this的能力
if (!Function.prototype.softBind) {
Function.prototype.softBind = function(obj) {
var fn = this;
// 捕获所有 curried 参数
var curried = [].slice.call( arguments, 1 );
var bound = function() {
return fn.apply(
(!this || this === (window || global)) ?
obj : this
curried.concat.apply( curried, arguments )
);
};
bound.prototype = Object.create( fn.prototype );
return bound;
};
}
它会对指定的函数进行封装,首先检查调用时的 this,如果 this 绑定到全局对象或者 undefined,那就把指定的默认对象 obj 绑定到 this,否则不会修改 this
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 ); //实际上是传递了 foo ,使用了默认绑定
// name: obj <---- 应用了软绑定
分析一下,
var fooOBJ = foo.softBind( obj ); 使用了软绑定把foo的obj绑定到this上
obj2.foo = foo.softBind(obj); 首先使用了软绑定把obj绑定带this上,后来obj2.foo 应用了隐式绑定把obj2绑定到了this
fooOBJ.call( obj3 ); 将原来使用软绑定把this指向obj修改成指向obj3 这里应用的是显式绑定
setTimeout( obj2.foo, 10 ); 将obj2.foo作为参数,实际上是 foo 作为参数传递给setTimeout函数,绑定丢失了obj2对象
this词法
es6中的箭头函数无法使用四种标准规则,根据外层(函数或者全局)作用域来决定this,而且箭头函数被绑定后无法修改
function foo() {
var a=5;
// 返回一个箭头函数
return (a) => {
//this 继承自 foo()
console.log( this.a );
};
}
var obj1 = {
a:2
};
var obj2 = {
a:3
};
var a=4;
var bar = foo.call( obj1 );
bar.call( obj2 ); // 2, 不是 3
var b=foo();
b(); //4
分析:
foo()内部创建的箭头函数的this会指向foo函数的this。
var bar = foo.call( obj1 ); 由于 foo() 的 this 绑定到 obj1,bar(引用箭头函数)的 this 也会绑定到 obj1,
bar.call( obj2 ); 箭头函数的绑定无法被修改。
var b=foo(); foo()的this指向全局的a应用默认绑定
b(); //4
箭头函数常用于回调函数中,例如事件处理器或者定时器
function foo() {
setTimeout(() => {
// 这里的 this 在此法上继承自 foo()
console.log( this},100);
}var obj = {
a:2
};
foo.call( obj ); // 2
es6之前跟箭头函数完全一样的模式:
function foo() {
var self = this; // lexical capture of this
setTimeout( function(){
console.log( self.a );
}, 100 );
}
var obj = {
a: 2
};
foo.call( obj ); // 2
如果你经常编写 this 风格的代码,但是绝大部分时候都会使用 self = this 或者箭头函数
来否定 this 机制,那你或许应当:
- 只使用词法作用域并完全抛弃错误 this 风格的代码;
- 完全采用 this 风格,在必要时使用 bind(…),尽量避免使用 self = this 和箭头函数。
箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。这其实和 ES6 之前代码中的 self = this 机制一样。
这篇文章是在看《你不知道的JavaScript》(上卷) this绑定例外之后再加上自己的一些尝试和理解写下的读书笔记。
参考:《你不知道的JavaScript》(上卷)