难缠的this

2018-09-23  本文已影响0人  DiffY

在深入了解JavaScript中this关键字之前,有必要先退一步,看一下为什么this关键字很重要。this允许复用函数时使用不同的上下文。换句话说,this关键字允许在调用函数或方法时决定哪个对象才是焦点。之后讨论的所有东西都是基于这个理念。我们希望能够在不同的上下文或在不同的对象中复用函数或方法。

我们关注的第一件事是如何判断this关键字的引用。当你试图回答这个问题时,需要问自己的第一个也是最重要的问题是“这个函数在哪里被调用”。判断this引用什么的唯一方法就是看使用this关键字的这个方法在哪里被调用

一 隐式绑定

请记住,判断this指向要看使用this关键字的这个方法在哪里被调用。假如有下面一个对象

const obj = {
  title: 'test',
  fn(): {
    console.dir(this.title)
  }
}

如果想调用fn方法,就要

obj.fn()

这就把我们带到隐式绑定规则的主要关键点。为了判断 this 关键字的引用,函数被调用时先看一看点号左侧。如果有“点”就查看点左侧的对象,这个对象就是 this 的引用。
在上面的例子中,obj 在“点号左侧”意味着 this 引用了 obj 对象。所以就好像 在 fn 方法的内部 JavaScript 解释器把 this 变成了 obj。
再看如下例子

const obj = {
  title: 'test',
  fn() {
    console.dir(this.title)
  },
  sub: {
    title: 'test2',
    fn() {
      console.dir(this.title)
    }
  }
}
obj.fn()   // test
obj.sub.fn()  // test2

每当判断 this 的引用时,都需要查看调用过程,并确认“点的左侧”是什么。第一个调用,obj 在点左侧意味着 this 将引用 obj。第二次调用中,sub 在点的左侧意味着 this 引用 sub。
所以大多数情况下,检查使用this方法的点的左侧是什么。如果没有点呢,继续往下看。

二 显示绑定

如果fn只是一个独立的函数,如下

fn() {
  console.dir(this.title)
}
const obj = {
  title: 'test'
}

为了判断this的引用必须先查看这个函数的调用位置,那怎样才能让fn调用的时候将this指向this?我们并不能再像上面那样简单的使用obj.fn(),因为obj并没有fn方法。但是在js中,每个函数都有一个方法call,正好解决这个问题。

"call" 是每个函数都有的方法,它允许在调用函数时为函数指定上下文

所以,可以用下面的方式调用fn方法

fn.call(obj)

call是每个函数都有的属性,并且传递给它的第一个参数会作为函数调用时的上下文。也就是说,this会指向传递给call的第一个参数

这就是显示绑定的基础,因为我们明确的使用call指定了this的引用。
现在将fn改动一下

fn(name1, name2){
  console.dir(`${this.title} is ${name1} and ${name2}`)
}

此时使用call方法就要如下

fn.call(obj, '张三', '李四')

使用call方法需要将参数一个一个的传递进去,参数过多,就会越麻烦。此时apply方法就可以解决。

apply和call本质相同,但不是一个个传递参数,可以用数组传参且apply会在函数中为你自动展开。

const arr = ['张三', '李四']
fn.apply(obj, arr)

除了call和apply可以显示绑定this外,还有bind方法也可以

bind和call调用方式完全相同,不同的是bind不会立即调用函数,而是返回一个能以后调用的新函数

const newFn = fn.bind(obj, '张三', '李四')
newFn()

传入的不是对象:
如果传入了一个原始值(字符串,布尔类型,数字类型),来当做this的绑定对象,这个原始值转换成它的对象形式。
如果把null或者undefined作为this的绑定对象传入call/apply/bind,这些值会在调用时被忽略,实际应用的是默认绑定规则

三 new 绑定

function fn(a) {
  this.a = a
}
const bar = new fn(2)
console.log(bar.a) // 2

用new操作符创建对象时会发生如下步骤:

  1. 创建一个全新的Object对象实例
  2. 将构造函数的执行对象赋给新生成的这个实例
  3. 这个新对象会绑定到函数调用的this
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象
    规则:使用构造调用的时候,this会自动绑定在new期间创建的对象上

所以,new fn(2)就相当于

const newObj = new Object()
newObj.a = a

还有一种特殊情况,就是当构造函数通过 "return" 返回的是一个对象的时候,此次运算的最终结果返回的就是这个对象,而不是新创建的对象,因此 this 在这种情况下并没有什么用。注意,返回函数也是返回新的对象,函数对象

function fn(a) {
  this.a = a
  return {}
}
const bar = new fn(2)
console.log(bar.a) // undefined

四 window 绑定

fn() {
  console.dir(this.title)
}
const obj = {
  title: 'test'
}
fn()  // undefined

当点的左侧没有任何东西,也没有通过call、apply、bind方法调用,也没有new关键字,this就会指向了window。

window.title = '全局'
fn() {
 console.dir(this.title)
}
fn()  // 全局

在ES5添加的严格模式中,this不会指向window对象,而是保持为undefined

五 四种绑定的优先规则

显式绑定 > 隐式绑定 > 默认绑定
new绑定 > 隐式绑定 > 默认绑定

六 丢失的this

在某些情况下会丢失 this 的指向,此时,我们就需要借助 call、apply 和 bind 来改变 this 的指向问题。
示例一,当fn方法作为obj对象的属性调用时,this指向obj对象,当另外一个变量引用fn方法时,因为它作为普通函数调用,所以this指向window对象

const obj = {
  title: 'test',
  fn: function() {
    console.dir(this.title)
  }
}
obj.fn() // test
const getTitle = obj.fn
getTitle() // undefined

这种方式实际就是函数调用时,并没有上下文对象,只是对函数的引用,同样的问题,还发生在传入回调函数中

test(obj.fn)  // 传入函数的引用,调用时也是没有上下文

示例二,即使在函数内部定义的函数,如果它作为普通对象调用,this同样指向window对象

const obj = {
  title: 'test',
  name: '张三',
  fn: function() {
    console.dir(this.title)
    function getName(){
      console.dir(this.name)
    }
    getName()
  }
}
obj.fn() // test
            // undefined

七 练习

var xuuu = 123
function  test() {
  var xuuu = 456
  this.aa = 6666    
  return function() {
    console.log(xuuu)
    console.log(this.aa)
    console.log(this.xuuu) 
}

var sdf=new test()
sdf()      // 456, undefined, 123
test()()   // 456,6666,123

结论:第一种情况的中this.aa指向sdf,return中this指向全局对象window;第二种情况中两次this都指向window

八 箭头函数的this

在以往的函数中,this 有各种各样的指向(隐式绑定,显示绑定,new 绑定, window 绑定......),虽然灵活方便,但由于不能在定义函数时而直到实际调用时才能知道 this 指向,很容易给开发者带来诸多困扰。比如下面的代码

function User() {
  this.name = 'John'

  setTimeout(function greet() {
    console.log(`Hello, my name is ${this.name}`)  // Hello, my name is 
    console.log(this) // window
  }, 1000)
}

const user = new User()

如果想把greet里面的this指向user对象该怎么办呢?
1 使用闭包

function User(){
  const self = this
  this.name = 'John'

  setTimeout(function greet() {
    console.log(`Hello, my name is ${self.name}`)  // Hello, my name is John
    console.log(self) // User {name: "John"}
  }, 1000)
}

const user = new User()

** 2 使用显示绑定bind **

function User() {
  this.name = 'John';

  setTimeout(function greet() {
    console.log(`Hello, my name is ${this.name}`); // Hello, my name is John
    console.log(this); // User {name: "John"}
  }.bind(this)(), 1000);
}

const user = new User();

** 利用 setTimeout 的可以传更多参数的特性 **

function User() {
  this.name = 'John';

  setTimeout(function greet(self) {
    console.log(`Hello, my name is ${self.name}`); // Hello, my name is
    console.log(self); // window
  }, 1000, this);
}

const user = new User();

** 箭头函数如何解决 **

function User() {
  this.name = 'John';

  setTimeout(() => {
    console.log(`Hello, my name is ${this.name}`); // Hello, my name is John
    console.log(this); // User {name: "John"}
  }, 1000);
}

const user = new User();

箭头函数在自己的作用域内不绑定 this,即没有自己的 this,如果要使用 this ,就会指向定义时所在的作用域的 this 值。在上面的代码中即指向 User 函数的 this,而 User 函数通过 new 绑定,所以 this 实际指向 user 对象

function foo() {
  return () => {
    console.log(this.a);
  };
}
let obj1 = {
  a: 2
};
let obj2 = {
  a: 22
};
let bar = foo.call(obj1); // foo this指向obj1
bar.call(obj2); // 输出2 这里执行箭头函数 并试图绑定this指向到obj2 但并不成功

结论:
1、箭头函数没有自己的this,它的this继承于它外面第一个不是箭头函数的函数的this指向。
2、箭头函数的 this 一旦绑定了上下文,就不会被任何代码改变
3、箭头函数使用call、apply、bind时,会自动忽略掉第一个参数
4、严格模式并不影响箭头函数自己的this

九 最后的图片

上一篇下一篇

猜你喜欢

热点阅读