第二章 this、call 和 apply
本章主要理解this关键字、Function.prototype.call和Function.prototype.apply的概念
2.1 this
JavaScript 中的 this 总是指向一个对象,具体指向哪个对象则是在运行时基于函数的运行环境来动态绑定的,而不是函数声明时的环境(词法作用域则是根据函数声明时的环境生成的)。
2.1.1 this 的指向
this 的指向大概可以分为以下 4 种:
- 作为对象的方法调用
- 作为普通函数调用
- 作为构造函数调用
Function.prototype.call或者Function.prototype.apply调用
- 当作为对象的方法调用时,this 指向该对象
js1var obj = { 2 a: 1, 3 getA: function () { 4 alert(this === obj) // 输出:true 5 alert(this.a) // 输出: 1 6 }, 7} 8 9obj.getA()
- 当作为普通函数调用时,指向全局对象,浏览器环境下的全局对象是 window
js1window.name = 'globalName' 2 3var getName = function () { 4 return this.name 5} 6 7console.log(getName()) // 输出:globalName
或者
js1window.name = 'globalName' 2 3var myObject = { 4 name: 'sven', 5 getName: function () { 6 return this.name 7 }, 8} 9 10var getName = myObject.getName 11console.log(getName()) // globalName
有时候我们需要在事件函数内部使用一个 callback 方法,这个方法内可能需要用到 this,我们希望它指向触发点击事件的节点,可以这么做
html1<div id="div">我是一个div</div>
js1div.addEventListener('click', function () { 2 const that = this // 使用一个变量保存this 3 function callback() { 4 console.log(this.id) //undefined 5 console.log(that.id) // div 6 } 7 callback() 8})
否则callback就是普通函数调用,默认 this 指向 window。
在严格模式下,普通函数调用规定不会指向 window,而是 undefined。
js1function callback() { 2 'use strict' 3 console.log(this) //undefined 4}
- 构造器调用
构造器跟普通函数没有区别,只是我们调用它的方式不同。当使用 new 运算符调用时,该函数会返回一个对象。默认情况下,this 会指向返回的这个对象。
js1var MyClass = function () { 2 this.name = 'sven' 3} 4 5var obj = new MyClass() // new会新创建一个对象obj,并且让this指向它。相当于obj.name ='sven' 6alert(obj.name) // 输出:sven
当使用 new 关键字时,还需要注意一个问题,如果构造器显式返回一个 object 类型的对象,那么最终会返回这个对象。而不是 this 原指向的对象。
js1var MyClass = function () { 2 this.name = 'sven' 3 return { 4 // 显式地返回一个对象 5 name: 'anne', 6 } 7} 8 9var obj = new MyClass() // 由new关键字创建的并拥有this指向的obj不会被返回了 10alert(obj.name) // 输出:anne
如果构造器不显式地返回任何数据或者不返回对象类型的数据,那么就不会有问题
js1var MyClass = function () { 2 this.name = 'sven' 3 return 'anne' // 返回string类型 4} 5 6var obj = new MyClass() 7alert(obj.name) // 输出:sven
- Function.prototype.call 或 Function.prototype.apply 调用
使用 Function.prototype.call 或 Function.prototype.apply 可以动态地改变传入函数的 this
js1var obj1 = { 2 name: 'sven', 3 getName: function () { 4 return this.name 5 }, 6} 7 8var obj2 = { 9 name: 'anne', 10} 11 12console.log(obj1.getName()) // 输出: sven 13console.log(obj1.getName.call(obj2)) // 输出:anne
2.1.2 丢失的 this
js1var obj = { 2 myName: 'sven', 3 getName: function () { 4 return this.myName 5 }, 6} 7 8console.log(obj.getName()) // 输出:'sven' 9 10var getName2 = obj.getName 11console.log(getName2()) // 输出:undefined
当调用 obj.getName 时,getName 方法是作为 obj 对象的属性被调用的,此时的 this 指向 obj 对象。
当用另外一个变量 getName2 来引用 obj.getName,此时是普通函数调用方式,this 是指向全局 window 的,所以程序的执行结果是 undefined。
再来看一个例子,比如我希望封装一个函数来获取 ID 以替代document.getElementByid()的写法,我封装的函数是这样的:
js1function getId(id) { 2 return document.getElementById(id) 3} 4 5getId('div')
这种方式没问题。
如果换成这样不是更简单吗?
js1const getId = document.getElementById 2getId('div')
当运行一下,就会发现这样的代码会抛出异常。
原因是document.getElementByid内部使用了 this。
当getElementById方法作为document对象的属性被调用时,方法内部的this确实是指向document的
但当用getId来引用document.getElementById之后,再调用getId,此时就成了普通函数调用,函数内部的this指向了window,而不是原来的document。
我们可以尝试使用call或者apply或者bind来将 this 绑定到document上,这样就可以运行getId了
js1const getId = document.getElementById.bind(document) 2getId('div') 3console.log(getId('div').id) // 'div'
2.2 call 和 apply
Function.prototype中有两个方法,它们分别是call和apply,在函数式风格的代码中,它们尤为有用。
2.2.1 call 和 apply 的区别
call 和 apply 的作用一模一样,区别只在于传参形式的不同。
apply 接受两个参数,第一个参数指定函数体内的 this 指向,第二个参数是一个数组,也可以是类数组。apply 把第二个参数传递给被调用的函数。
js1var func = function (a, b, c) { 2 console.log(a, b, c) 3} 4func.apply(null, [1, 2, 3]) // 1 2 3
call是包装在apply上面的语法糖,第一个参数也指定函数体内的 this 指向,从第二个参数开始,所有参数都会被传递给被调用的函数。
js1var func = function (a, b, c) { 2 console.log(a, b, c) 3} 4func.call(null, 1, 2, 3) // 1 2 3
当call和apply的第一个参数是 null 时,函数内的this默认指向window。
js1var func = function (a, b, c) { 2 alert(this === window) // 输出true 3} 4 5func.apply(null, [1, 2, 3])
如果是严格模式,那么this指向null
js1var func = function (a, b, c) { 2 'use strict' 3 alert(this === null) // 输出true 4} 5 6func.apply(null, [1, 2, 3])
有时候我们使用call或者apply的目的是借用其他对象的方法,而不是指定this的指向那么我们可以传递null来替代某个具体的对象
js1Math.max.apply(null, [1, 2, 3, 4, 5]) // 5
2.2.2 call 和 apply 的用途
-
改变
this指向call和apply最常见的用途就是改变函数内部的this指向。
-
模拟
Function.prototype.bindjs1Function.prototype.myBind = function (context, ...rest) { 2 const thisFuc = this // 这里的this为调用bind的函数 3 return function () { 4 //调用call时让函数内部的this指向context 5 return thisFuc.call(context, ...rest) 6 } 7} 8const getName = function () { 9 console.log(this.name) 10} 11const obj = { 12 name: 'qiuyanxi', 13} 14getName.myBind(obj)() -
借用其他对象的方法
-
通过借用构造函数的方法,可以实现继承的效果
js1var A = function (name) { 2 this.name = name 3} 4 5var B = function () { 6 A.apply(this, arguments) 7} 8 9B.prototype.getName = function () { 10 return this.name 11} 12 13var b = new B('sven') 14console.log(b.getName()) // 输出: 'sven' -
通过借用其他对象的方法,可以实现某些功能
函数的
arguments是一个伪数组,它并没有数组的原型方法,除了使用Array.from将其变成真正的数组外,还可以使用call或者apply来借用数组的方法js1function fn() { 2 Array.prototype.push.call(arguments, 3) 3 console.log(arguments) // [1,2,3] 4} 5fn(1, 2)能够使用这种方法让 arguments 具备数组的能力的原因来自于 V8 的引擎源码:
js1function ArrayPush() { 2 var n = TO_UINT32(this.length); // 被push的对象的length 3 var m = %_ArgumentsLength(); // push的参数个数 4 for (var i = 0; i < m; i++) { 5 this[i + n] = %_Arguments(i); // 复制元素 (1) 6 } 7 this.length = n + m; // 修正length属性的值 (2) 8 return this.length; 9}结合例子分析一下源码:
- 先获取被 push 对象的长度,对应例子中的
arguments为被 push 的对象,它的长度为 2 - 获取 push 的参数个数,对应例子中的参数 3
- 循环遍历被 push 的参数个数,然后直接在
arguments的末尾位置挨个插入被 push 的值,%_Arguments用来获取 V8 源码中的参数对象,%_Arguments( i )获取到的是 3。 this[ i + n ] = %_Arguments( i )就相当于运行arguments[2]=3- 返回
length属性
从源码可以看出,V8 源码不在乎调用
push方法的是否为真实的数组,它只需要能够获取调用方法的对象的length属性,然后按照下标依次添加到被push对象上面,顺便修改掉length属性就可以了。通过
ArrayPush函数,我们可以知道想要实现Array.prototype.push.call类似的效果,需要具备两个条件:-
被
push对象属性可修改 -
被
push对象属性具有可读写的length属性
验证一下:
js1const obj = { 2 length: 0, 3} 4const n = 1 5 6function fn(a) { 7 console.log(a) 8} 9 10Array.prototype.push.call(obj, 1) 11console.log(obj) // {0:1,length:1} 12 13Array.prototype.push.call(n, 3) 14console.log(n) // 1 15 16Array.prototype.push.call(fn, 3) //"TypeError: Cannot assign to read only property 'length' of functionobj对象属性可存取,所以 push 成功了n不是对象,属性不可存取,所以 push 不成功fn函数的length是形参,只读属性,不可修改,所以报错了 - 先获取被 push 对象的长度,对应例子中的
-