前端内练基础-关于this的理解

158 阅读6分钟

前言:this的关键字是JavaScript中最复杂的机制之一,它是有一个很特别的关键字,被自动定义在所有函数作用域中。但是即使是非常有经验的JavaScript开发者也很难说清它到底指向的是什么。

——来源《你不知道的JavaScript上卷》

在开始之前 我们先带着几个问题出发。
  • this的定义什么?
  • 有几种绑定方式,分别是什么?谁的优先级比较高?
  • 改变this的指向有几种方式,分别是什么?底层是如何实现的?
this的定义

this是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数时的各种条件,this的绑定和函数声明的位置没有任何关系,只取决函数的调用方式

举个例子:

function foo () {
   var a = 2;
   this.bar();
}

function bar() {
    console.log(this.a)
}

foo();  // undefined

解析:

其实从上面那段代码可以明显看到,当foo()进行调用的时候,可以理解代码解析成 window.foo() 这个时候 this.bar() 指向的就是window,同样的在 bar() 函数打印的console.log(this.a) 可以理解为 console.log(window.a)但是真正的var a = 2其实是在foo()作用域中,当前的window对象中并不存在,那么只能输入undefined

总结: this实际上是在函数被调用时发生的绑定,它的指向什么完全取决于函数在哪里调用。


一、理解调用位置
function baz() {
    // 当前的调用栈 :baz
    // 因此,当前的调用位置是全局作用域
    
    console.log('baz')
    bar() // bar 的调用位置
}

function bar() {
   // 当前的调用栈是 baz -> bar
   // 因此,当前的调用位置在bar中。
   
   console.log('bar')
   foo(); // <-- foo的调用位置
}

function foo() {
    // 当前的调用栈是 baz -> bar -> foo
    // 因此,当前的调用位置在bar中。
    
    console.log('foo')
}

baz(); // <--- baz的调用位置

注意我们是如何从调用栈中分析出真正的调用位置,它决定了this的绑定。


二、绑定规则
  • 默认绑定
  • 隐式绑定
  • 显式绑定
  • 显式绑定变种之硬绑定
  • new 绑定
默认绑定
function foo () {
    console.log(this.a)
}

var a = 2;

foo();

解析:

function foo () {
    // console.log(this.a)
    console.log(window.a)
}

// var a = 2;
window.a = 2;

// foo();
window.foo();

**你应该理解的第一件事就是声明在全局作用域的变量,都会挂载到window对象中,**当然除了es6的let、const等存在着暂时性死区的声明。

总结:

在代码中,foo()直接使用不带任何修饰的函数引用进行调用,就是默认绑定。

注意:

如果使用严格模式,则不能将全局的对象用于默认绑定,因此this会绑定到undefined

function foo () {
    "use strict"

    console.log(this.a)
}

var a = 2;

foo(); //  Cannot read property 'a' of undefined

细节: 在严格模式中直接调用foo(),不会影响默认的绑定。

function foo () {
    console.log(this.a)
}

var a = 2;

;(function(){
    "use strict"

    foo();
}())

隐式绑定

例1: 普通的隐式绑定

function foo () {
    console.log(this.a)
}

var obj = {
    a:2,
    foo:foo
}

obj.foo()
       

解析:

// function foo () {
//     console.log(this.a)
// }

var obj = {
    a:2,
    // foo:foo,
    foo() {
        // 函数执行时,this指向的obj,console.log(obj.a)
        console.log(obj.a)
    }
}

obj.foo()

例2: 对象属性引用链只有上一层或者最后一层调用位置起作用。

function foo() {
    console.log(this.a)
}

var obj2 = {
    a:42,
    foo:foo
}

var obj1 = {
    a:2,
    obj2:obj2 
}

obj1.obj2.foo() // 42 

解析:

// function foo() {
//     console.log(this.a)
// }

var obj2 = {

    a:42,
    // foo:foo   // 我是在最后一层、我不管你前面是谁,我都指向你 => obj2
    foo(){
        //  函数执行时,this指向的obj2,console.log(obj2.a)
        console.log(obj2.a)
    }
}

var obj1 = {
    a:2,
    obj2:obj2 
}

obj1.obj2.foo() // 42 

例3:

隐式绑定this丢失

function foo () {
    console.log(this.a)
}

var obj = {
    a:2,
    foo:foo
}

var bar = obj.foo // 函数别名

var a = 'window'

bar()

解析:

 function foo () {
    console.log(this.a)
}

// 第一步
var obj = {
    a:2,
    // foo:foo
    foo(){
       /**
        * 函数在运行中是谁在调用的
        * 1、如果是 obj.foo() ; a = 2;因为当前的this指向是运行时的绑定。
        * 2、如果是函数别名 这个时候 其实在window.xx ,那么指向的就是全局
        * 
       */
        console.log(this.a)
    }
}

// 第二步
// 如果直接执行 obj.foo() ; a = 2

// 第三步
// var bar = obj.foo // 函数别名
window.bar = function(){
    console.log(this.a) // this => window
}

var a = '我是全局'

bar()

虽然bar是obj.foo的一个引用,实际上它引用的是foo的函数本身,因此应用了默认绑定

总结: 当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象,


显式绑定

普通的显式绑定

function foo () {
    console.log(this.a)
}

var obj = {
    a:2
}

foo.call(obj)

解析:

function foo () {
    console.log(this.a)
}

var obj = {
    a:2,
    foo(){  // 我借用一下这个方法喽
        console.log(this.a)
    }
}

foo.call(obj)

那么 怎么理解call到底做了什么?能让obj借用foo的方法呢。

call的实现

Function.prototype.call_ = function (context, ...args) {
    var context = context || window;  // 1、传入对象了吗?没有的就是全局喽
    context.fn = this;  // 2、 是谁在调用我,那我指向谁

    var result = context.fn(...args) //3、 把整个参数都执行一遍
    delete context.fn;  // 执行完了 我就删除之前的信息
    return result   
}

apply的实现

Function.prototype.apply_ = function (context, args) {
    var context = context || window;  // 1、传入对象了吗?没有的就是全局喽
    context.fn = this;  // 2、 是谁在调用我,那我指向谁

    var result = context.fn(...args) //3、 把整个参数都执行一遍
    delete context.fn;  // 执行完了 我就删除之前的信息
    return result   
}

其实从代码中就可以看出,两个实现的方式都是一样的,但是call传递的是普通的参数,apply传递的是数组


硬绑定

显示绑定的变种 -> bind

function foo () {
    console.log(this.a)
}

var obj = {
    a:2
}

var bar = function () {
    foo.call(obj)
}

bar();  // 2

setTimeout(bar,1000);  // 2

bar.call(window); //2

硬绑定的原理就是不管之后怎么调用bar,它总会手动在obj上调用foo。既然硬绑定相当于bind绑定,那么原生的bind是怎么实现的呢?

bind实现:

Function.prototype.bind = function (context, ...args) {
    // 1、 必须传递一个函数、<我可是挂载到函数原型的一个方法>
    if(typeof this !== 'function') {
        throw new Error('你得传递一个函数呀')
    }

    // 2、存储当前的指针对象
    var self = this;    

    // 3、返回一个函数
    var fbund = function () {
        // 3-1 使用apply的方法判断绑定的原型对象,拼接外面的函数和返回函数的arguments参数
        return self.apply(this instanceof self ? this : context, args.concat([...arguments]))
    }

    // 4、当前的原型存储、保存原型对象
    if(this.prototype) {
        fbund.prototype = Object.create(this.prototype)
    }

    // 返回硬绑定的函数对象;<对 我就是bar>
    return fbund
}

应用的场景:

包裹函数:接受参数并返回值

function foo (soms) {
   console.log(this.a,soms);
   return this.a + soms
}

var obj = {
   a:2
}

var bar = function () {
   return foo.apply(obj,arguments)
}

var b = bar(2)
console.log(b);

new绑定

想要了解new绑定的原理,首先要了解new关键字调用的时候发生了什么。

  • 创建一个全新的对象
  • 这个新对象会被执行prototype链接
  • 这个新对象会绑定到函数调用的this
  • 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象

示例代码:

function Foo (a) {
    this.a = a;
}

var bar = new Foo(2)

console.log(bar.a)

new的实现

function _new (ctor, ...args) {
    // 1、 必须传递一个函数、
    if(typeof ctor !== 'function') {
        throw new Error('你得传递一个函数呀')
    }

    // 2、创建一个全新的对象
    let obj = new Object();

    // 3、执行prototype 链接
    obj.__proto__ = Object.create(ctor.prototype);

    // 4、传递执行的参数
    let res = ctor.apply(obj,[...args]) 

    let isObject = typeof res === 'object' && res !== null;
    let isFunction = typeof res === 'function';

    // 5、判断构造函数是否返回了一个对象\函数、如果返回对象那么就执行对象\函数
    return isObject || isFunction ? res : obj;
}

箭头函数
function foo () {
    return (a) => {
        console.log(this.a)
    }
}

var obj1 = {
    a:2
};

var obj2 = {
    a:3
};

var bar = foo.call(obj1)

bar.call(obj2);

解析: foo()内部创建的箭头函数会捕获调用时的foo()的this,由于foo()的this绑定到了obj1,bar(箭头函数)的this也会绑定到obj1,箭头函数的绑定无法被修改

总结: 需要知道的是:箭头函数根本没有自己的this,导致内部的this指向了外层代码的this这个指向在定义时就已经确定而不会在调用时指向其执行环境的(变量)对象


三、绑定优先级判断

首先确定一点的是:默认绑定的优先级是最低的。

隐式绑定和显式绑定哪个优先级更高?

function foo () {
   console.log(this.a)
}

var obj1 = {
   a:2,
   foo:foo
}

var obj2 = {
   a:3,
   foo:foo
}

obj1.foo();  // 2
obj2.foo();  // 3

obj1.foo.call(obj2); // 3
obj2.foo.call(obj1); // 2

总结:显式绑定优先级高。

隐式绑定和new绑定哪个优先级更高?

function foo (some) {
  this.a = some
}

var obj1 = {
   foo:foo
}

var obj2 = {}

obj1.foo(2);
console.log(obj1.a)    // 2

var bar = new obj1.foo(4)
console.log(obj1.a)  // 2
console.log(bar.a)   // 4

总结:new绑定优先级高。

显式绑定和new绑定哪个优先级更高?

function foo (some) {
    this.a = some;
}

var obj1 = {};

var bar = foo.bind(obj1)
bar(2);

console.log(obj1.a) // 2

var baz = new bar(3);
console.log(obj1.a) // 2
console.log(baz.a) // 3

总结:new绑定优先级高。

结论:new绑定 > 显式绑定 > 隐式绑定 > 默认绑定。

结论

以上就是关于this的全部知识点,想要全部弄清楚还需要多写多看,下面让我们来几道关于this指向的题在巩固一下刚才看的内容。

四、关于this的面试题

实例代码1:

var a = 1
function foo () {
  var a = 2
  function inner () { 
    console.log(this.a) // ?
  }
  inner()
}

foo()

实例代码2:

var obj = {
  a: 1,
  foo: function (b) {
    b = b || this.a
    return function (c) {
      console.log(this.a + b + c)
    }
  }
}
var a = 2
var obj2 = { a: 3 }

obj.foo(a).call(obj2, 1)
obj.foo.call(obj2)(1)


实例代码3:

function foo1 () {
  console.log(this.a)
}
var a = 1
var obj = {
  a: 2
}

var foo2 = function () {
  foo1.call(obj)
}

foo2()
foo2.call(window)

更多请参考this面试题