js基础:this的指向以及call-apply-bind的实现

总是记不住,这里实现一下

this 的绑定规则有哪些?

  • 默认绑定
  • 隐式绑定
  • 显式绑定 / 硬绑定
  • new 绑定

默认绑定

在不能应用其它绑定规则时使用的默认规则,通常是独立函数调用。
如下, 严格模式下 this 为undefined

1
2
3
4
5
function sayHi() {
console.log('Hello,', this.name)
}
var name = 'gg'
sayHi() //gg

隐式绑定

函数的调用是在某个对象上触发的,即调用位置上存在上下文对象。典型的形式为 XXX.fun(), 对象属性链中只有最后一层会影响到调用位置。

1
2
3
4
5
6
7
8
9
10
11
12
function sayHi() {
console.log('Hello,', this.name)
}
var person2 = {
name: 'ggg',
sayHi: sayHi,
}
var person1 = {
name: 'yyy',
friend: person2,
}
person1.friend.sayHi() //Hello,ggg

函数赋值于其他变量:

1
2
3
4
5
6
7
8
9
10
function sayHi() {
console.log('Hello,', this.name)
}
var person = {
name: 'ggg',
sayHi: sayHi,
}
var name = 'yyy'
var Hi = person.sayHi
Hi() // Hello, yyy

回调的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function sayHi() {
console.log('Hello,', this.name)
}
var person1 = {
name: 'YvetteLau',
sayHi: function () {
setTimeout(function () {
console.log('Hello,', this.name)
})
},
}
var person2 = {
name: 'Christina',
sayHi: sayHi,
}
var name = 'Wiliam'
person1.sayHi()
setTimeout(person2.sayHi, 100)
setTimeout(function () {
person2.sayHi()
}, 200)
1
2
3
Hello, Wiliam
Hello, Wiliam
Hello, Christina
  • 情况 1: setTimeout 中回调为默认绑定
  • 情况 2: 相当于 person2.sayHi 函数赋值给了一个中间形参 fn,最后执行 fn, 所以和函数赋值于其他变量一样
  • 情况 3: 对象上触发的,没毛病

显式绑定 / 硬绑定

就是通过 call,apply,bind 的方式,显式的指定 this 所指向的对象。箭头函数无法通过call、apply,bind改变this指向

new 绑定

顾名思义

使用 new 来调用函数,会自动执行下面的操作:

  1. 创建一个新对象
  2. 将构造函数的作用域赋值给新对象,即 this 指向这个新对象
  3. 执行构造函数中的代码
  4. 返回新对象

因此,我们使用 new 来调用函数的时候,就会新对象绑定到这个函数的 this 上。

绑定优先级

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

call/apply/bind 的实现

三者都可以用于改变 this 的指向,区别在于

  • call 接受目标函数和需要枚举参数,并立刻执行
  • apply 接受目标函数和参数数组,并立刻执行
  • bind 接受目标函数和需要枚举参数,返回新函数,不会立刻执行

call 和 apply 如何实现呢?利用隐式绑定的特性,即 obj.xxx(),分三步走:

  1. 将函数设为对象的属性
  2. 执行该函数
  3. 删除该函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Function.prototype.call = function (context) {
// 没有传时,指向 window
var context = context || window
// 例如 func.call(obj), 那么 this 就是函数自身
context.fn = this
// 取得参数
var args = []
for (var i = 1; i < arguments.length; i++) {
args.push('arguments[' + i + ']')
}
// 执行
var result = eval('context.fn(' + args + ')')
// 从对象上删除
delete context.fn
// 返回 result
return result
}

apply 的实现是类似的,只是参数变成了一个显示传入的数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Function.prototype.apply = function (context, arr) {
var context = Object(context) || window
context.fn = this

var result
if (!arr) {
result = context.fn()
} else {
var args = []
for (var i = 0, len = arr.length; i < len; i++) {
args.push('arr[' + i + ']')
}
result = eval('context.fn(' + args + ')')
}

delete context.fn
return result
}

bind 的实现,可以借助柯里化的思路,将传入的 obj 和参数保存在内存中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Function.prototype.bind = function () {
var self = this // 保存原函数
// 保存需要绑定的this上下文(第一个参数,就是传进来的对象)
var context = [].shift.call(arguments)
// 剩余的参数转为数组
var args = [].slice.call(arguments)

return function () {
// 返回一个新函数
// 合并参数
var pars = [].concat.call(args, [].slice.call(arguments))
self.apply(context, pars)
}
}

注意,bind 方法可以接受并保存多余的参数,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let obj = {
a: 3,
func(a, b) {
console.log(this.a + '' + a + b)
},
}
// 执行
obj.func(1, 2) // => 312

// 此时 在 window 上绑定一个 a
window.a = 1
const f = obj.func.bind(window, 1) // 指定第一个参数

// 执行
f(2) // => 112

可以看到,1 是被保存起来的。