JS中的this原理(一)箭头函数 or bind

446 阅读2分钟

箭头函数没有 this

let bottle = {
  nickname: "bottle",
  sayHi(){
    setTimeout(function(){
      console.log('Hello, ', this.nickname)
    }, 1000)
    
    // 或箭头函数
    setTimeout(() => {
      console.log('Hi, ', this.nickname)
    }, 1000)
  }
};

bottle.sayHi()
// Hello,  undefined
// Hi,  bottle
复制代码

报错是因为 Hello, undefined 是因为运行时 this=WindowWindow.nicknameundefined

但箭头函数就没事,因为箭头函数没有 this。在外部上下文中,this 的查找与普通变量搜索完全相同。this 指向定义时的环境。

箭头函数 vs bind

箭头函数 => 和正常函数通过 .bind(this) 调用有一个微妙的区别:

  • .bind(this) 创建该函数的 “绑定版本”。
  • 箭头函数 => 不会创建任何绑定。该函数根本没有 this。在外部上下文中,this 的查找与普通变量搜索完全相同。

获取this指向实例

JavaScript 新手经常犯的一个错误是将一个方法从对象中拿出来,然后再调用,希望方法中的 this 是原来的对象(比如在回调中传入这个方法)。如果不做特殊处理的话,一般 this 就丢失了。

例如:

let bottle = {
  nickname: "bottle",
  sayHello() {
    console.log(`Hello, ${this.nickname}!`)
  },
  sayHi(){
    setTimeout(function(){
      console.log('Hello, ', this.nickname)
    }, 1000)
  }
};

// 问题一
bottle.sayHi();
// Hello, undefined!

// 问题二
setTimeout(bottle.sayHello, 1000); 
// Hello, undefined!

问题一的 this.nickname 是 undefined ,原因是 this 指向是在运行函数时确定的,而不是定义函数时候确定的,再因为 sayHi 中 setTimeout 在全局环境下执行,所以 this 指向 setTimeout 的上下文:window。

问题二的 this.nickname 是 undefined ,是因为 setTimeout 仅仅只是获取函数 bottle.sayHello 作为 setTimeout 回调函数,this 和 bottle 对象分离了。

问题二可以写为:

// 在这种情况下,this 指向全局作用域
let func = bottle.sayHello;
setTimeout(func, 1000); 
// 用户上下文丢失
// 浏览器上,访问的实际上是 Window 上下文

那么怎么解决这两个问题喃?

解决方案一: 缓存 this 与包装

首先通过缓存 this 解决问题: bottle.sayHi();

let bottle = {
  nickname: "bottle",
  sayHello() {
    console.log(`Hello, ${this.nickname}!`)
  },
  sayHi(){
    var _this = this // 缓存this
    setTimeout(function(){
      console.log('Hello, ', _this.nickname)
    }, 1000)
  }
};

bottle.sayHi();
// Hello,  bottle

那问题二 setTimeout(bottle.sayHello, 1000); 喃?

let bottle = {
  nickname: "bottle",
  sayHello() {
    console.log(`Hello, ${this.nickname}!`);
  }
};

// 加一个包装层
setTimeout(() => {
  bottle.sayHello()
}, 1000); 
// Hello, bottle!

这样看似解决了问题二,但如果我们在 setTimeout 异步触发之前更新 bottle 值又会怎么样呢?

var bottle = {
  nickname: "bottle",
  sayHello() {
    console.log(`Hello, ${this.nickname}!`);
  }
};

setTimeout(() => {
  bottle.sayHello()
}, 1000); 

// 更新 bottle
bottle = {
  nickname: "haha",
  sayHello() {
    console.log(`Hi, ${this.nickname}!`)
  }
};
// Hi, haha!

bottle.sayHello() 最终打印为 Hi, haha! ,那么怎么解决这种事情发生呢?

解决方案二: bind

bind() 最简单的用法是创建一个新绑定函数,当这个新绑定函数被调用时,this 键值为其提供的值,其参数列表前几项值为创建时指定的参数序列,绑定函数与被调函数具有相同的函数体(ES5中)。

let bottle = {
  nickname: "bottle",
  sayHello() {
    console.log(`Hello, ${this.nickname}!`);
  }
};

// 未绑定,“this” 指向全局作用域
let sayHello = bottle.sayHello
console.log(sayHello())
// Hello, undefined!

// 绑定
let bindSayHello = sayHello.bind(bottle)
// 创建一个新函数,将 this 绑定到 bottle 对象
console.log(bindSayHello())
// Hello, bottle!

所以,从原来的函数和原来的对象创建一个绑定函数,则能很漂亮地解决上面两个问题:

let bottle = {
  nickname: "bottle",
  sayHello() {
    console.log(`Hello, ${this.nickname}!`);
  },
  sayHi(){
    // 使用 bind
    setTimeout(function(){
      console.log('Hello, ', this.nickname)
    }.bind(this), 1000)
    
    // 或箭头函数
    setTimeout(() => {
      console.log('Hello, ', this.nickname)
    }, 1000)
  }
};

// 问题一:完美解决
bottle.sayHi()
// Hello,  bottle
// Hello,  bottle

let sayHello = bottle.sayHello.bind(bottle); // (*)

sayHello(); 
// Hello, bottle!

// 问题二:完美解决
setTimeout(sayHello, 1000); 
// Hello, bottle!

// 更新 bottle
bottle = {
  nickname: "haha",
  sayHello() {
    console.log(`Hi, ${this.nickname}!`)
  }
};

问题一,可以通过 bind 或箭头函数完美解决。

问题二,使用bind,最终更新 bottle 后, setTimeout(sayHello, 1000); 打印依然是 Hello, bottle!,完美解决!

参考链接:深入 call、apply、bind、箭头函数以及柯里化