共计 3854 个字符,预计需要花费 10 分钟才能阅读完成。
在理解 javascript 的 this 之前,首先先了解一下作用域。
作用域分为两种:
1、词法作用域:引擎在当前作用域或者嵌套的子作用域查找具有名称标识符的变量。(引擎如何查找和在哪查找。定义过程发生在代码书写阶段)2、动态作用域:在运行时被动态确定的作用域。
词法作用域和动态作用域的区别是:词法作用域是在写代码或定义时确定的;动态作用域是在运行时确定的。
this 的绑定规则
this 是在调用时被绑定,取决于函数的调用位置。由此可以知道,一般情况下(非严格模式下),this 都会根据函数调用(调用栈)的上下文来绑定对象。
一、默认绑定
默认绑定:默认绑定是指在非严格模式下,且没有使用别的绑定规则时,this 根据函数调用(调用栈)的上下文来绑定对象(全局对象)。(严格模式下则绑定 undefined)
举个栗子:
function foo() {
console.log(this.a);
};
function bar() {
var a = 3;
foo();
}
var a = 2;
bar(); // 调用栈在全局作用域,this 绑定全局对象
运行结果为:2
// 加上 ”use strict” 运行结果则会变成 this is undefined
这里的函数调用时,使用了默认绑定,函数调用(调用栈)的上下文是全局作用域,因此 this 绑定了全局对象(global)。
eg2:
function foo() {
console.log(this.a)
};
var a = 2;
(function() {
“use strict”
foo();
})();
运行结果为:2
这里需要注意:对于默认绑定,决定 this 绑定对象的不是调用位置是否处于严格模式,而是函数体是否处于严格模式(函数体处于严格模式则 this 绑定 undefined;否则 this 绑定全局对象)。另外:严格模式和非严格模式虽然有可能可以绑定,但是最好不混用。
间接引用一般也是会应用默认绑定规则。
eg:
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。
二、隐式绑定
隐式绑定:由上下文对象调用,绑定到上下文对象。
举个栗子:
function foo() {
console.log(this.a);
};
var obj = {
a: 2,
foo: foo
};
obj.foo(); //2
foo(); //undefined
这段代码中,foo() 被当做引用属性添加到 obj 对象中,obj 调用这个引用属性函数时,会使用该引用属性上下文,this 会被绑定到 obj 对象。(这个函数严格来说不属于 obj 对象,只是作为引用属性)。属于隐式绑定。
而下面 foo() 函数的直接执行,并不是 obj 对象引用,所以上下文对象是全局对象。故 this 绑定了 undefined。属于默认绑定。
对象引用链中只有上一层或者说最后一层在调用位置中起作用。
注意:
隐式绑定的函数会丢失绑定对象。此时它会应用默认绑定,将 this 绑定到全局对象或者 undefined 上,取决于是否是严格模式。eg:
function foo() {
console.log(this.a);
};
var obj = {
a: 2;
foo: foo
}
var bar = obj.foo;
var a = ‘biubiubiu’;
bar();
运行结果:”biubiubiu”
解析:看似 bar 是 obj.foo 的一个引用,实际上 bar 是直接引用了函数 foo,是一个单纯的函数调用,故实为默认绑定。
参数传递就是隐式赋值,因此传入函数时也会被隐式赋值。eg:
function foo() {
console.log(this.a);
};
var obj = {
a: 2,
foo: foo
};
function bar(fn) {
fn();
};
var a = “biubiubiu”;
bar(obj.foo);
运行结果: “biubiubiu”
解析:实际上参数也是隐式赋值,但是参数传入函数中,并在函数中执行。此时也是直接引用了函数 foo,因此也是单纯的函数调用,采用了默认绑定。
把函数传入语言内置函数。(与上面情况基本相似,将自己声明函数改成语言内置函数)回调函数丢失 this 的情况比较常见,况且还有调用回调函数的函数可能还会修改 this。
三、显式绑定
显式绑定:直接将 this 绑定到指定对象上。Javascript 中绝大多数函数和自己所创建的函数都可以使用这两种显式绑定的方法。
1、.call()2、.apply() 这两种绑定方法,第一个参数是 this 绑定的对象。(如果传入的参数是原始值(字符串类型、布尔类型、数字类型),这个原始值就会被转成对象形式(new String、new Boolean、new Number)这个称为:装箱)
举个栗子:
function foo() {
console.log(this.a);
};
var obj = {
a: 2
};
foo.call(obj);
运行结果:2
然鹅,显示绑定并不能解决绑定丢失的问题。这个时候来了一位新朋友 — 硬绑定(bind)。
3、.bind()(硬绑定是常见场景,故 es5 提供了该内置方法 Function.prototype.bind。)bind() 会返回一个新编码函数,把 this 绑定在指定参数上,并调用函数。
举个栗子:
function foo(e) {
console.log(this.a + e);
return this.a + e;
};
var obj = {
a: 2
}
var bar = foo.bind(obj); // 新编码函数
var b = bar(3); // 2 3
console.log(b); // 5
bind() 还有一个功能:将除了第一个用于绑定 this 的参数之外的其他参数传给下层的函数(部分应用,是“柯里化”的一种)。
这里涉及到一个概念:把 null 或者 undefined 作为 this 的绑定对象传入 call、apply、bind,这些值在调用的时候会被忽略,实际应用默认绑定规则。应用场景:1、使用 apply() 展开一个数组,并作为参数传递给一个函数。2、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
解析:传入一个参数作为 this 绑定对象,如果不传则使用占位符(null),此时会使用默认绑定规则。
上面这个例子可能会产生一定的副作用,如果需要运用这种场景并且更加安全。可以创建一个空对象(可以用任意喜欢的名字来命名)。
var ∅ = Object.create(null);
// 上面这个例子就可以改写为:
foo.apply(∅,[2,3]); //a:2,b:3
var bar = foo.bind(∅,2);
bar(3); //a:2,b:3
注意:硬绑定之后不能使用隐式绑定和显式绑定对 this 进行修改在这里介绍一种软绑定的方法 softBind(),检查 this 绑定到全局对象或者 undefined 后,绑定 this 到指定的默认对象。绑定后效果和硬绑定一样,但是保留隐式绑定或者显式绑定修改 this 的能力。
四、new 绑定
Javascript 中的 new 机制与面向类语言的完全不同。在 Javascript 中,构造函数只是一些使用 new 操作符时被调用的函数,不属于一个类,也不会实例化一个类。称为对函数的“构造调用”。
举个栗子:
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log(bar.a); //2
使用 new 的过程会创建一个全新的对象,this 会绑定这个新对象。如果函数没有返回其他对象,则 new 表达式函数调用会返回该新对象。(这个新对象会连接 prototype)
四种绑定规则的优先级为:new> 显式 > 隐式 > 默认
箭头函数
箭头函数是根据外层作用域(函数或全局)来决定 this。(词法作用域取代 this 机制)箭头函数 this 会绑定调用时的对象,且箭头函数的绑定无法修改(new 也不行)。
其实可以理解为,箭头函数的 this 在词法上继承的是它所在的作用域(函数或全局)的 this,而它继承的函数作用域的 this 绑定的是在该函数调用上下文对象,所以箭头函数的 this 间接的绑定在调用上下文对象。
简述:箭头函数 this(绑定作用域 this)– 作用域 this(绑定在调用上下文对象)。
故:箭头函数 this == 调用的上下文对象
举个栗子:
function foo() {
setTimeout(function() {
// 这里的 this 在词法上继承自 foo()
console.log(this.a);
},100);
};
var obj = {a: 2};
foo.call(obj); //2
其实这个栗子也等价于:
function foo() {
var that = this; //lexical capture of this
setTimeout(function() {
console.log(self.a)
},100);
}
… 与上面一样
所以,有两种风格:this 风格(四种规则)和词法作用域风格(that = this 和箭头函数)可供使用。使用时尽量避免混用,否则会造成难以维护的后果。