努力说清this的指向和怎么改变this的指向
JS 中的 this,总是神神叨叨的,不小心就错了。
希望我自己写完本文之后,以后也按着现在捋顺的逻辑来分析 this。
TL;DR
- 箭头函数的
this
,和它书写的位置密切相关,在书写阶段(即声明位置)就绑定到它父作用域的this
- 构造函数的
this
,会绑定到我们new
出来的这个对象上 - 在不使用
call/apply/bind
改变this
指向的时候,普通函数的this
,只在调用的时候,绑定到调用方,和它的位置没有关系
- 立即执行函数、setTimeout、setInterval 内部是普通函数的时候,因为其调用方是
window
,所以this
是window
- call/apply/bind 均可改变 this 指向,且均可被手写实现
箭头函数里的this
箭头函数的this
,和它书写的位置密切相关,在书写阶段(即声明位置)就绑定到它父作用域的 this
。
因为其由父作用域决定,所以父作用域至关重要。
- 若箭头函数的父作用域是全局作用域,则 this 始终指向
window
- 若箭头函数的父作用域是函数作用域,则指向函数作用域的
this
var a = 1; var obj = { a: 2, func2: () => { // 父作用域是全局,this始终都是window console.log(this.a); }, func3: function() { // 等同于func3作用域的this () => { console.log(this.a); }; } }; // func1 var func1 = () => { // 父作用域是全局,this任何时候都是window console.log(this.a); }; // func2 var func2 = obj.func2; // func3 var func3 = obj.func3; func1(); func2(); func3(); obj.func2(); obj.func3();
构造函数里的 this
构造函数里面的 this 会绑定到我们 new 出来的这个对象上:
function Person(name) { this.name = name; console.log(this); } // this是person var person = new Person("yan");
function 定义的普通函数
function 定义的普通函数,this 的指向是在调用时决定的,而不是在书写时决定的。这点和闭包恰恰相反。
换言之:不管方法被书写在哪个位置,它的 this 只会跟着它的调用方走, 再大白话点:xx.fn()
,点前面是谁,fn 里的 this 就是谁。没有点就是 window。
注意上面的前提是:不使用
call/apply/bind
改变this
指向的时候。
必须严格区分 “声明位置” 与 “调用位置”!!!
上面的秘诀掌握好了,下面的例子就是 easy 了
// 声明位置 var me = { name: "yan", hello: function() { console.log(`你好,我是${this.name}`); } }; var you = { name: "xiaoming", hello: function() { var targetFunc = me.hello; targetFunc(); } }; var name = "BigBear"; // 调用位置 you.hello();
还有 2 种特殊点的情景: 没有调用方,所以 this 始终都是window
- 立即执行函数(IIFE),
(function(){})()
- setTimeout/setInterval 中传入的普通函数,
setTimeout(function(){...},1000)
!!!注意,必须是 function 写的普通函数,如果箭头函数,仍遵循箭头函数的规则
先看个立即执行函数的例子,
var name = "BigBear"; var obj = { name: "yan", fn: function() { (function() { console.log(this.name); })(); } }; obj.fn();
仔细看看就是自执行函数执行,this 肯定是 window,自然就是BigBear
。
注意!!!换成箭头函数的话,规则就变了,因为父作用域是 fn,其 this 绑定成了 obj,所以打印的话是yan
var obj = { fn: function() { (() => { console.log(this); })(); } }; obj.fn();
再看下 setTimeout
var name = "BigBear"; var me = { name: "yan", hello: function() { setTimeout(function() { console.log(`你好,我是${this.name}`); }); } }; me.hello();
setTimeout 里面的函数,this 指向window
,自然值是BigBear
。 同理,如果将 setTimeout 里面的函数修改成箭头函数,则打印yan
了。
var name = "BigBear"; var me = { name: "yan", hello: function() { setTimeout(() => { console.log(`你好,我是${this.name}`); }); } }; me.hello();
严格模式下的 this
笔者不用严格模式,真要用的时候百度下吧。
改变 this 的指向
this 的指向,要么被书写位置限制,要么被调用位置限制,很是被动。
- 对于箭头函数,因为其
this
只和书写位置有关,所以一般不修改箭头函数的 this 指向。 - 构建函数,this 就是 new 出来的实例,所以一般不修改构建函数的 this 指向
- 于是重点!!!对于 function 定义的普通函数,修改 this,必须要显示的调用
call/apply/bind
。
一般问的修改 this,也是指 function 定义的普通函数。
call/apply/bind
三者用法及区别:
- call/apply 改变函数的 this,且函数立即执行。但 apply 的参数是数组,call 的参数是非数组
- bind 改变函数的 this,但函数未执行
var init = 0; function add(num1, num2) { console.log(this.init + num1 + num2); } // 普通执行,肯定输出3 add(1, 2); var obj = { init: 100 }; // 此时因为this变成obj,所以输出103 add.apply(obj, [1, 2]); // 此时因为this变成obj,所以输出103 add.call(obj, 1, 2); // 此时因为this变成obj,但需要调用一次函数才行,bind本身返回的是函数 add.bind(obj, 1)(2);
手写实现 call/apply/bind
其实细看下 call,发现 call 有以下特征:
- call 是函数的方法,可以被函数直接调用
- call 第一个参数是 this 绑定的对象,后面的参数是函数的参数
- call 调用之后,函数执行,但 this 绑定到第一个参数上
推理下 call 怎么实现:
- 是函数的方法,每个函数都可调用,可写在 Function.prototype 上
- 参数区别第一个参数,和后面的参数
- call 内,this 绑定到第一个参数上,函数执行
其实想想普通函数 this 的指向只和调用方有关
=> 于是 obj.fn()
=> 但是 fn 并不是 obj 的方法
=> 于是给 obj 增加 fn 这个方法不就行了
=> 但是 call 执行完,必须再将 fn 这个方法删掉就好了
Function.prototype.myCall = function(context, ...args) { if (context === null) { return fn(...args); } // 注意 这里的this就是调用call的函数 const fn = this; // 先增加 context.fn = fn; // 执行 return fn(...args); // 在删除 delete context.fn; }; // 测试下,没问题,输出103 add.myCall(obj, 1, 2);
同理:
// 注意因为参数args就是数组,所以不要加... Function.prototype.myApply = function(context, args) { if (context === null) { return fn(...args); } // 注意 这里的this就是调用myApply的函数 const fn = this; context.fn = fn; return fn(...args); delete context.fn; }; // 同理输出103 add.myApply(obj, [1, 2]);
Bind 稍微复杂点:
- bind 是函数的方法,可以被函数直接调用
- bind 返回一个原函数的拷贝,但 this 被指定
- bind 也可以传原函数的部分参数
其实 bind 就是返回一个函数,函数内部执行 call~
Function.prototype.myBind = function(context, ...frontArgs) { const fn = this; return function(...behindArgs) { return fn.call(context, ...frontArgs, ...behindArgs); }; }; // this被指定,但是需要调用一次才能执行函数,输出103 add.myBind(obj, 1)(2);