搞清楚
this
这种玄学的东西的机制,作用一自然是应付面试官,作用二就是可以维护别人的烂代码啦~
1 前置知识
1.1 对this的一个大误解
很多人对this有一个潜意识里的误解——认为this的值取决于其所在函数是在哪里声明的
let obj = {
a: function () {
console.log(this);
},
b: function () {
let f = obj.a;
f();
}
}
obj.b(); // window
很多人在遇到上面这个面试题时,看到函数是在对象内部声明,都会误认为this指向obj
1.2 函数名即指针
函数名
就是指向函数对象的指针
this作为一个函数内部的对象,自然与函数紧密相连
然而,很多朋友都没搞清楚函数名竟然是个指针
看下面的代码,思考问题:
- fn1是否能够直接找到其函数体的位置(ans:能)
- fn1和obj还有关系吗(ans:无关系)
function fn() {
console.log(this);
}
let obj = {
a() {
return fn;
}
}
let fn1 = obj.a();
fn1(); // window
如果上面两个问题你能想明白,或许你对this能够指向window已经有一定的感觉
2 this机制详解
2.1 this本质上就是指向它的调用者
javascript
作为一门解释型语言,this的值到底是什么,必须到函数被调用时才能确定
。
什么意思呢,比如下面这段代码
function fn() {
console.log(this);
}
请问:你现在知道fn里的this是什么吗?
不可能知道的,因为fn没有被调用!
那么什么又叫做必须到函数被调用时才能确定呢?
我们来看看对上面的fn
进行不同的调用,结果是什么
function fn() {
console.log(this);
}
let obj = {fn};
fn(); // window
obj.fn(); // obj
可以很明显地发现,因为调用方式的不同,this的值也不同
那么
this本质上就是指向它的调用者
这句话该怎么理解呢?
先别急,整个文章都是围绕这句话展开的
首先,我们分析一下一个函数到底有几种调用方式,再分别进行阐述:
全局调用
方法调用
new调用
2.1 全局调用(独立调用)
只要遇到独立调用
,基本可以无脑推断该函数内部的this为windows
function foo() {
console.log(this);
}
foo(); // window
对于foo()
而言,foo
是函数名,而在js中,函数名只是一个指针
类似这种函数名()
形式,孤零零地单独出现,《你不知道的javascript》作者把这种方式称之为独立调用,而这种调用会使得被调用函数内部的this
默认绑定为window
结合this本质上就是指向它的调用者
这句话,全局调用的本质其实就是window
调用了foo
这个函数
foo();
// 等价于下面
window.foo();
2.2 方法调用
方法(method)
指的是某个对象的属性是一个函数,如obj = {fn:function(){}}
,而obj.fn()
叫做 方法调用结合
this本质上就是指向它的调用者
这句话:经过方法调用后,方法内的this指向拥有该方法的对象
let obj = {
fn() {
console.log(this);
}
}
obj.fn(); // obj
2.2.1 方法调用的就近原则
在多个对象嵌套的情况下,this指向调用它的距离最近的那一个对象
let obj = {
a: {
b: {
fn() {
console.log(this);
}
}
}
}
obj.a.b.fn(); // obj.a.b
2.2.2 和全局调用进行对比
下面这段代码,在不运行的情况下,很多人都会猜错
let obj = {
a() {
console.log(this);
}
}
let fn = obj.a;
obj.a(); // obj
fn(); // window
相信大家对obj.a();
没有疑问,关键在于fn()
为什么是window
其实结合1.2小节 函数名即指针
一起来看,fn只是一个指针,现在fn指向了obj.a这个函数,而fn()
是一个全局调用,因此this自然指向了window
2.3 new
关键在于记住
new做了哪些事情
:
- 创立一个临时对象,
this指向该临时对象
- 把实例的
__proto__
指向类的prototype- return临时对象
function fn() {
console.log(this);
}
new fn(); // 结果看下面的截图
3 其它场景下的this解惑
3.1 严格模式下的this
严格模式下只需要 注意一点就行,其它情况下与非严格模式相同
全局作用域里函数中的this是undefined
function test() {
"use strict"
console.log(this)
}
test() // undefined
所以,在使用构造函数时,如果忘了加new,this不再指向全局对象,而是报错
,因为这就是函数的全局调用
let People = function (name) {
"use strict"
this.name = name
}
People() // Cannot set property 'name' of undefined
3.2 数组中的this
function fn() {
console.log(this)
}
arr[fn, fn2, fn3]
arr[0]() // ??
// answer:arr
// 解析
// 数组也是对象的一种
// arr[0]() 可以看做 arr.0().call(arr)
3.3 嵌套函数里的this
要注意的是:不论
函数名()
这种形式出现在哪,都是
独立调用
// 例子一
function fn0() {
function fn() {
console.log(this);
}
fn();
}
fn0(); // fn中this是全局变量
// 例子二
let a = {
b: function () {
console.log(this) // {b:fn}
function xx() {
console.log(this) // window
}
xx()
}
}
a.b()
3.4 setTimeout、setInterval中的this
this指向全局变量
document.addEventListener('click', function (e) {
console.log(this);
setTimeout(function () {
console.log(this); // 这里的this是全局变量
}, 200);
}, false);
3.5 事件中的this
事件里的this指向的是 触发事件的DOM节点
document.querySelector('div').addEventListener('click',function (e) {
console.log(this) //
})
4 自己指定this
我个人认为,对待this,应该尽量使用
call/apply/bind
去强制绑定,这样才是上策
4.1 call/apply和bind概览
- 我们要将
call/apply
归为一类,bind
单独归为一类 - 三者的共同点是都可以指定this
- call/apply和bind都是绑定在Function的原型上的,所以Function的实例都可以调用这三个方法
Function.prototype.call(this,arg1,arg2)
Function.prototype.apply(this,[arg1,arg2])
Function.prototype.bind(this,arg1,arg2)
4.2 call/apply —— 第一个参数是this
4.2.1 call/apply的作用
call
和apply
只有一个区别:call()方法接受的是 若干个参数,apply()方法接受的是一个 包含若干个参数的数组
作用:
- 调用函数
- 改变该函数的this指向
- 给函数传递参数
返回值
返回值是你调用的函数的返回值
window.a = 1
function print(b, c) {
console.log(this.a, b, c)
}
// 独立调用
print(2, 3) // 1 2 3
// 使用call和apply
print.call({a: -1}, -2, -3) // -1 -2 -3
print.apply({a: 0}, [-2, -3]) // 0 -2 -3
4.2.2 apply传递数组参数
虽然apply
传递的参数为数组,但实际上apply会将这个数组拆开再传递
,因此函数接受到的参数是 数组内的元素,而非一个数组
let fn = function () {
console.log(arguments)
}
fn.apply(null, [1, 2, [3, 4]]);
因此,apply经常性的作用之一就是
将数组元素迭代为函数参数
例子一
Math.max()不接收数组的传递,因此如果想要找到一个长度很长的数组的最大值会非常麻烦
我们可以使用apply方法将数组传递给Math.max()
其本质还是将参数数组拆开再传递给Math.max()
let answer = Math.max.apply(null, [2, 4, 3])
console.log(answer) // 4
// 注意下面三个等价
Math.max.apply(null, [2, 4, 3])
Math.max.call(null, 2, 4, 3)
Math.max(2, 4, 3)
例子二:合并两个数组
非常值得注意的就是arr2数组被拆开了,成了一个一个的参数
// 将第二个数组融合进第一个数组
// 相当于 arr1.push('celery', 'beetroot');
let arr1 = ['parsnip', 'potato']
let arr2 = ['celery', 'beetroot']
arr1.push.apply(arr1, arr2)
// 注意!!!this的意思是要指定调用了push这个方法
// 所以当 this = arr1 后
// 就成了 arr1 调用了 push方法
// 上述表达式等价于 arr1.push('celery', 'beetroot')
console.log(arr1)
// ['parsnip', 'potato', 'celery', 'beetroot']
当然,在使用apply的时候, 也一定要注意是否要对this的指向进行绑定,否则可能会报错
Math.max.apply(null, [2, 4, 3]) // 完美运行
arr1.push.apply(null, arr2) // 报错 Uncaught TypeError: Array.prototype.push called on null or undefined
// 说明
Math = {
max: function (values) {
// 没用到this值
}
}
Array.prototype.push = function (items) {
// this -> 调用push方法的数组本身
// this为null的话,就是向null里push,会报错
// Array.prototype.push called on null or undefined
}
// 下面三个值是完全等价的,因为this值已经是arr1
Array.prototype.push.apply(arr1, arr2)
arr1.push.apply(arr1, arr2)
arr2.push.apply(arr1, arr2)
4.2.3 小测试
function xx() {
console.log(this)
}
xx.call('1') // ??
xx() // ??
4.3 bind
fun.bind(thisArg[, arg1[, arg2[, ...]]])
4.3.1 作用
- 改变该函数的this指向
- 返回一个新函数
4.3.2 绑定函数与目标函数
函数使用bind()方法后会返回一个新函数【绑定函数】
而原函数为【目标函数】
我个人更喜欢用 新函数和 原函数来区分,因为新名词越多,理解上的困难越大
那么新函数被调用时会发生什么呢?
下面一句话务必记住其实就是把原函数call/apply一下,并指定你传递的this
function xx() {
console.log(this)
}
let foo = xx.bind({'name':'jason'})
// foo —— 新函数【绑定函数】
// xx —— 原函数【目标函数】
foo()
// 新函数调用时对原函数的操作如下(伪代码)
function foo(){
xx.call({'name':'jason'})
}
也就是说,实际上,在foo()
这一句执行时做了如下事情:
1.给xx(原函数)指定this
2.调用xx(原函数)
一定要注意这两步是在新函数被调用时才发生,不调用不发生
你也可以总结为一句话:给原函数 call/apply 了一下
4.3.3 bind()传参
- bind(this,arg1,arg2...)会将
arg1,arg2...
插入到新函数【绑定函数】的arguments的开始位置
- 调用新函数时,再传递的参数也只会跟在
arg1,arg2...
后面
function list() {
// 原函数【目标函数】
return Array.prototype.slice.call(arguments);
}
// 新函数【绑定函数】
let leadingThirtysevenList = list.bind(null, 37, 38);
let newList1 = leadingThirtysevenList();
let newList2 = leadingThirtysevenList(1, 2, 3);
let newList3 = leadingThirtysevenList(-1, -2);
console.log(newList1) // [ 37, 38 ]
console.log(newList2) // [ 37, 38, 1, 2, 3 ]
console.log(newList3) // [ 37, 38, -1, -2 ]
4.3.4 使用 this + call/apply原生实现一个bind【重点】
思考过程
实现bind其实就是实现bind的特点
- bind的第一个参数是this
- bind可以return一个新函数,这个新函数可以调用原函数并且可以指定其this,还可以接受参数
- 新函数传递的参数要在bind传递的参数的后面
代码
Function.prototype._bind = function () {
// bind指定的this
let bindThis = arguments[0]
// bind传递的参数
let bindArgs = Array.prototype.slice.call(arguments, 1)
// this指向调用_bind的原函数,即旧函数
let oldFunction = this
// 返回的新函数
return function () {
// 所谓的新函数和旧函数,函数体一样,实际上只是用apply调用了旧函数,再return旧函数的返回值
// 截获调用新函数时传递的参数
let newArgs = Array.prototype.slice.call(arguments)
// 合并bindArgs和newArgs,并且newArgs要在bindArgs后面,再传递给旧函数
return oldFunction.apply(bindThis, bindArgs.concat(newArgs))
}
}
// 测试
function fn() {
console.log(arguments)
}
let newFn = fn._bind(null, 1, 2);
newFn(4, 6)