js 最复杂的机制之一,this终于来了!


1. 调用位置:寻找调用位置最重要的是分析调用栈(为了到达当前执行位置所调用的所有函数)。我们关心的调用位置就在当前正在执行的函数的前一个调用中。

function first() {
	//当前调用栈是: first
	//因此,当前调用位置是全局作用域
	
	console.log("first") ;
	second(); //<-- second 的调用位置
}


function second() {
	//当前调用位置是 first -> second
	// 因此,当前调用位置在 first 中
	
	console.log('second');
	third(); // third调用位置
}

function third() {
	//当前调用位置在 first -> second -> third
	// 当前调用位置在 second 中
	
	console.log("third");
}

first(); // first调用位置

2. 绑定规则:

  • 默认规则:

独立函数调用时,可以把这条规则看作是无法应用其他规则时的默认规则。

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

var a  = 1;

foo(); // 1

解释:

  • foo()是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应用其他规则。
  • 如果使用严格模式,则不能将全局变量用于默认绑定,因此this会绑定到undefined
    • 虽然this的绑定规则完全取决于调用位置,但是只有foo()运行在非struct mode时,默认绑定才能绑定到全局对象;在严格模式下调用foo()则不影响默认绑定;
function foo(){
	console.log(this.a);
}
var a = 2;
(function(){
	"use strict";
	
	foo(); // 2
});

  • 隐式绑定

另外一条需要考虑的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含,不过这种说法可能会造成一些误导。

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

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

obj.foo(); //2

需要注意的是,无论直接在obj中定义,还是先定义再添加为引用属性,这个函数严格来说都不属于obj对象。


然而,调用位置会使用obj上下文来引用函数,因此你可以说函数被调用时obj对象"拥有"或者"包含"函数引用。


对象属性引用链中只有上一层或者说最后一层在调用位置中起作用。

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

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

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

obj1.obj2.foo();//42

隐式丢失


一个常见的this绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而将this绑定到全局对象或者undefined上,取决于是否是严格模式。


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

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

var bar = obj.foo; //函数别名

var a = 'global';

bar();//'global'

虽然bar是obj.foo的一个引用,但是实际上,它引用的是foo函数本身,因此此时的bar()其实是一个不带任何修饰的函数调用,因此应用了默认绑定。


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

function doFoo(fn){
	// fn 其实引用的是foo
	fn();
}

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

var a = "global";
doFoo( obj.foo );//'global'

参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果和上一个例子一样。


如果把函数传入语言内置函数而不是传入自己定义的函数,结果是一样的,没有区别。

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

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

var a = "global" //a是全局对象的属性

setTimeout( obj.foo ,100);//'global'

JavaScript 环境中内置的setTimeout()函数实现和下面的伪代码类似:

function setTimeout(fn,delay){
	//等待delay毫秒
	fn(); // <--调用位置
}

回调函数丢失 this 绑定是非常常见的。除此之外,调用回调函数的函数可能会修改this,在一些流行的JavaScript库中处理器常会把回调函数的this强制绑定到触发事件的DOM元素上。

  • 显式绑定

  • call 和 apply方法

它们的第一个参数是一个对象,是给this准备的,接着在调用函数时将其绑定到this。因此你可以直接指定this的绑定对象。


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

var obj = {
	a:2
};

foo.call( obj ); //2

通过 foo.call(...) ,我们可以在调用foo时强制把它的this绑定到obj上。

如果你传入了一个原始值( 字符串类型,布尔类型或者数字类型)来当作this的绑定对象,这个原始值会被转换成它的对象形式,这通常被称为“装箱”。


硬绑定

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

var obj = {
	a:2
};

var bar = function(){
	foo.call(obj);
};

bar();

setTimeout(bar,100); //2

//硬绑定的bar不可能再修改它的this
bar.call( window ); // 2
//传参又没用上~~

硬绑定的典型应用场景就是创建一个包裹函数,负责接受参数并返回值:

function foo(something){
	console.log( this.a, something);
	return this.a + something;
}

var obj = {
	a:2
};

var bar = function() {
	return foo.apply( obj, arguments );
};

var b = bar( 3 );//  2 3
console.log( b );//5

另一种使用方法是创建一个可以重复使用的辅助函数:

function foo(something){
	console.log( this.a, something);
	return this.a + something;
}

//简单辅助绑定函数
function bind(fn, obj){
	return function(){
		return fn.apply( obj , arguments );
	};
}

var obj = {
	a:2
};

var bar = bind( foo ,obj );

var b = bar( 3 );//2 3 
console.log( b ); // 5

ES5提供了内置的方法Function.prototype.bind,用法如下:

function foo(something) {
	console.log( this.a, something);
	return this.a + something;
}

var obj = {
	a:2
};

var bar = foo.bind( obj );
var b = bar( 3 ); //2 3
console.log( b ); // 5

bind( ... )会返回一个硬绑定的新函数,它会把你指定的参数设置为this的上下文并调用原始函数。


第三方库的许多函数,以及JavaScript语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,通常被称为“上下文”,其作用和bind(...)一样,确保你的回调函数使用指定的this.

function foo(el){
	console.log( el, this.id );
}

var obj = {
	id : "awesome"
};

//调用foo(...)时把this绑定到obj
[1,2,3].forEach(foo,obj);
//1 awesome 2 awesome 3 awesome

  • new绑定

在传统的面向对象的语言中,“构造函数”是类中的一些特殊方法,使用new初始化类时会调用类中的构造函数。但是JavaScript中的new的机制实际上和面向类的语言完全不同。

在JavaScript中,构造函数只是一些使用new操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上,它们甚至都不能说是一种特殊的函数类型,它们只是被new操作符调用的普通函数而已。


也即,实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。

使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。

  1. 创建(或者说构造)一个全新的对象。
  2. 这个新对象会被执行[[prototype]]连接。
  3. 这个新对象会绑定到函数调用的this。
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
function foo(a){
	this.a = a;
}

var bar = new foo(2);
console.log( bar.a ); //2

使用new来调用foo(...)时,我们会构造一个新对象并把它绑定到foo(...)调用中的this上。new时最后一种可以影响到函数调用时this绑定行为的方法,我们称之为new绑定。


  1. 优先级

毫无疑问,默认绑定的优先级时四条规则中最低的,所以我们可以先不考虑它。

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

var obj1 = {
	a : 2,
	foo : foo
}

var obj2 = {
	a : 3,
	foo : foo
}

obj1.foo(); // 2
obj2.foo();// 3

obj1.foo.call( obj2 ); // 3
obj2.foo.call( obj1 ); // 2

可以看到,显示绑定优先级更高,也就是说在判断时应当先考虑是否可以存在显示绑定。


function foo(something){
	this.a = something;
}

var obj1 = {
	foo: foo
};

var obj2 = {};

obj1.foo(2);
console.log( obj1.a ) ;//2

obj1.foo.call( obj2, 3);
console.log( obj2.a ) ;//3

var bar = new obj1.foo(4);
console.log( obj1.a ); // 2
console.log( bar.a ); //4

可以看到 new 绑定比隐式绑定优先级高。


new 和 call/apply 无法一起使用,因此无法通过 new foo.call(obj) 来直接进行测试,但是我们可以通过使用 硬绑定 来测试它俩的优先级。

被忽略的this

如果你把 null 或者 undefined 作为 this 的绑定对象传入 call ,apply 或者 bind ,这些值在调用的时候会被忽略,实际应用的是默认绑定规则:


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

var a = 2;

foo.call( null ); //2

一种非常常见的做法是使用 apply(..)来“展开”一个数组,并当作参数传入一个参数,类似的,bind(..)可以对参数进行柯里化。


function foo(a,b){
	console.log("a:"+a,",b:"+b;
}

//将数组“展开”成参数
foo.apply( null ,[2,3]);//a:2,b:3

//使用bind(..)进行柯里化
let bar = foo.bind( null,2 );
bar( 3 ); //a:2,b:3



总是使用 null 来忽略 this 绑定可能会产生一些副作用。默认绑定会把this绑定到全局对象,如果某个函数确实使用了this,这将导致不可预计的后果(比如修改全局对象)。

更安全的this

一种"更安全"的做法是传入一个特殊的对象,把this绑定到这个对象不会对你的程序产生任何副作用,就像网络一样,我们可以创建一个"DMZ"(非军事区)对象--它就是一个空的非委托的对象。


如果我们在忽略this绑定的时总是传入一个DMZ对象,那就什么都不用担心了,因为任何对于this的使用都会被限制在这个空对象中,不会对全局对象产生任何影响。


间接引用

你可能创建一个函数的“间接引用”,在这种情况下,调用这个函数会应用默认绑定规则。

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()。


注意: 对于默认绑定,决定this绑定对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式。如果函数体处于严格模式,this会被绑定到undefined,否则this会被绑定到全局对象。


软绑定

硬绑定这种方式可以把this强制绑定到指定的对象,防止函数调用应用默认绑定规则。硬绑定会大大降低函数的灵活性,使用硬绑定之后无法使用应隐式绑定或显式绑定来修改this。


如果可以给默认绑定指定一个全局对象和undefined以外的值,那就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显示绑定修改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.creat( fn.prototype );
		return bound;
	}
}

softBind(...)的原理和ES5内置的bind(..)类似。它会对指定的函数进行封装,首先检查调用时的this,如果this绑定到全局对象或者undefined,那就把指定的默认对象obj绑定到this,否则不会修改this.此外,这段代码还支持可选的柯里化。


箭头函数this词法

箭头函数不使用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

foo() 内部创建的箭头函数会捕获调用时foo()的 this。由于foo()的this绑定到obj1,bar(引用箭头函数)的this, bar (引用箭头函数)的this也会绑定到obj1,箭头函数的绑定无法被修改。


箭头函数最常用于回调函数,例如事件处理器或者定时器:

function foo() {
	setTimeout( ()=>{
		//这里的this在词法上继承自foo()
		console.log( this.a );
	},1000);
}

var obj = {
	a:2
}

foo.call( obj ); //2 

箭头函数可以像bind()一样确保函数的this被绑定到指定对象,此外,其重要性还体现在它用更常见的词法作用域取代了传统的this机制。实际上,在ES6之前我们就已经在使用一种几乎和箭头函数完全一样的模式。

function foo(){
	var self = this;
	setTimeout( fucntion(){
		console.log(self.a)
	},1000)
}

var obj = {
	a : 2
};

foo.call( obj ); //2

遗留问题:硬绑定无法被改绑的原理是什么?

var obj1 = {
    a:1

};
var obj2 = {
    a:2
};

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

var bar = foo.bind(obj1);
bar();//1
bar.call(obj2);//1 硬绑定无法被改绑