javascript this

2019-06-06  本文已影响0人  小泡_08f5

在深入了解JavaScript中的this关键字之前,先了解一下this关键字的重要性。
this 允许复用函数时使用不同的上下文。 换句话说:this关键字允许在调用函数或方法时决定哪个对象应该是焦点。 后面讨论的都是希望能够在不同的上下文或在不同的对象中复用函数或方法。

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

function greet (name) {
  alert(`Hello, my name is ${name}`)
}

greet 会具体警告什么内容?
只给出函数定义是不可能知道答案的。
必须看看greet 函数的调用过程:

greet('Tyler')

判断this关键字引用什么也是同样道理, 甚至可以把this当成一个普通的函数参数对待--它会随着函数调用方式的变化而变化

为了判断 this 的引用 必须先看看函数的定义, 在实际地查看函数定义时, 外面设立四条规则来查找引用,它们是:

一、隐式绑定

执行绑定的第一个也是最常见的规则称为 隐式绑定。 80%的情况下它会告诉你 this 关键字引用的是什么

假如我们有一个这样的对象

const user = {
  name: 'Tyler',
  age: 27,
  greet() {
    alert(`Hello, my name is ${this.name}`)
  }
}

现在,如果你要调用 user 对象上的 greet 方法, 你会用到点号。

user.greet()

这就把我们带到隐式绑定规则的主要关键点。 为了判断this关键字的引用, 函数被调用时先看一点点号左侧。 如果有”点“就查看点左侧的对象,这个对象就是 this 的引用。

在上面的例子中, user 在 ”点号左侧“ 意味着 this 引用了 user 对象。 所以就好像在 greet 方法的内部 JavaScript 解释器把 this 变成了 user

greet() {
  // alert(`Hello, my name is ${this.name}`)
  alert(`Hello, my name is ${user.name}`) // Tyler
}
const user = {
  name: 'Tyler',
  age: 27,
  greet() {
    alert(`Hello, my name is ${this.name}`)
  },
  mother: {
    name: 'Stacey',
    greet() {
      alert(`Hello, my name is ${this.name}`)
    }
  }
}
user.greet()
user.mother.greet()

每当判断this的引用时, 我们都需要查看调用过程, 并确认”点的左侧“是什么。
第一个调用,user在点左侧意味着this将引用user.
第二次调用, mother 在点的左侧意味着this 引用mother

user.greet() // Tyler
user.mother.greet() // Stacey

如前所述, 大约80%的情况下在”点的左侧“都会有一个对象。 这就是为什么在判断this指向时”查看点的左侧“是你要做的第一件事。 但是,如果没有点呢? 这就为我们引出了下一条规则:

二、显式绑定

如果greet 函数不是 user 对象的函数, 只是一个独立的函数

function greet () {
  alert(`Hello, my name is ${this.name}`)
}

const user = {
  name: 'Tyler',
  age: 27,
}

我们知道为了判断this的引用我们首先必须查看这个函数的调用位置。 现在就引出了一个问题,我们怎么样能让greet方法调用的时候将this指向user 对象呢? 我们不能再像之前那样简单的使用 user.greet(), 因为user 并没有greet方法。 在JavaScript中, 每个函数都包含了一个能让你恰好解决这个问题的办法,这个方法的名字叫 call

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

考虑到这一点, 用下面的代码可以再调用 greet 时用user做上下文

greet.call(user)

再强调一遍, call 是每个函数都有的一个属性, 并且传递给它的第一个参数作为函数被调用时的上下文, 换句话说, this 将指向传递给call 的第一个参数。

这就是第2条规则的基础(显式绑定)。 因为我们明确地(使用。call)指定了 this 的引用。

如果greet方法要接收一些参数

function greet (lang1, lang2, lang3) {
  alert(`Hello, my name is ${this.name} and I know ${lang1}, ${lang2}, and ${lang3}`)
}

我们将参数传递给使用.call 调用的函数,你需要在指定上下文(第一个参数)后一个一个的传入

function greet (lang1, lang2, lang3) {
  alert(`Hello, my name is ${this.name} and I know ${lang1}, ${lang2}, and ${lang3}`)
}

const user = {
  name: 'Tyler',
  age: 27,
}

const languages = ['JavaScript', 'Ruby', 'Python']

greet.call(user, languages[0], languages[1], languages[2])

上面代码可以改写

const languages = ['JavaScript', 'Ruby', 'Python']

// greet.call(user, languages[0], languages[1], languages[2])
greet.apply(user, languages)

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

到目前为止,我们学习了关于 .call 和 。apply的”显式绑定“规则, 用此规则调用的方法可以让你指定this在方法内的指向。 关于这个规则的最后一部分是. bind. .bind和.call完全相同。 除了不会立刻调用函数,而是返回一个能以后调用的新函数。 上面代码换成用 .bind

function greet (lang1, lang2, lang3) {
  alert(`Hello, my name is ${this.name} and I know ${lang1}, ${lang2}, and ${lang3}`)
}

const user = {
  name: 'Tyler',
  age: 27,
}

const languages = ['JavaScript', 'Ruby', 'Python']

const newFn = greet.bind(user, languages[0], languages[1], languages[2])
newFn() // alerts "Hello, my name is Tyler and I know JavaScript, Ruby, and Python"

三、new 绑定

第三条判断 this 引用的规则是 new 绑定。 每当用new调用函数时, JavaScript 解释器都会在底层创建一个全新的对象并把这个对象当做this. 如果用 new 调用一个函数, this 会自然的引用解释器创建的新对象
也就是用构造函数new出来的实例,this指向实例

function User (name, age) {
  /*
    JavaScript 会在底层创建一个新对象 `this`,它会代理不在 User 原型链上的属性。
    如果一个函数用 new 关键字调用,this 就会指向解释器创建的新对象。
  */

  this.name = name
  this.age = age
}

const me = new User('Tyler', 27)

四、 window绑定
function sayAge () {
  console.log(`My age is ${this.age}`)
}

const user = {
  name: 'Tyler',
  age: 27
}

如前所述,如果你想用 user 做上下文调用 sayAge, 你可以使用 .call.apply.bind。但如果直接调用

sayAge() // My age is undefined

不出意外,你会得到 My age is undefined, 因为 this.age 是 undefined
实际上这是因为点的左侧没有任何东西,我们也没有用 .call, .apply, .bind, 或者new 关键字, JavaScript 会默认this 指向window对象。

window.age = 27

function sayAge () {
  console.log(`My age is ${this.age}`)
}
sayAge() // My age is 27

这就是第4条规则为什么是 window 绑定的原因。 如果其他规则都没满足,JavaScript就会默认this指向window对象

在ES5添加的 严格模式中, JavaScript不会默认this指向windowd对象,而会正确地把this保持为 undefined

'use strict'

window.age = 27

function sayAge () {
  console.log(`My age is ${this.age}`)
}

sayAge() // TypeError: Cannot read property 'age' of undefined

因此,将所有规则付诸实践,每当我在函数内部看到 this 关键字时 ,判断它的引用而采取的步骤:
1、查看函数在哪被调用。
2、点左侧有没有对象? 如果有, 它就是 ”this“ 的引用。 如果没有,继续第3步。
3、该函数是不是用”call“ 、”apply“或者”bind“ 调用的? 如果是,它会显式的指明 this 的引用,如果不是, 继续第4步。
4、该对象是不是用 ”new“ 调用的? 如果是,this 指向的就是 JS 解释器新创建的对象。 如果不是,继续第5步。
5、是否在 ”严格模式“下? 如果是, this 就是 undefined, 如果不是, 继续第6步。
6、this 指向 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 可以由上面的四个规则判断出来
没有显式绑定,隐式绑定, new ,所以直接得出结论 this 指向 window. 但实际上这里是想把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();

3. 利用setTimeout 的可以传更多参数的特性
其实第三种和第一种比较像, 都用到了闭包。

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

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

const user = new User();

三种办法都可以解决问题,但是都要额外写冗余的代码来指定 this

现在,箭头函数可以轻松的让greet函数保持this指向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 User() {
  this.name = 'John';

  setTimeout(() => {
    'use strict'
    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 来自于 user 的this, 只要 user 的this不变, 箭头函数的this也保持不变。

那么使用bind, call或者apply呢

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"}
  }).bind('no body'), 1000);
}

const user = new User();

答案还是没有影响。 因为箭头函数没有自己的this, 使用bind, call 或者apply时, 箭头函数会自动忽略掉bind 的第一个参数

总结: 箭头函数在自己的作用域内没有自己的this, 如果要使用this, 就会指向定义时所在的作用域的this值

一张图概括:

image

https://juejin.im/post/5b9f176b6fb9a05d3827d03f

面试题:
题目一:

var a = {
    name:"zhang",
    sayName:function(){
        console.log("this.name="+this.name);
    }
};
var name = "ling";
function sayName(){
    var sss = a.sayName;
    sss(); //this.name = ?
    a.sayName(); //this.name = ?
    (a.sayName)(); //this.name = ?
    (b = a.sayName)();//this.name = ?
}
sayName();
image.png

分析:
1.在全局变量中执行函数sayName,先是var sss = a.sayName,相当于sss = function(){console.log(this.name)},然后执行sss(),这时候调用sss()函数就已经不是a.sayName了,而是window环境下的sayName函数,拿到的this的值自然就是"ling";
2.然后执行a.sayName(),这个a.sayName函数中的this指向的是对象a,所以拿到的也是对象a中的属性name的值;
3.执行(a.sayName)(),这一条代码中,可以等价于a.sayName(),括号的作用只是包裹了a.sayName,然后执行它,a.sayName中的this依旧指向的是对象a,所以拿到的还是对象a中的name的值;
4.执行(b = a.sayName)(),这一条代码中,首先执行了b = a.sayName这个赋值操作,然后执行b(),和第一条的执行sss()没有什么区别,所以拿到的this也是全局变量name;

题目二:

var name = "ling";
function sayName(){
    var a = {
        name:"zhang",
        sayName:getName
    };
     
    function getName(){
        console.log(this.name);
    }
     
    getName(); //this.name = ?
    a.sayName(); //this.name = ?
    getName.call(a);//this.name = ?
}
sayName();
image.png

分析:
1.当执全局上下文中的函数sayName时,其实就是执行了该函数内的getName(); a.sayName();getName.call(a);这三个函数,其中第一个getName();因为是直接在全局上下文中进行的调用,所以该函数内的this指向window,this.name指向window.name的值;
2.执行a.sayName();时就不一样了,因为a.sayName是在对象a的上下文中,这时候对象a中的属性sayName中的值getName函数的执行上下文就切换到了对象a上,所以这时候的this.name指向a.name;
3.执行getName.call(a),这条代码中,加入了call关键字,而call的作用是切换函数的执行上下文(也就是动态改变this的值),而第一个参数是a,相当于把getName函数绑定到a对象中去,这时候的this指向的就是对象a;

题目三:

var name = "ling";
var obj = {
    name:"zhang",
    sayName:function(){
        console.log("this.name="+this.name);
    },
    callback:function(){
        var that = this;
        return function(){
            var sayName = that.sayName;
            that.sayName(); //this.name = ?
            sayName();//this.name = ?
        }
    }
}
obj.callback()()
image.png

分析:
1.当执行obj.callback()()时,中介是分了两个阶段的,第一阶段执行了obj.callback(),在这一步执行了var that = this;,这时候的callback函数还是处于对象obj的上下文中,所以这时候的this指向obj,所以that === obj,然后返回一个函数出来;
2.现在来到第二阶段,执行obj.callback()返回出来的函数,在这个函数中,首先执行出结果的是that.sayName();,而在第一阶段中,that指向的是obj,相当于执行obj.sayName(),得出的结果自然是"zhang";
3.然后有var sayName = that.sayName;这句话,相当于sayName = function(){console.log(this.name)},然后执行这个sayName(),这时候就要回头看obj.callback()返回出来的这个函数是在哪里进行调用的了,答案是在window中进行调用的,所以在这个函数中的this指向的是window,所以这时候执行sayName(),
得到的答案是全局变量name的值"ling"

上一篇下一篇

猜你喜欢

热点阅读