学习this关键字的第一步是理解:
- this并不指向函数本身
- this也不指向函数的词法作用域
this是在运行时进行绑定的,并不是在编写代码时绑定的,它的上下文取决于函数调用的各种条件,而和函数的声明位置没有任何关系,只取决于函数的调用方式
当一个函数调用时,会创建一个活动记录(有时候也称之为执行上下文),这个记录会包含函数:
- 调用位置
- 调用方式
- 传入的参数信息
- 等等…
this就是这个记录的一个属性
在理解this的绑定过程之前,首先要知道什么是函数的调用位置:调用位置就是函数在代码中被调用的位置(而不是声明位置)
this的绑定有4条规则,在理解函数的调用位置之后,我们依次来分析这4条规则
默认绑定
默认绑定用于最常用的函数调用类型:独立函数调用
思考下面的代码
function foo() {
console.log(this.a);
}
var a = 2;
foo(); // 2
首先我们应该注意foo()中并没有定义过一个叫做a的变量,但是IDE并不会报错,函数依然会有一个输出值,这个输出值是2,代表着this.a被解析成了全局变量a,为什么会发生这种状况呢?这就是我们的this的第一条绑定规则的作用,函数调用应用了this的默认绑定,因此this指向全局对象
通过分析函数的调用位置,我们可以发现foo()是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应用其他规则
但是需要我们注意的是,默认绑定规则在严格模式下不能生效
"use strict"
function foo() {
console.log(this.a);
}
var a = 2;
foo(); // TypeError: this is undifned
隐式绑定
隐式绑定需要考虑的是函数的调用位置是否有上下文对象,或者是函数的调用位置是否被某个对象拥有或包含
思考下面的代码
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
fn: foo
};
obj.fn() // 2
首先我们应该注意到foo()的声明位置是在全局作用域当中,并不被对象obj拥有或包含,foo()是被当作引用属性被添加进obj当中的,但是无论是直接在obj中定义还是先定义再添加为引用属性,这个函数严格来说都不属于对象obj
然而,看看fn()(实际上是foo())的调用位置,调用位置会使用obj上下文来引用函数,因此你可以说函数被调用时对象obj“包含”或“拥有”函数foo()
无论你如何称呼这个模式,可以确定的是,当foo()被调用时,它的前面确实加上了对象obj的引用,当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象,因此,在这段代码中this.a和obj.a是等价的
需要我们注意的是对象属性引用链只有最后一层在调用位置中起作用,例子:
function foo() {
console.log(this.a);
}
var obj1 = {
a: 2,
fn: foo
};
var obj2 = {
a: 32,
obj1: obj1
}
obj2.obj1.fn(); // 2
隐式丢失
一个最常见的this绑定问题是隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,将this绑定绑定到全局对象或者undefined上,取决于是否是严格模式
思考下面的代码:
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
fn: foo
};
var bar = obj.fn;
var a = "哦豁,丢失绑定对象了...";
bar(); // 哦豁,丢失绑定对象了...
虽然 bar 是 obj.fn 的一个引用,但是,实际上它引用的是 foo() 本身,因此此时的 bar() 其实是一个不带任何上下文修饰的函数调用,因为应用了默认绑定,this绑定到了全局对象上了(非严格模式下)
一个更加微妙且常见的情况发生在参数传递时:
function foo() {
console.log(this.a);
}
function doFoo(fn) {
fn();
}
var obj = {
a: 2,
fn: foo
};
var a = "哦豁,丢失绑定对象了...";
doFoo(obj.fn); // 哦豁,丢失绑定对象了...
参数传递是一种隐式赋值,因此我们传入函数作为参数时也会被隐式复制,所以结果和上一个例子一样,把函数传入JavaScript内置的函数而不是传入你自己生命的函数,如 setTimeout() ,结果也是一样的
显式绑定
隐式绑定需要我们在一个对象内部包含一个指向某个函数的属性,并通过这个属性间接引用这个函数,从而把 this 隐式地绑定到这个对象上,那么如果我们不愿意构建一个包含函数引用的对象,而想在某个对象上强制调用一个函数,那么我们就需要用到显示绑定
JavaScript中几乎所有函数不管是JavaScript提供的还是我们自己创建的都可以使用 call() 和 apply() 方法
这两个方法的第一个参数是一个对象,是给 this 准备的,接着在调用这个函数时 this 将绑定到你作为参数传递的对象上,因为你可以直接置顶 this 的绑定对象,所以我们称之为显示绑定
function foo() {
console.log(this.a);
}
var obj = {
a: 2
};
foo.call(obj); // 2
通过 foo.call() 我们将 obj 对象强制绑定到了 this 上面
如果你传入的是一个原始值(字符串类型、布尔类型、数字类型…),来当作 function.call() 的第一个参数也就是 this 的绑定对象,那么这个原始值会被封装成它的对象形式,也就是(new String(…), new Boolean(…), new Number(…))我们熟知的装箱
硬绑定
硬绑定是显示绑定的一个子元素,它可以解决我们之前提到过的隐式丢失的问题
看看下面的代码
function foo() {
console.log(this.a);
}
var obj1 = {
a: 2
};
var obj2 = {
a: 4
};
var bar = function() {
foo.call(obj1);
};
bar(); // 2
setTimeout(bar, 1000); // 2
bar.call(obj2); // 2,已经进行过硬绑定的 bar 无法再修改 this 指向的对象
我们在这段代码中创建了 foo() ,并在它的内部手动调用 foo.call(obj1) ,因此强制把 obj1 绑定到了 foo 的 this 上,之后无论如何调用 bar() ,它总会手动在 obj 上调用 foo ,这种绑定是一种显示的强制绑定,所以称之为硬绑定
由于硬绑定是一种非常常用的模式,所以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 的上下文并调用原始函数
new绑定
这是 this 绑定规则的最后一条,在讲解它之前我们首先需要澄清一个非常常见的关与 JavaScript 中函数和对象的误解
在传统的面向对象语言中,构造函数是一个类中的一个特殊方法,使用 new 操作符初始化某个类时会调用这个类当中的构造函数,通常的形式是: something = new MyClass(…);
JavaScript 中也有一个 new 操作符,使用方法看起来也和那些面向对象的语言一样,绝大多数开发者(包括学习之前的我)都认为 JavaScript 中 new 的机制也和那些语言一样,然而在 JavaScript 中 new 的机制实际上和那些面向对象的语言完全不同
首先我们重新定义一下 JavaScript 中的构造函数,在 JavaScript 中,构造函数只是一些使用 new 操作符时被调用的函数,它们并不属于某个类,也不会实例化一个类。实际上,它们甚至都不能说是一种特殊的函数类型,他们只是被 new 操作符调用的普通函数而已
举例来说,在ES5.1中这样描述 Number(…)作为构造函数时的行为:
当 Number 在 new 表达式中被调用时,它是一个构造函数:他会初始化新创建的对象
所以,包括内置对象函数在内的所有函数都可以被 new 操作符调用,这种函数调用被称之为构造函数调用,这里有一个很重要但非常细微的差别:实际上并不存在所谓的构造函数,只有对某个函数的构造函数调用
使用 new 操作符调用函数,也就是我们刚刚才提到的构造函数调用时,会自动执行以下操作:
- 构造一个全新的对象
- 这个新对象会被执行[[Prototype]]连接
- 这个新对象会绑定到函数调用的 this
- 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象
我们先忽略掉 step 2 ,思考下面的代码
function foo() {
this.a = a;
}
var bar = new foo(2);
console.log(bar.a); // 2
使用 new 操作符来调用 foo() 时,我们会构造一个新对象并把它绑定到 foo() 调用的 this 上。new 操作符是最后一种可以影响函数调用时 this 绑定行为的方法,我们称之为:new绑定