this指向以及 call、apply 的手写实现

120 阅读3分钟

this的概念:

在js中,this的意思为当前,是一个指针型变量,它动态指向当前函数的运行环境。

在不同的场景中调用同一个函数,this的指向也可能会发生变化,但是它永远指向其所在函数的真实调用者;如果没有调用者,就指向全局对象window。

普通函数: 关于this,谁调用就指向谁,没有调用者,就指向全局对象window。

箭头函数: 箭头函数的this指向于函数作用域所指向的对象。

默认绑定

全局环境下的this

在全局作用域下,this始终指向全局对象window,无论是否是严格模式!

congsole.log()完整的写法是window.console.log(),

window可以省略,window调用了console.log()方法,所以此时this指向window。

在全局环境定义的函数中使用 this

var name = 'Jenny';
function person() {
    return this.name;
}
console.log(person());  //Jenny

因为调用函数的对象在浏览器中为window,因此this指向window,所以输出Jenny。

而在严格模式下,不能将全局对象用于默认绑定,this会绑定到undefined,只有函数运行在非严格模式下,默认绑定才能绑定到全局对象

隐式绑定

请记住,这里的目标是查看使用 this 关键字的函数定义,并判断 this 的指向。执行绑定的第一个也是最常见的规则称为 隐式绑定。80% 的情况下它会告诉你 this 关键字引用的是什么。

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

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

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

user.greet()

函数上的this

普通函数内的this分为两种情况,严格模式下和非严格模式下。

function test(){
  'use strict'
  console.log(this)
}

// 在严格模式下,必须严格的写出被调用的函数的对象,
// 不可以有省略或者说简写。
test()  // undefined
window.test() // window


function test2(){
  console.log(this)  
}

//非严格模式下,通过test2()和window.test2()调用函数对象,this都指向window。
test2()  // window
window.test2() // window
  • 严格模式下,必须严格的写出被调用的函数的对象,不可以有省略或者说简写。

直接test()调用函数,this指向undefined,window.test()调用函数this指向window。

  • 非严格模式下,通过test2()和window.test2()调用函数对象,this都指向window。

对象中的this

  1. 一层对象
let obj = {
	name = 'A'
  sayName: function(){
  	name='B'
    console.log(this.name)
	}
}

obj.sayName()  // A

调用对象中的方法,此时 sayName() 中的this指向调用该方法的对象 obj

函数的定义位置不影响其this指向,this指向只和调用函数的对象有关

  1. 二层对象
let obj = {
  name: 'A',
  sayName: function(){
  	name='B'
    console.log(this.name)
	}
  obj2:{
    name:'C'
    sayName2: function(){
    	name='B'
      console.log(this.name)
  	}
	}
}


obj.sayName() // A
// 内部方法的 this 指向被调用函数最近的对象
obj.obj2.sayName2() // C

obj.obj2.sayName2()方法中的this指向obj2。

多层嵌套的对象内部方法的 this 指向被调用函数最近的对象(就近原则)

箭头函数中的this

箭头函数本身没有 this 和 arguments ,箭头函数体内的this对象,是定义该函数时所在的作用域指向的对象,而不是使用时所在的作用域指向的对象。

箭头函数没有自己的this指向,它会捕获自己定义所处的外层执行环境,并且继承这个环境的this,

箭头函数的this指向在被定义的时候就确定了,指向当前 定义时所在的对象,且之后永远都不会改变。即使使用 call() apply() bind() 等方法改变this指向也不可以。

对象内部方法的this: 指向调用这些方法的对象,也就是谁调用就指向谁。

var name = 'window'; // 其实是window.name = 'window'

var A = {
   name: 'A',
   sayHello: function(){
      console.log(this.name)
   }
}

A.sayHello();// 输出A

var B = {
  name: 'B'
}

A.sayHello.call(B);//输出B

A.sayHello.call();//不传参数指向全局window对象,输出window.name也就是window

箭头函数中的 this 指向

箭头函数在编译时就能确定 this 指向 (编译时绑定)

var name = 'window'; 

var obj = {
   name: 'obj ',
   sayHello: () => {
      console.log(this.name)
   }
}

obj .sayHello();   // window
  1. 由于sayHello函数是箭头函数,所以自身不能绑定this,因此找它的上一级作用域。如果父级作用域还是箭头函数,就再往上找,一层一层的直到直到this的指向。

  2. obj .sayHello(), 因为 JavaScript 没有块作用域,所以在定义 sayThis 的时候,里面的 this 就绑到 window 上去了

如何使sayHello永远绑定A呢:

var name = 'window'; 

var A = {
   name: 'A',
   sayHello: function(){
      var s = () => console.log(this.name)
      return s//返回箭头函数s
   }
}

var sayHello = A.sayHello();
sayHello();// 输出A 

var B = {
   name: 'B';
}

sayHello.call(B); //还是A
sayHello.call(); //还是A

根据“该函数所在的作用域指向的对象”来分析一下:

  1. 该函数所在的作用域: 箭头函数s 所在的作用域是sayHello,因为sayHello是一个函数。
  2. 作用域指向的对象:A. sayHello指向的对象是A。

虽然箭头函数的this能够在编译的时候就确定了this的指向,但也需要注意一些潜在的坑

  1. 绑定事件监听
const button = document.getElementById('mngb');
button.addEventListener('click', ()=> {
    console.log(this === window) // true
    this.innerHTML = 'clicked button'
})

上述可以看到,我们其实是想要this为点击的button,但此时this指向了window

  1. 通过箭头函数在原型上添加方法时, this 会指向 window
function Person(name,age) {
  this.name = name
  this.age = age
}


// 用箭头函数在原型上添加方法时,this 会指向window
Person.prototype.sayName = () => {
  // 此时this 指向 window
  console.log(this === window) //true
  return this.name
}

// 使用用匿名函数添加原型方法,this 会指向调用方法的的对象
Person.prototype.sayMyName = function(){
  console.log(this)
  return this.name
}



const person = new Person('mm');
console.log(person.sayName()); // window
console.log(person.sayMyName()); // mm

箭头函数其他几点需要注意的地方

  1. 箭头函数没有 this ,其中的 this指向当前 定义时所在的对象

  2. 箭头函数没有原型,不可以当作构造函数使用,不能使用new命令,否则会报错,占用内存小。

  3. 箭头函数中不存在 arguments 对象,如果要用,可以用 rest 参数代替。

  4. 不可以使用yield命令,因此箭头函数不能用作 Generator 函数。

原型链中的this

this这个值在一个继承机制中,仍然是指向它原本属于的对象,而不是从原型链上找到它时,它所属于的对象。

new 绑定

构造函数的this

new构造调用的过程

无论是通过字面量还是通过new进行构造函数调用创建出来的对象,其实都一样。

用new 来调用,背地里创建一个连接到该函数的 prototype 成员的新对象,同时 this 会被绑定到新对象上

调用new的过程如下:

  1. 创建一个新对象

  2. 原型绑定

  3. 绑定this到这个新对象上

  4. 返回新对象

new 绑定

当用 new 调用函数时,JavaScript 解释器都会在底层创建一个全新的对象并把这个对象当做 this。如果用 new 调用一个函数,this 会自然地引用解释器创建的新对象。

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

  this.name = name
  this.age = age
}

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

显式绑定(改变this指向)

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

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

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

.call

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

call(a, b, c)方法接收三个参数,第一个是this指向,第二个,三个是传递给函数的实参,可以是数字,字符串,数组等类型的数据类型都可以。

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

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

greet.call(user)
// 让 greet 方法调用的时候将 this 指向 user 对象

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

换句话说,this 将会指向传递给 call 的第一个参数。
greet 方法调用的时候将 this 指向 user 对象

.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])

call 的手写实现

/**
 * 
 *  将执行函数 作为 所指向对象的一个参数进行调用执行,将 this 指向context
 * 
 * call 与 apply 的差别是 传参是否为数组
 * call 与 bind 的差别是, bind返回的是一个函数,不能立即执行,call 会立即执行
 * 
 */

Function.prototype.myCall = function (context){

  console.log( 'this', this)
  console.log('context',context);
  // 1. 判断调用 myCall 的调用者是否为函数 即 foo 是否为函数
  if( typeof this !== 'function') {
    throw new Error('not a function')
  }
  //  2. 获取传入的参数 先将arguments 转为数组
  //  arguments 中 第一个参数为 改变指向的对象,其余参数为传入的参数
  let args = [...arguments].slice(1)
  console.log( 'args', args)

  // 3. 判断是否传入参数 context, context 为 改变指向的对象
  //  若没有指定指向则默认指向 window
  context = context || window 

  // 4.将调用的函数设置为参数 context 的方法
  context.fn = this
  let result = null
  //  执行调用的函数
  result = context.fn(...args)
  delete context.fn
  return result
}

.apply

apply(a, [b])和call基本上一致,唯一区别在于传参方式,apply把需要传递给fn()的参数放到一个数组(或者类数组)中传递进去,虽然写的是一个数组,但是也相当于给fn()一个个的传递。

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

//  call() 传参的方式
greet.call(user, languages[0], languages[1], languages[2])

//  apply() 传参的方式
greet.apply(user, languages)

apply 的手写实现

Function.prototype.myApply = function(context){
  if( typeof this !== 'function'){
    throw new Error(' not a function')
  }
  context = context || window
  context.fn =this
  // 判断 arguments[1]  是否传入参数
  let result =  arguments[1] ? context.fn(...arguments[1]) : context.fn();
  delete context.fn
  return result

}

foo.myApply(obj,[1,2,3,4]) 

.bind

其使用方法与call一致,唯一的区别在于立即执行还是等待执行

  • call直接改变函数greet的指向 改变fn中的this,并且把fn立即执行

  • bind 返回一个函数   ,该函数改变了this 的指向,需要手动调用

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']
//  call() 改变greet中的this,并且立即执行
greet.call(user, languages[0], languages[1], languages[2])
// bind()改变greet中的this,greet并不执行
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"

bind手写实现

/**
 *  bind 返回的是函数,无法立即执行
 *  
 * 
 */


Function.prototype.myBind = function(context) {

  if( typeof this !== 'function') {
    throw new Error(' not a function')
  }

  const self = this

  context = context || window
  let args = [...arguments].slice(1)
  // 返回函数,实际返回的是 通过 call 实现
  return function fn() {
    return self.myCall(context,...args)
  }
}

var newFunc = foo.myBind(obj,1,2,3,4);

newFunc()

小结

相同点:

call、apply和bind都是JS函数的公有的内部方法,他们都是重置函数的this,改变函数的执行环节。

不同点:

  • bind是创建一个新的函数,而call和aplay是用来调用函数;

  • call和apply作用一样,只不过call为函数提供的参数是一个个地罗列出来,而apply为函数提供的参数是一个数组

总结

优先级

new绑定优先级 > 显示绑定优先级 > 隐式绑定优先级 > 默认绑定优先级

判断 this 指向

  1. 独立函数调用,例如getUserInfo(),此时this指向全局对象window,严格模式需要指定调用对象,否则为undefined
  2. 对象调用,例如stu.getStudentName(),此时this指向调用的对象stu
  3. call()、apply()和bind()改变上下文的方法,this指向取决于这些方法的第一个参数,当第一个参数为null时,this指向全局对象window
  4. 箭头函数没有this,箭头函数里面的this只取决于包裹箭头函数的第一个普通函数的this
  5. new构造函数调用,this永远指向构造函数返回的实例上,优先级最高。
var name = 'global name';
var foo = function() {
  console.log(this.name);
}
var Person = function(name) {
  this.name = name;
}
Person.prototype.getName = function() {
  console.log(this.name);
}
var obj = {
  name: 'obj name',
  foo: foo
}
var obj1 = {
  name: 'obj1 name'
}

// 独立函数调用,输出:global name
foo();
// 对象调用,输出:obj name
obj.foo();
// apply(),输出:obj1 name
obj.foo.apply(obj1);
// new 构造函数调用,输出:p1 name
var p1 = new Person('p1 name');
p1.getName();

this解析流程图

参考

  1. JS中的this指向