虽然this指向有一句广为流传的话”谁调用就指向谁“,但却并不全面,在复杂的使用场景中,this指向容易被混淆,带出隐藏bug。this指向一直是一个基础但是最重要的概念。
本文是侯策大大《前端开发基础知识进阶》一文的读书笔记整理 + 补充了部分知识点。
PS: 侯策大大的书写的非常好,读完后对整个this指向的理解清晰多了,读侯策大大的书,有一种融会贯通的感觉,也推荐大家阅读。
死记硬背规律
-
在函数体中,非显式或隐式地(通过obj.fn()对象上方法这种方式叫做隐式绑定)简单调用函数时,在严格模式下,函数内的 this 会被绑定到undefined 上,在非严格模式下则会被绑定到全局对象 window/global 上。
-
隐式绑定:一般通过上下文对象调用函数时,函数体内的 this 会被绑定到该对象上。
注意: 当以变量方式取出对象方法,再去调用的时候,调用的时候是没有绑定对象的,隐式绑定丢失,这种情况下this指向参考1
-
显示绑定:一般通过 call/apply/bind 方法显式调用函数时,函数体内的 this 会被绑定到指定参数的对象上。
-
一般使用 new 方法调用构造函数时,构造函数内的 this 会被绑定到新创建的对象上。
-
在箭头函数中,this 的指向是由外层(函数或全局)作用域来决定的。 箭头函数的this指向在定义时就确定了,没法改变,new、显示绑定也不能改变
当然,真实环境多种多样,下面就根据具体环境来逐一梳理。
一. 全局环境中的this
例子1
// ex1
function f1(){
console.log(this)
}
function f2(){
'use strict'
console.log(this)
}
f1() // window
f2() //undefiened
容易混淆的例子2
let foo = {
'name': 'haha',
bar: function(){
console.log(this)
console.log(this.name)
}
}
let fn = foo.bar
fn()
输出结果与分析
fn()
// window
// undefiened
这里this指向了window,因为bar虽然是对象的方法,但在复制给变量fn后。fn的执行是在window的全局环境中执行的,在非严格模式下,this指向window.
例子2 变种
let foo = {
'name': 'haha',
bar: function(){
console.log(this)
console.log(this.name)
}
}
foo.bar()
输出结果与分析
foo.bar()
// foo
// 'haha'
this指向最后调用它的对象。在foo.fn(),fn被foo调用,this指向了foo对象。
二. 上下文对象对用中的this
案例
const o1 ={
text:'ol'
fn: function(){
return this.text
}
}
const o2 ={
text:'o2'
fn: function(){
return o1.fn()
}
}
const o3 ={
text:'o3'
fn: function(){
var fn = o1.fn
return fn()
}
}
o1.fn()
o2.fn()
o3.fn()
输出结果与分析
o1.fn() // 'o1'
o2.fn() // 'o1'
o3.fn() // 'undefiened'
第2条,因为fn被调用的时候,调用的对象显示的绑定了o1.所以输出了 'o1' 第3条,先把o1.fn复制给fn。然后fn被调用,被调用时fn没有显示绑定,所以this指向全局对象。
案例进阶
如果希望让 console.log(o2.fn())语句输出o2, how to do ?
-
- bind、call、apply 改变this指向
-
- 如果不用bind、call、apply呢? => 简单改写函数
const o2 = {
text: 'o2',
fn: o1.fn
}
console.log(o2.fn())
可以通过提前进行复制操作,将函数fn挂在到o2对象上,fn最终作为o2对象的方法被调用。
三. 通过bind、apply、call改变this指向
1. 区别
call => 直接调用函数,接受的参数是 (context,接受多个参数作为调用函数的入参) apply => 直接调用函数,接受的参数是 (context,参数数组) bind => 不执行,返回新函数,新函数的this绑定传入的context (context,接受多个参数作为调用函数的入参)。仍然可以接受参数列表,在新函数被调用时会传入
代码调用示例,把fn的this指向target对象
const target = {}
// call
fn.call( target, 'arg1','arg2')
// apply
fn.apply (target, ['arg1','arg2'])
// bind
fn.bind( target, 'arg1','arg2')
这里this的指向也很容易明白,不过三、四章内容一般在知识体系上考察的重点在于明白这几个方法的原理,如何手写实现,这部分内容可参见手写相关方法
四. 构造函数(new)和this
1. 简易demo
function Foo(){
this.bar = "hhh"
}
const instance = new Foo()
console.log(instance.bar)
结果:
console.log(instance.bar) // 'hhh'
2.进阶
场景1:
function Foo(){
this.bar = "hhh"
const o = {}
return o
}
const instance = new Foo()
console.log(instance.bar)
场景2:
function Foo(){
this.bar = "hhh"
return 'haha'
}
const instance = new Foo()
console.log(instance.bar)
结果与分析
// 场景1输出 'undefiened'
// 场景2输出 ‘hhh’
这里场景1输出'undefiened'、场景2输出‘hhh’。是因为1中new Foo()返回的是一个空对象,2中返回的是目标对象实例。
这里涉及到构造new实现的原理: 如果构造函数中显式返回一个值,且返回的是一个对象 (返回复杂类型),那么 this 就指向这个返回的对象:如果返回的不是一个对象(返回基本类型),那么 this 仍然指向实例。更进一步的代理讲解可查看下面章节手写相关方法
五. 箭头函数
const foo = {
fn: function(){
setTimeout(function(){
console.log(this)
})
}
}
console.log(foo.fn())
分析: this是在setTimeout中以匿名函数中,同时调用的时候是没有绑定上下文的,所以输出 window
const foo = {
fn: function(){
setTimeout(()=> {
console.log(this)
})
}
}
分析: this是在箭头函数中,this指向定义时的环境,即fn这个局部作用域。
箭头函数的this比较简单易懂,但是其需要注意的是,结合this优秀级,在复杂情况下判断this指向。
六. this优先级
- 显示绑定 > 隐式绑定 这个很容易理解
- new绑定 > 显示绑定
- 箭头函数this > 其他,不能被修改
重点举例子分析2、3点。
案例1
function foo(a){
this.a = a
}
const obj1 = {}
var bar = foo.bind(obj1)
bar(2)
console.log(obj1.a)
var baz = new bar(3)
console.log(baz.a)
结果与分析
console.log(obj1.a) // 2
console.log(obj1.a) // 3 => new的优先级高于bind,修改了this的指向
案例2
function foo(){
return a => {
console.log(this.a)
}
}
const obj1 = {
a: 2
}
const obj2 = {
a: 3
}
const bar = foo.call(obj1)
console.log(bar.call(obj2))
结果与分析
console.log(bar.call(obj2)) // 输出2
// 1. foo.call(obj1) 把foo的this指向了obj1,同时返回了this指向为obj1的bar(引用箭头函数)。bar中的箭头函数的指向是bar (修改的是箭头函数所在的环境的this指向)
// 2. bar.call(obj2) 修改箭头函数this指向,不生效
进阶,把a改成用const声明
const a = 123
const foo = () => a => {
console.log(this.a)
}
const obj1 = {
a: 2
}
const obj2 = {
a: 3
}
const bar = foo.call(obj1)
console.log(bar.call(obj2))
结果与分析
console.log(bar.call(obj2)) // 输出undefiened
答案为undefiened,因为const声明的变量不会挂载到window全局对象上。
七. 手写相关方法
call、apply、bind核心思路: 将函数挂载到指定对象的属性上, 然后再通过对应的对象调用函数
1. call
Function.prototype.myCall = function (context, ...args) {
if (typeof this !== 'function') {
throw new Error("this must be a function");
}
context = context || window || global;
// 使用symbol,防止属性名污染
const fn = Symbol();
// call 调用模式为函数自己调用call, 其中this 指向含函数本身
context[fn] = this;
const result = context[fn](...args);
delete context.fn;
return result;
}
2. apply
Function.prototype.myApply = function (context, args) {
if (typeof this !== 'function') {
throw new Error("this must be a function");
}
context = context || window || global;
const fn = Symbol();
context[fn] = this;
const result = context[fn](...args);
delete context.fn;
return result;
}
3. bind
bind要区分两种场景
- 被当做构造函数,通过new操作符调用
=> 通过New操作符调用,不绑定传入的this,而是把this指向实例化出来的对象
- 作为普通函数调用
=> 作为普通函数调用,直接改变this指向即可。
/** 返回绑定this的函数 */
Function.prototype.myBind = function(context, ...args) {
if (typeof this !== 'function') {
throw new Error("this must be a function");
}
context = context || window || global;
const fn = Symbol();
const _this = this;
// 第一种情况 :若是将 bind 绑定之后的函数当作构造函数,
// 通过 new 操作符使用,则不绑定传入的 this,而是将 this 指向实例化出来的对象
// 此时由于new操作符作用 this指向result实例对象 而result又继承自传入的_this 根据原型链知识可得出以下结论
function newFunction (...innerArgs) {
// 第一种情况newFunction 被new 调用,即被作为构造函数被调用
if (this instanceof _this) {
// 也可以通过 new.target 来判断, 在普通函数中new.target 为null 展示
this[fn] = _this;
const result = this[fn](...[...args, ...innerArgs]);
delete this[fn];
return result;
} else {
context[fn] = _this;
// 如果只是作为普通函数调用 那就很简单了 直接改变this指向为传入的context
const result = context[fn](...[...args, ...innerArgs]);
// delete context[fn];
return result;
}
}
/** bind 函数绑定原函数的原型 */
newFunction.prototype = Object.create(this.prototype);
return newFunction;
}
4. new
new的两个核心:
- new + 构造函数得到的实例可以实现对构造函数this属性的继承
- new + 构造函数得到的实例可以访问到构造函数的原型
new的原理:
- 创建一个空对象
- 将空对象的原型对象指向构造函数的原型属性,从而继承原型的方法
- 将构造函数的this指向绑定到空对象上执行,以获取私有属性
- 如果构造函数返回了一个对象,就直接返回这个对象,如果返回的不是对象,就把创建的对象返回
function myNew(Func,...args){
let obj = {};
// ES6写法,建议不适用__proto__,使用setPrototypeOf
Object.setPrototypeOf(obj,Func.prototype)
let res = Func.call(obj,...args)
return typeof res === 'object' ? res : obj;
}
参考文章
<<前端开发基础知识进阶>>第一章