this
this 是什么
当一个函数被调用时,会创建一个活动记录(或者称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。
this
就是这个记录的一个属性,会在函数执行的过程中用到。
如果对这个解释看不太懂可以去看看JavaScript基础之执行上下文
this
实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。
this 值如何确定
this
永远指向最后调用它的对象。
调用位置
在理解 this 的绑定过程之前,首先理解调用位置:调用位置就是函数在代码中被调用的位置。
确定调用位置,最重要的时分析调用栈。
baz() // baz 的调用位置
function baz() {
// 当前的调用栈是 baz
// 因此,当前调用位置是全局作用域
console.log('baz')
bar() // bar 的调用位置
}
function bar() {
// 当前的调用栈是 baz -> bar
// 因此,当前调用位置在 baz 中
console.log('bar')
foo() // foo 的调用位置
}
function foo() {
// 当前的调用栈是 baz -> bar -> foo
// 因此,当前调用位置在 bar 中
console.log('foo')
}
绑定规则
默认绑定
对函数进行不带任何修饰的调用,即为默认绑定。
默认绑定的
this
指向全局对象window
(非严格模式)
独立的函数调用可以看作是 this
的默认绑定规则。
function foo() {
console.log(this.a)
}
var a = 1
foo() // 1
foo()
函数是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定。
通过分析调用位置,我们知道 foo
函数是在全局作用域中运行,那么 foo
函数中的 this
绑定的就是全局对象 window
。
如果使用严格模式:
function foo() {
"use strict"
console.log(this) // undefined
console.log(this.a) // TypeError: Cannot read property 'a' of undefined at foo
}
var a = 1
foo()
"use strict"
function foo() {
console.log(this) // undefined
console.log(this.a) // TypeError: Cannot read property 'a' of undefined at foo
}
var a = 1
foo()
在严格模式下,默认绑定的 this
会指向 undefined
。
虽然
this
的绑定规则完全取决于调用位置,但是只有foo()
运行在非 strict mode 下时,默认绑定才能绑定到全局对象,在严格模式下调用foo()
则不影响默认绑定。
function foo() {
console.log(this.a)
}
var a = 1
(function () {
"use strict"
foo() // 1
})()
隐式绑定
当函数引用有上下文对象时,隐式绑定规则会把函数调用中的
this
绑定到这个上下文对象。
function foo() {
console.log(this.a)
}
var obj = {
a: 1,
foo: foo
}
obj.foo() // 1
foo()
函数并不属于 obj
对象,然而,调用位置会使用 obj
上下文来引用函数,因此你可以说函数被 obj
对象“拥有”或者“包含”函数引用。当 foo()
被调用时,它的前面确实加上了对 obj
的引用,this
被绑定到 obj
,因此 this.a
和 obj.a
是一样的。
分析下面的代码:
var a = 3
function foo() {
console.log(this.a)
}
var obj1 = {
a: 1,
foo: foo
}
var obj2 = {
a: 2,
obj: obj1
}
obj2.obj.foo() // 1
this
永远指向最后调用它的对象。- 如果函数调用前存在多个对象,
this
指向距离调用自己最近的对象。
实际上,上面两个表达的意思是相同的,把 obj2.obj.foo()
按照运算符的优先级分解:
- 先运算
obj2.obj
结果为obj1
obj1.foo
(foo
为obj1
上的属性)结果为foo
(全局上的函数)foo()
执行
从上面的步骤中可以看出,最后调用 foo
函数的对象是 obj1
,距离 foo
函数最近的对象也是 obj1
,因此 foo
函数里的 this
指向 obj1
隐式丢失
思考下面的代码:
function foo() {
console.log(this.a)
}
var obj = {
a: 2,
foo: foo
}
var bar = obj.foo
var a = 'window'
bar() // 'window'
虽然 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 = 'window'
doFoo(obj.foo) // 'window'
我们发现,在函数回调的过程中, this 丢失了绑定对象,这种情况更微妙,但又更常见。
参数传递是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果和上面的例子一样。
显示绑定
使用函数的 call(..) 和 apply(..) 方法在某个对象上强制调用函数。
思考下面的代码:
function foo() {
console.log(this.a)
}
var obj = {
a: 2
}
foo.call(obj) // 2
通过 foo.call(..)
,我们可以在调用 foo
时强制把它的 this
绑定到 obj
上。
在 JavaScript中,当我们调用一个函数时,我们习惯称之为函数调用,函数处于一个被动的状态;而
call
与apply
让函数从被动变主动,函数能主动选择自己的上下文,所以这种写法我们又称之为函数应用。
注意,如果在使用 call
之类的方法改变 this
指向时,指向参数提供的是 null
或者 undefined
,那么 this
将指向全局对象。
function foo() {
console.log(this.a)
}
var obj = {
a: 'obj'
}
var a = 'window'
foo.call(obj) // 'obj'
foo.call(null) // 'window'
- 硬绑定
绑定丢失可以通过应绑定来解决,观察以下代码:
function foo() {
console.log(this.a)
}
function doFoo(fn) {
// fn 其实引用的是foo
fn.call(obj) // 调用位置
}
var obj = {
a: 2,
foo: foo
}
var a = 'window'
doFoo(obj.foo) // 2
我们创建了函数 doFoo()
,并在它的内部手动调用了 foo.call(obj)
,因此强制把 foo
的 this
绑定到了 obj
。
硬绑定是一种非常常见的模式,所以 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
的上下文并调用原始函数。
- API 调用“上下文”
在 JavaScript 的 API 中部分方法也内置了显式绑定,以 forEach
为例:
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
操作符的具体操作过程在这里不提,我们只需要知道 new
操作符创建的新对象会绑定到其构造函数的 this
上。
function foo(a) {
this.a = a
}
var bar = new foo(2)
console.log(bar.a) // 2
箭头函数绑定
箭头函数的
this
继承上层执行上下文里面的this
function foo() {
return () => {
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
也会绑定到 obj1
,箭头函数的绑定无法被修改。(new
也不行!)
思考下面的代码:
var name = 'laozhang'
var grandFather = {
name: 'laowang',
father: {
name: 'xiaowang',
son: {
name: 'xiaoming',
getName: () => {
console.log(this.name) // 1
}
}
},
getName: function () {
console.log(this.name) // 2
var name = 'xiaowang'
var son = {
name: 'xiaoming',
getName1: () => {
console.log(this.name) // 3
},
getName2: function() {
console.log(this.name) // 4
}
}
son.getName1()
son.getName2()
}
}
grandFather.father.son.getName()
grandFather.getName()
上面的代码执行的结果是什么?
- 1处为
'laozhang'
,这里的 this 在箭头函数内,指向上层执行上下文,为全局对象window
- 2处为
'laowang'
, 这里的 this 在普通声明函数中,指向最后一个调用它的对象(grandFather
) - 3处为
'laowang'
,这里的 this 在箭头函数中,指向上层执行上下文,为grandFather
对象 - 4处为
'xiaoming'
,这里的 this 在普通声明函数中,指向最后一个调用它的对象(son
)
对执行上下文的不是很理解的话,可以看看JavaScript基础之执行上下文.
优先级
new 绑定 > 隐式绑定 > 默认绑定
显示绑定 > 隐式绑定 > 默认绑定
new
和 call/apply/bind
无法一起使用,不存在使用场景,无需去比较它们的优先级。
练习
如果觉得上面的内容都理解了,那么可以试试 js 从两道面试题加深理解闭包与箭头函数中的this。
call/apply/bind
定义
在MDN中的定义:
call:
call()
方法使用一个指定的this
值和单独给出的一个或多个参数来调用一个函数。
apply:
apply()
方法调用一个具有给定this
值的函数,以及作为一个数组(或类似数组对象)提供的参数。
bind:
bind()
方法创建一个新的函数,在bind()
被调用时,这个新函数的this
被指定为bind()
的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
语法
call
func.call(thisArg, arg1, arg2, ...)
参数:
thisArg
:可选的。在function
函数运行时使用的this
值。arg1, arg2, ...
:指定的参数列表。(原函数所需的参数)
作用:
改变函数的 this
指向。
示例:
function Product(name, price) {
this.name = name
this.price = price
}
function Food(name, price) {
Product.call(this, name, price)
this.category = 'food'
}
console.log(new Food('cheese', 5).name) // 'cheese'
apply
func.apply(thisArg, [argsArray])
参数:
thisArg
:必选的。在func
函数运行时使用的this
值。argsArray
:可选的。一个数组或者类数组对象,其中的数组元素将作为单独的参数传给func
函数。
作用:
改变函数的 this
指向。
示例:
const numbers = [5, 6, 2, 3, 7];
const max = Math.max.apply(null, numbers);
console.log(max);
// expected output: 7
const min = Math.min.apply(null, numbers);
console.log(min);
// expected output: 2
bind
func.bind(thisArg[, arg1[, arg2[, ...]]])
参数:
thisArg
:调用绑定函数时作为this
参数传递给目标函数的值。如果使用new
运算符构造绑定函数,则忽略该值。arg1, arg2, ...
:当目标函数被调用时,被预置入绑定函数的参数列表中的参数。
作用:
改变函数的 this
指向。
返回值:
返回一个原函数的拷贝,并拥有指定的 this
值和初始参数。
示例:
const module = {
x: 42,
getX: function() {
return this.x;
}
};
const unboundGetX = module.getX;
console.log(unboundGetX()); // The function gets invoked at the global scope
// expected output: undefined
const boundGetX = unboundGetX.bind(module);
console.log(boundGetX());
// expected output: 42
区别
call
、apply
与bind
都用于改变this
绑定,但call
、apply
在改变this
指向的同时还会执行函数,而bind
在改变this
后是返回一个全新的boundFcuntion
绑定函数,这也是为什么上方例子中bind
后还加了一对括号()
的原因。bind
属于硬绑定,返回的boundFunction
的this
指向无法再次通过bind
、apply
或call
修改;call
与apply
的绑定只适用当前调用,调用完就没了,下次要用还得再次绑。call
与apply
功能完全相同,唯一不同的是call
方法传递函数调用形参是以散列形式,而apply
方法的形参是一个数组。在传参的情况下,call
的性能要高于apply
,因为apply
在执行时还要多一步解析数组。
手动实现
call
Function.prototype.myCall = function (context) {
// 判断调用对象是否为函数
if (typeof this !== 'function') {
throw new TypeError(`${this} is not a function`)
}
// 判断传入的对象是否存在,默认是 window
context = context ? Object(context) : window
// 把当前调用的函数赋值给传入对象的 context.fn
context.fn = this
// 声明一个数组用来保存当前调用函数的参数
let args = []
for (let i = 1; i < arguments.length; i++) {
args.push('arguments['+i+']')
}
// 执行调用函数,并返回结果
let result = eval('context.fn(' + args + ')')
// 删除 fn
delete context.fn
return result
}
apply
Function.prototype.myApply = function (context, args) {
// 判断调用对象是否为函数
if (typeof this !== 'function') {
throw new TypeError(`${this} is not a function`)
}
// 判断传入的对象是否存在,默认是 window
context = context ? Object(context) : window
// 把当前调用的函数赋值给传入对象的 context.fn
context.fn = this
// 判断是否传入函数参数
if(!args){
return context.fn()
}
// 判断传入的参数是否为数组
if (!(args instanceof Array)) {
throw new TypeError(`${args} is not a array`)
}
// 执行调用函数,并返回结果
let result = eval('context.fn('+ args +')')
// 删除 fn
delete context.fn
return result
}
bind
Function.prototype.myBind = function (context) {
// 判断调用对象是否为函数
if (typeof this !== 'function') {
throw new TypeError(`${this} is not a function`)
}
// 获取调用函数
let that = this
// 将调用函数的参数转成数组
let bindArgs = Array.prototype.slice.call(arguments, 1)
// 声明一个新函数 Fn
function Fn() {}
function bindFn() {
// 将 myBind 函数的参数转成数组
let args = Array.prototype.slice.call(arguments)
// 绑定 this 指向
// 如果绑定的函数被 new 执行,当前函数的 this 就是当前的实例
that.apply(this instanceof bindFn ? this : context, bindArgs.concat(args))
}
// new 出来的结果可以找到原有类的原型
Fn.prototype = that.prototype
bindFn.prototype = new Fn()
// 返回一个绑定后的函数
return bindFn;
}
参考
《你不知道的JavaScript上卷》