JS 中 this 在各个场景下的指向

151 阅读9分钟

1.this的奥秘

不同于其它的编程语言,在JavaScript中,this表示函数当前执行的上下文,根据不同的调用方式决定了函数当前的执行上下文:

  • 函数调用:fn( arguments )

  • 方法调用:obj.fn( arguments )

  • 构造函数:new Fn( arguments )

  • 隐式调用: fn.call( obj, arguments )

    每种调用模式都是以自己的方式定义上下文,另外严格模式下也会影响执行上下文,容易产生混淆. 不同的函数调用方式是如何影响this的呢?

要搞懂这个问题先了解一下几个术语的意思--- 以toString(123)为例:

  • 函数调用:执行构成函数主体的代码,例如toString函数的调用主体是toString(123)
  • 调用的上下文:指this在函数体内的值,例如arr.push('abc')的调用上下文就是arr
  • 函数的作用域:指的是函数体内可以访问的变量,对象,函数的集合

2.函数的调用

函数的调用表达式为fn(arguments),函数的调用与方法的调用obj.fn()要区分开:

function person(name,age){
	return `我叫${name},今年${age}岁了`
}
//函数调用,person等价于一个函数表达式,后面接(参数体)
person(薇恩,18)
//除此之外还有立即执行的函数表达式(function(...){...})
//例子中第一个圆括号内是一个表达式,计算结果是一个函数,第二个圆括号是对函数的调用
const vn = (function person(name,age){
	return `我叫${name},今年${age}岁了`
})('薇恩',18)

2-1函数调用中的this

this在函数调用中是一个全局对象,它由执行上下文确定,在浏览器中全局对象是window

function sum(a,b){
	console.log(this ===window)
    this.love = 520
    return a+b
}
sum()  // true 
window.love = 520

在浏览器中执行JS调用sum()时,自动将函数的this对象指向winow,也就是浏览器中的全局对象

当函数在任何函数体之外(顶层作用域)使用时,this指向window对象

console.log(this === window)  //true
this.love = 520
console.log(window.love)  // 520

2-2严格模式下函数调用的this

开头提到了严格模式下的this对象,那么严格模式下函数调用的this对象是指向哪里的呢?

  ** 严格模式下函数调用的this是指向undefined **

严格模式是在ECMAScript 5.1引入的,它提供了更好的安全性和更好的错误检查,启用严格模式在函数头部写入use strict;启用后严格模式会影响执行上下文,this在常规函数调用中指向undefined.与上述相反,执行上下文不再是全局对象

function fn(){
	'use strict' //fn一下的子作用域(内部声明的所有函数)
    console.log(this)
}
fn() //undefined

单个Javascript脚本可能包含严格模式和非严格模式

function fn1(){
	console.log(this)
}
fn1() //window

function fn2(){
	'use strict'
	console.log(this)
}
fn2() //undefined

2-3 敲黑板:this在内部函数的时候

函数调用有一个需要注意的问题,通常认为this在内部函数中的情况与外部函数中的情况相同,确切的说,内部函数的上下文只依赖它的调用类型,不依赖于外部函数的上下文. 这个时候就可以通过call() / apply()来修改内部函数的上下文,亦或使用bind()创建绑定函数.

const obj = {
	x:500,
    y:20,
    sum:function(){
    	console.log(this)  // obj
        function cla(){
        	console.log(this === obj) //false
            return this.x + this.y
        }
        return cla()
    }
}
obj.sum() // NaN

上面的例子中sum()是obj对象上面的方法,所以sum的this对象指向obj对象,cla方法定义在sum方法内部,在执行cla()的时候cla是作为一个函数调用,由它所在的执行环境来确定:此时的cla的执行上下文是window,外层函数sum的this对象对它没有影响,因此得不到预期的结果 为了是cla与sum的执行上下文一致,我们可以人为的修改this指向:

	return cla.call(this) 

除此之外我们还可以将外层函数的this对象保存在一个变量中,供cla函数执行的时候使用

sum:function(){
    	console.log(this)  // obj
        const that = this
        function cla(){
        	console.log(that === obj) //true
            return that.x + that.y
        }
        return cla()
    }

还可以通过箭头函数的方法来定义cla 函数:

sum:function(){
    	console.log(this)  // obj
         cla = () => {
        	console.log(this === obj) //true
            return this.x + this.y
        }
        return cla()
    }

3.方法的调用

** 方法是存储在对象属性中的函数 **
const obj = {
	method:function(){
   	return '阿里哈瑟哟'
   }
}
const meg = obj.method()

上面是以属性访问的方式来来执行method方法,区分函数调用和方法调用是非常重要的,因为他们是不同的类型,正确的区分两者之间的区别有助于正确识别上下文

3-1 方法调用的this指向

 **方法调用中,this指向这个方法的拥有者**
const obj = {
   x:520,
   sum:function(){
     this.x += 1
   }
}
 obj.sum() //sum为obj对象的一个属性,以方法调用的方式来调用sum,this指向obj

另外一种情况,对象从原型上继承一个方法,当在对象上调用继承来的方法是,上下文依然指向调用对象本身:

const person = {
	sayHi:function(){
   	console.log(this)
       return this.name
   }
}
const momo = Object.create(person)
momo.name = '欧巴'
momo.sayHi()
// Object.create()创建一个新对象,根据第一个参数设置其原型,momo对象继承person,执行momo.sayHi()的时候,momo是执行上下文

3-2 从对象中提取方法

对象中的方法可以提取到一个单独的变量中:const self = obj.method 当方法单独调用时(在没有对象的情况下调用),函数调用就会发生,此时的this指向全局对象window,严格模式下指向undefined

function Person(name,age){
	this.name = name
    this.age = age
    this.sayHi = function(){
    	console.log(this)
        console.log(`姓名:${this.name}---年龄:${this.age}`)
    }
}
const momo = new Person('mingming',20)
setTimeout(momo.sayHi,1000)
你可能会认为输出结果是打印出momo的相关信息,但是结果并非如此
以上调用方法跟 :
const self = momo.sayHi  
self() 
是等价的(相当把momo.sayHi赋值给了setTimeout的第一个参数)
此时可以借用bind()来得到正确的结果:
setTimeout(momo.sayHi.bind(momo),1000)

4. 构造函数的调用

 function Person(name,age){
 	this.name = name ? name : 'caicai'
    this.age = age
 }
 Person.prototype.sayHi = function(){
 	console.log(`姓名:${this.name}---年龄${this.age}`)
 }
 const momo = new Person('xiaoming',18)
 const roro = new Person
 momo.sayHi()

ES6规定JS允许用class关键词来定义构造函数:

class Person{
	constructor(name,action){
    	this.name = name
        this.action =false
    }
    change(){
    	this.action = true
    }
}
const momo = new Peroson('shuy',false)
momo.change()

构造函数Person创建了一个新的对象,它从原型继承了属性.构造函数的作用就是初始化实例,new调用构造函数,this指向实例对象 需要注意的是当执行 new obj.method()时,JS会执行构造函数调用而不是原来的方法调用:

new obj.method()相当于 const self = obj.method ---> new self

4-1构造函数中的this

构造函数中的this指向新创建的对象,它利用构造函数的参数初始化新的对象,设定属性的初始值,添加事件处理函数 JavaScript function Fn(){ console.log(this instanceof Fn) //true } const Fninstance = new Fn()

4-2在不使用new关键字的情况下调用构造函数

new关键字能保证构造函数的this对象指向实例,以下属于函数调用,上下文指向window对象

function Person(name,age){
	this.name = name
    this.age = age
}

const momo = Person('caicai',18)
momo.name //caicai
momo.age //18
console.log(momo === window) //true

5 隐式调用

call()或apply()属于隐式调用,第一个参数是函数执行时的上下文,当应该使用特定上下文执行函数时,隐式调用是非常有用的,隐式调用时可以用于模拟在一个对象上调用某个方法

function Runner(name) {
  console.log(this instanceof Rabbit); // => true
  this.name = name;  
}
function Rabbit(name, countLegs) {
  console.log(this instanceof Rabbit); // => true
  Runner.call(this, name); //隐式调用了Runner
  this.countLegs = countLegs;
}
const myRabbit = new Rabbit('White Rabbit', 4);
myRabbit; // { name: 'White Rabbit', countLegs: 4 }

6绑定函数

绑定函数是与对象链接的函数.通常使用bind()方法从原函数创建.原函数和绑定函数拥有相同的代码和作用域,但是执行时上下文不同.

方法 myFunc.bind(thisArg[, arg1[, arg2[, ...]]])接受第一个参数thisArg作为绑定函数执行时的上下文,并且它接受一组可选的参数 arg1, arg2, ...作为被调用函数的参数。它返回一个绑定了thisArg的新函数

function multiply(number) {
  'use strict';
  return this * number;
}
const double = multiply.bind(2);

double(3);  // => 6
double(10); // => 20

6-1绑定函数中的this

 **调用绑定函数的时候,this是bind()的第一个参数**

bind()的作用是创建一个新函数,调用该函数的时候,将上下文作为参数传递给bind的第一个参数.它可以是我们创建一个定义了this值得函数

const numbers = {
  array: [3, 5, 10],
  getNumbers: function() {
    return this.array;    
  }
};
const boundGetNumbers = numbers.getNumbers.bind(numbers);  //绑定了函数的执行上下文
boundGetNumbers(); // => [3, 5, 10]
// Extract method from object
const simpleGetNumbers = numbers.getNumbers;
simpleGetNumbers(); // => undefined (严格模式下报错)

6-2紧密的上下文绑定

bind(()创建一个永久的执行上下文,并始终保持它.一个绑定函数不能通过.call()或者.apply()来改变它的上下文,甚至是再次绑定也不会有什么作用.只有绑定函数的构造函数调用才能更改已经绑定的上下文

function getThis() {
  'use strict';
  return this;
}
const one = getThis.bind(1);
// 绑定函数调用
one(); // => 1
// 使用带有.apply()和.call()的绑定函数
one.call(2);  // => 1
one.apply(2); // => 1
// 再次绑定
one.bind(2)(); // => 1
// 以构造函数的形式调用绑定函数
new one(); // => Object

只有new one()改变了绑定函数的上下文,其他方式的调用中this总是等于1。

7.箭头函数

箭头函数的语法简单,没有function关键字.当返回值只有一条语句时,甚至可以省略return关键字.箭头函数是匿名的,所以name属性是一个空字符串.这样它就没有词法上的函数名了.跟常规函数相反,箭头函数不提供arguments对象.但是在ES6中通过rest parameters修复了

 **箭头函数的this指向定义它的上下文**
 

箭头函数不会创建自己的执行上下文,而是从定义它的外部函数中获取this,也就是说箭头函数在定义的时候已经绑定this了

class Person{
	constructor(name,age){
    	this.name = name
        this.age = age
    }
    sayHi(){
    	console.log(this) //Person
        setTimeout(()=>{
        	console.log(this) //Person
        },1000)
        setTimeout(function(){
        	console.log(this) //window
        },1000)
    }
}
const momo = new Person('caicai',50)
momo.sayHi()

setTimeout使用与sayHi方法相同的上下文(momo对象),箭头函数从定义它的函数继承上下文

如果箭头函数在最顶层的作用域定义,则始终是全局对象window

const person = () => {
	console.log(this) //window
}

箭头函数的this指向不能被修改:

const numbers = [1, 2];
(function() {  
  const get = () => {
    console.log(this === numbers); // => true
    return this;
  };
  console.log(this === numbers); // => true
  get(); // => [1, 2]
  // Use arrow function with .apply() and .call()
  get.call([0]);  // => [1, 2]
  get.apply([0]); // => [1, 2]
  // Bind
  get.bind([0])(); // => [1, 2]
}).call(numbers);

无论如何调用箭头函数,他总是保留词汇上下文.箭头函数不能作构造函数

最好不用箭头函数定义对象上面的方法,往往得不到与你期待的效果

	function Person(name,age){
   	this.name = name
       this.age = age
   }
   Person.prototype.sayHi = () => {
   	console.log(this) //window 
       return `我叫${this.name},今年${this.age}`
   }
   const momo = new Person('caicai',50)
   momo.sayHi()  // 由于是箭头函数,this在函数定义的时候就已经绑定了
   合理的写法是:
   Person.prototype.sayHi = function(){
       	console.log(this) //window 
       return `我叫${this.name},今年${this.age}`
   }