JavaScript进阶笔记(三):this绑定

在 JavaScript 执行上下文创建阶段,会进行 this 绑定,当时提到有五种绑定规则:

  1. 默认绑定
  2. 隐式绑定
  3. 显式绑定(硬绑定)
  4. new 绑定
  5. 箭头函数绑定

一、默认绑定

在全局环境中默认 this 指向全局对象。严格模式下,this 会指向 undefined 。

二、隐式绑定

当函数引用有上下文对象时,隐式绑定规则会把函数中 this 绑定到这个上下文对象。

1
2
3
4
5
6
7
8
9
10
function foo() {
console.log( this.a );
}

var obj = {
a: 2,
foo: foo
};

obj.foo(); // 2 this 指向 obj

2.1 隐式缺失

被隐式绑定的函数特定情况下会丢失绑定对象,应用默认绑定,把this绑定到全局对象或者undefined上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 虽然bar是obj.foo的一个引用,但是实际上,它引用的是foo函数本身。
// bar()是一个不带任何修饰的函数调用,应用默认绑定。
function foo() {
console.log( this.a );
}

var obj = {
a: 2,
foo: foo
};

var bar = obj.foo; // 函数别名,引用的foo函数本身。

var a = "oops, global"; // a是全局对象的属性

bar(); // "oops, global"

参数传递就是一种隐式赋值,传入函数时也会被隐式赋值。回调函数丢失this绑定是非常常见的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function foo() {
console.log( this.a );
}

function doFoo(fn) {
// fn其实引用的是foo

fn(); // <-- 调用位置!
}

var obj = {
a: 2,
foo: foo
};

var a = "oops, global"; // a是全局对象的属性

doFoo( obj.foo ); // "oops, global"

// ----------------------------------------

// JS环境中内置的setTimeout()函数实现和下面的伪代码类似:
function setTimeout(fn, delay) {
// 等待delay毫秒
fn(); // <-- 调用位置!
}

三、显示绑定

通过call(..) 或者 apply(..)方法。第一个参数是一个对象,在调用函数时将这个对象绑定到this。因为直接指定this的绑定对象,称之为显示绑定。

1
2
3
4
5
6
7
8
9
function foo() {
console.log( this.a );
}

var obj = {
a: 2
};

foo.call( obj ); // 2 调用foo时强制把foo的this绑定到obj上

显示绑定无法解决丢失绑定问题。

3.1 call 和 apply 区别

call 和 apply 的作用是一样的,只是参数的形式不同。

  • apply 方法传入两个参数:一个是函数上下文对象,另一个是作为函数参数的所组成的数组
  • calll 方法第一个参数也是函数上下文对象,后面的是参数列表,而不是单个数组。

apply 适用于参数本来就是数组的有关联的,而 call 适用参数散列的。两个作用相同,哪个方便用哪个即可。

四、绑定丢失的解决方案

4.1 硬绑定

创建函数bar(),并在它的内部手动调用foo.call(obj),强制把foo的this绑定到了obj。这种方式让我想起了借用构造函数继承,没看过的可以点击查看 JavaScript常用八种继承方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function foo() {
console.log( this.a );
}

var obj = {
a: 2
};

var bar = function() {
foo.call( obj ); // foo.apply(obj, arguments)
};

bar(); // 2
setTimeout( bar, 100 ); // 2

// 硬绑定的bar不可能再修改它的this
bar.call( window ); // 2

ES5 内置了Function.prototype.bind,bind会返回一个硬绑定的新函数,用法如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
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

4.2 bind 和 call 的区别 ?

ES5中添加了 bind 方法,低版本IE不兼容,与 bind 功能类似,接收两部分参数,第一个参数函数上下文对象,后面是参数列表。

不同点:

  • bind返回值是一个函数。
1
2
3
4
5
6
7
8
9
10
11
function foo () {
console.log(this.a)
}

var obj = {
a: 10
}

var bar = foo.bind(obj)
bar()
// bind 返回一个函数,bar 的this指向 obj。而 foo 函数的 this 还是指向 window 对象。
  • 参数使用

call 是把第二个及以后的参数作为 func 方法的实参传进去,而 func1 方法的实参实则是在 bind 中参数的基础上再往后排。

CodePen

兼容低版本 IE 浏览器

1
2
3
4
5
6
7
8
9
10
if (!Function.prototype.bind) {
Function.prototype.bind = function () {
var self = this, // 保存原函数
context = [].shift.call(arguments), // 保存需要绑定的this上下文
args = [].slice.call(arguments); // 剩余的参数转为数组
return function () { // 返回一个新函数
self.apply(context,[].concat.call(args, [].slice.call(arguments)));
}
}
}

4.3 API调用的 ‘上下文’

JS许多内置函数提供了一个可选参数,被称之为“上下文”(context),其作用和bind(..)一样,确保回调函数使用指定的this。这些函数实际上通过call(..)和apply(..)实现了显式绑定。

1
2
3
4
5
6
7
8
9
10
11
12
function foo(el) {
console.log( el, this.id );
}

var obj = {
id: "awesome"
}

var myArray = [1, 2, 3]
// 调用foo(..)时把this绑定到obj
myArray.forEach( foo, obj );
// 1 awesome 2 awesome 3 awesome

五、new 绑定

构造函数其实只是一个普通的函数,不同点是使用 new 调用,称为构造调用。

new 调用函数时发生什么?

  • 创建一个新对象。
  • 这个新对象连接到原型,this 指向这个新对象。
  • 执行构造函数中的代码,绑定其他属性。
  • 返回新对象。
1
2
3
4
5
6
7
8
9
// 手写一个 new 实现。

function create() {
var obj = new Object() // 创建一个新对象
Con = [].shift.call(arguments) // 获取arguments第一个元素,此时argruments第一个元素被移除。
obj.__proto__ = Con.prototype // 绑定原型,obj可以访问构造函数中的属性
var ret = Con.apply(obj, arguments) // 绑定 this ,实现继承。
return ret instanceof Object ? ret : obj // 优先返回构造函数创建的对象
}
  1. 用new Object()的方式新建了一个对象obj
  2. 取出第一个参数,就是我们要传入的构造函数。此外因为 shift 会修改原数组,所以 arguments会被去除第一个参数
  3. 将 obj的原型指向构造函数,这样obj就可以访问到构造函数原型中的属性
  4. 使用apply,改变构造函数this 的指向到新建的对象,这样 obj就可以访问到构造函数中的属性
  5. 返回 obj

把 null 或者 undefined 作为 this 绑定对象传入 call 、apply 或者 bind,这些值在调用时会被忽略,实际应用的是默认绑定。

六、箭头函数

其实大部分情况下可以用一句话来概括,this总是指向调用该函数的对象。

但是对于箭头函数并不是这样,是根据外层(函数或者全局)作用域(词法作用域)来决定this。

对于箭头函数的this总结如下:

  • 箭头函数不绑定this,箭头函数中的this相当于普通变量。
  • 箭头函数的this寻值行为与普通变量相同,在作用域中逐级寻找。
  • 箭头函数的this无法通过bind,call,apply来直接修改(可以间接修改)。
  • 改变作用域中this的指向可以改变箭头函数的this。
  • eg. function closure(){()=>{//code }},在此例中,我们通过改变封包环境closure.bind(another)(),来改变箭头函数this的指向。

测试题目

七、call 和 apply 的应用

7.1 合并数组

1
2
3
4
5
6
var a = [1,2,3]
var b = [4,5,6]
Array.prototype.push.apply(a, b)
// 简写方式
// [].push.apply(a,b)
console.log(a) // [1, 2, 3, 4, 5, 6]

注意:第二个数组的值不能太大,不同的引擎有不同的限制,JS核心限制为65535,超过限制会抛出异常或者丢失多余的参数。
解决方法:将参数数组切块后循环传入方法。

1
2
3
4
5
6
7
8
9
10
function concatOfArray(arr1, arr2) {
var QUANTUM = 32768;
for (var i = 0, len = arr2.length; i < len; i += QUANTUM) {
Array.prototype.push.apply(
arr1,
arr2.slice(i, Math.min(i + QUANTUM, len) )
);
}
return arr1;
}

7.2 获取数组中最大值和最小值

1
2
3
4
var numbers = [1,30,10,423,-123]
Math.max.apply(Math, numbers) // 423
Math.max.call(Math, ...numbers) // 423

7.3 验证是否为数组

1
2
3
4
function isArray(obj) {
return Object.prototype.toString.call(obj) === '[object Array]'
}
isArray([1,2,3]) // true

使用 Object.prototype.toString() 来检测。

7.4 类数组转数组

类数组特性:

  1. 具有指向对象元素的数字索引下标和 length 属性。
  2. 不具有 push/shift/forEach/indexOf 等数组方法。

类数组是一个对象,JS中的一种数据结构。比如 arguments 对象,DOM API 返回的节点列表。可以通过 [].slice.call 转换成真正的数组。

1
2
3
4
5
var arr = [].slice.call(arguments);

// ES6:
let arr = Array.from(arguments);
let arr = [...arguments];

八、测试题

CodePen - this 指向测试题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function Person (name) {
this.name = name;
this.show1 = function () {
console.log(this.name)
}
this.show2 = () => console.log(this.name)
this.show3 = function () {
return function () {
console.log(this.name)
}
}
this.show4 = function () {
return () => console.log(this.name)
}
}
// new 方法创建对象。
var personA = new Person('personA')
var personB = new Person('personB')

personA.show1()
personA.show1.call(personB)

personA.show2()
personA.show2.call(personB)

personA.show3()()
personA.show3().call(personB)
personA.show3.call(personB)()

personA.show4()()
personA.show4().call(personB)
personA.show4.call(personB)()

第二题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function Person (name) {
this.name = name;
this.show1 = function () {
console.log(this.name)
}
this.show2 = () => console.log(this.name)
this.show3 = function () {
return function () {
console.log(this.name)
}
}
this.show4 = function () {
return () => console.log(this.name)
}
}
// new 方法创建对象。
var personA = new Person('personA')
var personB = new Person('personB')

personA.show1()
personA.show1.call(personB)

// PersonA , 和第一题不同的原因是:new 方法会创建一个新对象,产生构造函数作用域。
// 然后是箭头函数绑定,this指向外层作用域,即personA函数作用域
personA.show2() // PersonA
personA.show2.call(personB) // PersonA


personA.show3()()
personA.show3().call(personB)
personA.show3.call(personB)()

personA.show4()()
personA.show4().call(personB)
personA.show4.call(personB)()

参考

木易杨前端进阶