深刻浅出JavaScript的this机制

本文发于ONE中无一,转载请注明出处,谢谢。javascript

「this 是在函数被调用时发生的绑定,它指向什么彻底取决于函数在哪里被调用。」java

说到this,可能要涉及到一点JavaScript基础:数组

「词法做用域」与「动态做用域」

一般来讲,做用域一共有两种主要的工做模型。浏览器

  • 词法做用域
  • 动态做用域

词法做用域:定义在词法阶段的做用域,也就是说词法做用域是由你在写代码时将变量和块做用域写在哪里决定的缓存

动态做用域:动态做用域并不关心函数和做用域是如何声明以及在任何处声明的,只关心它们从何处调用。换句话说,做用域链是基于调用栈的,而不是代码中的做用域嵌套闭包

JavaScript采用的是词法做用域,大多数时候,对做用域产生混乱的主要缘由是分不清楚应该按照函数位置的嵌套顺序,仍是按照函数的调用顺序进行变量查找。再加上this机制的干扰,使得变量查找极易出错。app


下面咱们就深刻了解一下JavaScript的this机制函数

this 的四种绑定规则

在JavaScript中,this绑定规则有四种:oop

  • 默认绑定ui

  • 隐式绑定

  • 显式绑定

  • new 绑定

默认绑定

默认绑定是JavaScript中this绑定最基本、最直接的一种绑定方式,简而言之,就是直接调用函数

function foo(){
    console.log(this.a); // 输出: 2 直接调用 this指向全局对象window
}

var a = 2;
foo(); // 等价于 window.foo(); foo()由 window直接调用,故this指向 window 
复制代码

注:在浏览器中全局对象是BOM的window对象,Node中是global对象

Node环境下,没有window对象,只有全局的global对象
复制代码

严格模式下this指向会有一些误差,请注意区分

一、严格模式下,独立调用 的函数的this为undefined

二、非严格模式下,使用call、apply时,null、undefined会被转换为全局对象(window/global),严格模式下,this始终是指定的值

// 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
})();

// demo4
var color = 'red';
function displayColor(){
    console.log(this.color);
}
displayColor.call(null);//red

var color = 'red';
function displayColor(){
 'use strict';
    console.log(this.color);
}
displayColor.call(null);// TypeError: Cannot read property 'color' of null
复制代码

在默认绑定中存在几种容易混淆的状况:

  • 嵌套函数独立调用(this默认绑定到window)
  • IIFE当即执行函数
  • 闭包

嵌套函数独立调用

// Code nested
var a = 0;
var obj = {
    a : 2,
    foo:function(){
        function test(){
        //虽然test()函数被嵌套在obj.foo()函数中,但test()函数是独立调用,而不是方法调用。因此this默认绑定到window
        console.log(this.a);
        }
        test(); 
    }
}
obj.foo();//0
复制代码

IIFE(Imdiately Invoked Function Expression)当即执行函数

IIFE有一点的特殊性,但IIFE函数实际上就是函数声明后直接调用执行,这样this绑定就比较清楚了

// 上个例子 Code nested 等价于这个IIFE版本
var a = 0;
function foo(){
    (function test(){
        console.log(this.a);
    })()
};
var obj = {
    a : 2,
    foo:foo
}
obj.foo();//0
复制代码

闭包

var a = 0;
function foo(){
    function test(){
        console.log(this.a);
    }
    return test;
};
var obj = {
    a : 2,
    foo:foo
}
obj.foo()();// 0 闭包中返回的函数test在这里是独立调用,而不是方法调用,因此this指向window
复制代码

闭包中的this默认指向window对象,有时候咱们须要在闭包中访问嵌套函数的this,因此咱们也经常使用临时变量 var that = this 缓存外层的this,而后在闭包中使用缓存变量that去访问外层的this


隐式绑定

在隐式绑定中,this的指向可能受「上下文对象」影响,对于函数来说也就是方法调用。

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

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

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

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

// 只有对象属性引用链最后一层影响调用点
// 结果是42而不是2
var obj3 = {
	a: 42,
	foo: foo
};

var obj4 = {
	a: 2,
	obj3: obj3
};

obj4.obj3.foo(); // 42 谁直接调用,this就指向谁
复制代码

注:当调用一个函数时,若是该函数具备上下文对象,this会被绑定到该上下文对象 当调用obj1.foo();this 指向 obj1

首先去判断谁是最直接调用,谁直接调用this就指向谁,若是没有,this指向window,这种方法适用于大部分状况

特殊状况:隐式丢失

隐式丢失是指被隐式绑定的函数丢失绑定对象,从而默认绑定到window。这是一种常见的问题,须要注意判别

隐式丢失通常有如下几种状况:

  • 【函数别名】
  • 【参数传递】
  • 【内置函数】
  • 【间接引用】
  • 【其余状况】

【函数别名】

var a = 0;
function foo(){
    console.log(this.a);
};
var obj = {
    a : 2,
    foo:foo
}
// 把obj.foo赋予别名bar,形成了隐式丢失
// 由于只是把foo()函数赋给了bar,而bar与obj对象则毫无关系,故this指向window
var bar = obj.foo;
bar();//0

// 上面的例子等价于
var a = 0;
var bar = function foo(){
    console.log(this.a);
}
bar();// 0
复制代码

【参数传递】

var a = 0;
function foo(){
    console.log(this.a);
};
function bar(fn){
    fn();
}
var obj = {
    a : 2,
    foo:foo
}
// 把obj.foo看成参数传递给bar函数时,有隐式的函数赋值fn=obj.foo
// 与上例相似,只是把foo函数赋给了fn,而fn与obj对象则毫无关系
bar(obj.foo); // 0

// 上面的例子等价于
var a = 0;
function bar(fn){
    fn();
}
bar(function foo(){
    console.log(this.a);
});
复制代码

函数做为参数传递,若是想函数执行时保留this指向,使用硬绑定。在上例中,foo.bind(obj)代替obj.foo

【内置函数】

var a = 0;
function foo(){
    console.log(this.a);
};
var obj = {
    a : 2,
    foo:foo
}
setTimeout(obj.foo,100);// 0

// 等价于
var a = 0;
setTimeout(function foo(){
    console.log(this.a);
},100);//0
复制代码

【间接引用】

函数的"间接引用"很容易在无心间建立,最容易在赋值时发生,会形成隐式丢失

// demo1
function foo() {
   console.log( this.a );
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
// 将o.foo函数赋值给p.foo函数,而后当即执行。
// 至关于仅仅是foo()函数的当即执行
(p.foo = o.foo)(); // 2

// demo2 对比 demo1
function bar () {
    console.log( this.a );
}
var a = 2;
var o = { a: 3, bar: bar };
var p = { a: 4 };
o.bar(); // 3
// 将o.foo函数赋值给p.foo函数,以后p.foo函数再执行,是属于p对象的foo函数的执行
p.bar = o.bar;
p.bar();// 4

复制代码

【其余状况】

var a = 0;
var obj = {
    a : 2,
    foo:foo
};
function foo() {
    console.log( this.a );
};

(obj.foo = obj.foo)(); // 0

(false || obj.foo)(); // 0

(1, obj.foo)(); // 0 知识点:逗号操做符 对它的每一个操做数求值(从左到右),并返回最后一个操做数的值。
复制代码

判断函数时直接调用,仍是经过对象的方法调用就能够判断出this的指向


显式绑定

显式绑定是借助 call(),apply(),bind() 去显式的改变this指向。对于被调用的函数来讲,叫作间接调用。

apply、call 的区别

在 JavaScript 中,call 和 apply 都是为了改变某个**函数运行时的上下文(context)**而存在的,简而言之,就是为了改变函数体内部 this 的指向。两者做用彻底同样,都是当即执行函数,只是接受的参数形式不太同样:

var func = function(arg1, arg2) {
};
func.call(this, arg1, arg2, ...)
func.apply(this, [arg1, arg2, ...])
复制代码

其中 this 是你想指定的上下文,能够是任何一个 JavaScript 对象(JavaScript 中一切皆对象)

call 须要把参数按顺序传递进去。明确知道参数数量时使用

apply 则是把参数放在数组里。不肯定参数数量时使用,而后把参数 push 进数组传递进去。当参数数量不肯定时,函数内部也能够经过 arguments 这个数组来遍历全部的参数【arguments 已经不推荐使用】。

说到JavaScript中上下文(context),又涉及到几个经常使用的概念:「定义时上下文」和「运行时上下文」以及「上下文是能够改变的」,这里不深刻。

bind

MDN的解释是: bind() 方法会建立一个新函数,称为绑定函数,当调用这个绑定函数时,绑定函数会以建立它时传入bind()方法的第一个参数做为 this,传入bind()方法的第二个以及之后的参数加上绑定函数运行时自己的参数按照顺序做为原函数的参数来调用原函数。

bind() 则是建立一个新的包装函数,而且返回该包装函数,便于稍后调用,而不是马上执行。

fun.bind(thisArg[, arg1[, arg2[, ...]]])
复制代码

注:将null或者undefined做为applycall或者bindthis指定值,回到默认绑定的规则。

因为回到默认绑定规则考虑到污染全局对象,传一个空对象替换null或`undefined,以下:

function foo(a,b){...}

// 空对象,也被称为DMZ空对象
var ø = Object.create( null );
                  
foo.apply(ø,[2,3])
复制代码

显式绑定中有个特例:硬绑定 在硬绑定中,this的指向不能再被修改

var a = 0;
function foo(){
    console.log(this.a);
}
var obj = {
    a:2
};
var bar= function(){
    foo.call(obj);
}
// 在bar函数内部手动调用foo.call(obj)。
// 所以,不管以后如何调用函数bar,它总会手动在obj上调用foo
bar(); // 2
setTimeout(bar,100); // 2
bar.call(window); // 2
复制代码

上面提到到this的硬绑定 和 bind 方法,但硬绑定一旦绑定就没法修改this指向,有一种软绑定的实现方案提供了一种更加灵活的绑定方式

//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,
					[].concat.call( 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 <---- 退回到软绑定
复制代码

此外JavaScript还有不少内置函数,默认具备显示绑定的功能:如:map()、forEach()、filter()、some()、every()

var id = 'oops, global';
function foo(el){
    console.log(el,this.id);
}
var obj = {
    id: 'fn'
};
[1,2,3].forEach(foo);// 1 "oops, global" 2 "oops, global" 3 "oops, global"
[1,2,3].forEach(foo,obj);// 1 "fn" 2 "fn" 3 "fn"
复制代码

new 绑定

new 运算符建立一个用户定义的对象类型的实例或具备构造函数的内置对象的实例。

当使用new调用函数时,会执行如下操做(四个步骤):

  • 建立一个全新对象 :obj
  • 对建立的新对象执行 [[Prototype]] 链接:obj.__proto__ = foo.prototype
  • 绑定this到新建立的新对象上:foo函数内this指向obj
  • 由构造函数返回的对象就是 new 表达式的结果。若是函数没有显式的返回一个对象,那么 new 表达式中的函数调用会自动返回这个建立的新对象(通常状况下,构造函数不返回值,可是用户能够选择主动返回对象,来覆盖正常的对象建立步骤)
// this new绑定demo
function foo(a) {
	this.a = a;
}
var bar = new foo( 2 );
console.log( bar.a ); // 2
复制代码

综上:this的四种绑定规则:默认绑定、隐式绑定、显式绑定和new绑定,分别对应函数的四种调用方式:独立调用(直接调用)、方法调用、间接调用和构造函数调用。


优先级

「new 绑定」>「显式绑定」> 「隐式绑定」> 「默认绑定」

// Demo1 
function foo(){
    console.log(this.a);
}
var obj = {
    a:222,
    foo,
};
obj.foo(); // 222;
obj.foo.call({a:1}); // 1 优先级: 显式绑定 > 隐式绑定 

// Demo2
function func1(a){
    this.a = a;
}
var obj1 = {};
var bar = func1.bind(obj1);
bar(2);
console.log(obj1) // {a:2}

var obj2 = new bar(3);
console.log(obj1) // {a:2}
console.log(obj2) // {a:3} 优先级: new绑定 > 显式绑定 
复制代码

ES6 箭头函数 =>

箭头函数没有本身的this, 它的this是继承而来;ES6箭头语法会保存函数建立时的this值,而不是调用时的值。因此this默认指向在定义它时所处的对象(宿主对象),而不是执行时的对象, 定义它的时候,可能环境是window; 箭头函数能够方便地让咱们在 setTimeout ,setInterval中方便的使用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!
复制代码

箭头函数的本质是词法做用域(和调用点决定的机制不同)

相关文章
相关标签/搜索