0%

JavaScript进阶笔记4

8. JavaScript中的this

8.1 this 使用

当我们要创建一个构造函数的实例时,需要使用 new 操作符,函数执行完成后,函数图中的 this 就指向了这个实例,通过下面的这个实例可以访问看绑定在 this 上的属性。

1
2
3
4
5
6
function Person(name) {
this.name = name
}

const p = new Person('zhangsan')
console.log(p.name)

假如我们将 Person() 函数当成一个普通函数执行,其中 this 又指向谁呢?从哪个对象可以访问到定义的 name 属性的的值呢?

事实上,在 window 对象上,我们可以找到 name 属性的值,这表明函数体中的 this 指向了 window 对象。

1
2
3
4
5
6
function Person(name) {
this.name = name
}

Person('lisi')
console.log(window.name) // lisi

为什么会出现这种情况呢?其实 this 抓鬼概念并不是 JavaScript 所特有的,在 java c++等面向对象的语言中也存在 this 关键字,它们中的 this 概念很好理解,this 指向的就是当前类的实例对象,而在 JavaScript 中,this 的指向是随着宿主环境的变化而变化的,在不同的地方对呀,返回的坑是不同的结果。

在大多数场景中,随着函数的运行,就会产生一个 this 的值,这个 this 存储着调用该函数的对象的值,因此我们可以认为,在 JavaScript 中,this 指向的永远是函数的调用者

8.2 this 指向全局对象

当函数没有所属对象直接调用时, this 指向的就是全局对象

1
2
3
4
5
6
7
8
9
10
11
12
13
var value = 10									// 一定是 var,当let时,window.value 为 undefined
const obj = {
value: 100,
method: function() {
var foo = function() {
console.log(this.value) // 10
console.log(this) // window
}
foo()
return this.value // 100
}
}
console.log(obj.method())

首先我们定义一个全局的 value 属性为 10,相当于 window.value = 10。

然后定义一个 obj 对象,设置了 value 属性值为100, 然后设置 method 属性为一个函数,其method 属性中定义了一个函数表达式 foo ,并执行了 foo() 函数,最终返回 value 属性。

当我们调用 obj.method() 函数时, foo() 函数被执行,此时 foo() 函数执行时没有所属对象的,因此 this 会指向全局的 window 对象,在输出 this.value 时,实际输出 window.value,因此输出 “10”。

而 method() 函数的返回值是 this.value,method() 函数的调用体是 obj 对象,此时 this 就指向 obj 对象,而 obj.value = 100,因此调用 obj.method() 函数后会返回“100”。

8.2 this指向所属对象

当通过 new 操作符调用构造函数(生成对象)时,this 指向该实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 全局变量
var number = 10
function Person() {
// 覆写全局变量
number = 20
// 实例变量
this.number = 30
}

// 原型函数
Person.prototype.getNumber = function() {
return this.number
}
// 通过 new 操作符获取对象的实例
const p = new Person()
console.log(p.getNumber()) // 30

在上面的这段代码中,我们定义了全局变量 nember 和实例变量 number,通过 new 操作符生成 Person 对象的实例 p 后,在调用 getNumber() 操作时,其中的 this 就指向该实例 p, 而实例 p 在初始化的时候被赋予 number 的值是30,所以输出的结果是 30。

8.3 this重新绑定对象

通过call()函数、apply()函数和bind()函数可以改变函数的执行主体,如果函数中存在 this 关键字,则 this 也会指向 call() 函数、apply() 函数和 bind() 函数处理后的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 全局变量
var value = 10
const obj = {
value: 20
}
// 全局函数
const method = function() {
console.log(this.value)
}
method() // 10 如果第一行为 let value = 10,结果为 undefined
method.call(obj) // 20
method.apply(obj) // 20
var newMethod = method.bind(obj)
newMethod() // 20

8.4 闭包中的this

函数的 this 变量只能被自身访问,其内部函数无法访问,因此在遇到闭包时,闭包内部的 this 关键字无法访问到外部变量函数的 this 变量。建议用箭头函数代替普通函数。

参见: 7.2.3 作用域链问题

8.5 call apply和 bind 函数

call() 函数调用一个函数时,会将该函数的执行对象上下文改变为另一个对象,其语法如下:

1
function.call(thisArg, arg1, arg2)
  • function 为需要调用的函数
  • thisArg 表示的是新的对象上下文,函数中的 this 将指向 thisArg,如果 thisArg 为 null 或者 undefined,则 this 会指向全局对象。
  • arg1,arg2…表示函数所接受的参数列表。
1
2
3
4
5
6
7
8
9
10
// 定义一个函数
function add(x, y) {
return x + y
}
// 通过 call 函数进行 add() 调用
function myAddCall(x, y) {
// 调用 add() 函数的 call 函数
return add.call(this, x, y)
}
console.log(myAddCall(10, 20)) // 30

myAddCall() 函数本身是不具备运算能力,但是我们在 myAddCall 函数中,通过调用 add() 函数,并传入 this值,将执行 add() 函数主题改变为 myAddCall() 函数自身,然后传输参数 x, y 这就使得拥有 add() 函数计算求和的能力。

apply()函数的作用域与 call() 函数时一样,只是在传递参数的形式上存在差异,语法格式如下:

1
function.apply(thisArg, [arg1,arg2])
  • function 为需要调用的函数
  • thisArg 表示的是新的对象上下文,函数中的 this 将指向 thisArg,如果 thisArg 为 null 或者 undefined,则 this 会指向全局对象。
  • 和 call() 函数的区别就是,在传值的时候,传递的事一个数组对象,如果传递的不是数组就会报错。

要想实现和 call() 函数一样的效果:

1
2
3
4
5
6
7
8
9
10
// 定义一个 add() 函数
function add(x, y) {
return x + y
}
// 通过 call() 函数进行 add() 函数的调用
function myAddApply(x, y) {
// 调用 add() 函数的 apply 函数
return add.apply(this, [x, y])
}
console.log(myAddApply(10, 20)) // 30

bind()函数创建一个新的函数,在调用时设置 this 关键字为提供的值,在执行新函数时,将给定的参数列表作为原函数的参数序列,从前往后匹配,其语法格式如下:

1
function.bind(thisArg, arg1, arg2)

call() 函数参数相同

  • function 为需要调用的函数
  • thisArg 表示的是新的对象上下文,函数中的 this 将指向 thisArg,如果 thisArg 为 null 或者 undefined,则 this 会指向全局对象。
  • arg1,arg2…表示函数所接受的参数列表。

想要实现和 call() 函数一样的效果:

1
2
3
4
5
6
7
8
9
10
11
// 定义一个 add() 函数
function add(x, y) {
return x + y
}
// 通过 call() 函数进行 add() 函数的调用
function myAddBind(x, y) {
// 调用 add() 函数的 bind 函数
const bindfn = add.bind(this, x, y)
return bindfn()
}
console.log(myAddBind(10, 20)) // 30

三者的相同之处是:都会改变函数调用的执行主体,修改 this 的指向。

不同之处:

  1. 关于函数立即执行, call() 函数与 apply() 函数在执行后会立即调用前面的函数,而 bind() 函数不会立即调用,它会返回一个新的函数,可以在任何时候调用。
  2. 关于参数传递,第一个参数表示将要改变的函数执行主体,即 this 的指向。从第二个参数开始,call() 函数与 bind() 函数相同都是函数接收的参数,apply() 第二个参数接收一个数组。

8.6 call apply和 bind 函数巧妙用法

8.6.1 求数组的最大项和最小项目

Array 数组本身没有 max() 函数和 min() 函数,无法直接获取最大值和最小值,但是 Math 却有最大值和最小值的 max() 函数和 min() 函数,我们可以使用 apply() 函数来改变 Math.max() 函数的执行主体,然后将数组作为参数传递给 max() 和 min() 函数。

1
2
3
4
5
6
7
const arr = [3, 5, 7, 2, 9]
// 求数组中的最大值
const max = Math.max.apply(null, arr)
console.log(max)
// 求数组中的最小值
const min = Math.min.apply(null, arr)
console.log(min)

apply() 函数第一个参数为 null,这是因为没有对象去调用这个函数,我们只需要这个函数帮助我们运算,得到返回结果。arr 是一个数组所以只能用 apply() 函数。

8.6.2 类数组对象转换为数组对象

函数的参数对象 arguments 是一个类数组对象,自身不能直接调用数组的方法,但是我们可以借助 call() 函数,让 arguments 对象调用数组的 slice() 函数,从而得到一个真实的数组,后面就能调用数组的函数。

1
2
3
4
5
6
7
8
9
10
11
12
// 通用求和函数
function sum() {
// 通过 call() 函数间接调用到 slice() 函数,以得到函数参数的数组
const arr = Array.prototype.slice.call(arguments)
return arr.reduce(function(pre, cur) {
return pre + cur
}, 0)
}

console.log(sum(1, 2))
console.log(sum(1, 2, 3))
console.log(sum(1, 2, 3, 4))
8.6.3 bind函数配合setTimeOut

在默认情况下,使用 setTimeout() 函数的的时候, this 关键字会指向全局对象 window,当使用类的函数时,需要 this 引用类的实例,我们坑需要显示的把 this 绑定到回调函数一遍继续使用实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function LateBloomer() {
this.petalCount = Math.ceil(Math.random() * 12) + 1;
}

// 在 1 秒钟后声明 bloom
LateBloomer.prototype.bloom = function() {
window.setTimeout(this.declare.bind(this), 1000);
};

LateBloomer.prototype.declare = function() {
console.log('I am a beautiful flower with ' +
this.petalCount + ' petals!');
};

var flower = new LateBloomer();
flower.bloom(); // 一秒钟后, 调用 'declare' 方法
赞赏是最好的支持