JS高级——彻底搞懂前端this指向

本文详细介绍了JavaScript中的this绑定规则,包括默认绑定、隐式绑定、显示绑定(call/apply/bind)和new绑定,以及它们之间的优先级。此外,还提到了this绑定规则之外的特殊情况,如忽略显示绑定、函数间接引用和箭头函数的特性。最后,通过多个测试题加深了对this理解的应用。
摘要由CSDN通过智能技术生成

一、认识this

二、this绑定规则(重点)

三、this绑定规则之外

四、this测试(重点)

一、认识this

 1.1 为什么使用this

        在JavaScript中使用this有什么意义呢?下面的代码中,我们通过对象字面量创建出来一个对象,当我们调用对象的方法时,希望将对象的名称一起进行打印。

        (1)如果没有使用this,那么我们的代码会是下面的写法:

//在方法中,为了能够获取到name名称,必须通过obj的引用(变量名称)来获取
//但是这样做有一个很大的弊端:如果我将obj的名称换成了info,那么所有的方法中的obj都需要换成info。
const obj = {
  name: "coder",
  running: function() {
    console.log(obj.name + " running");
  },
  eating: function() {
    console.log(obj.name + " eating");
  },
  studying: function() {
    console.log(obj.name + " studying");
  }
}
obj.running()//coder running
obj.eating()//coder eating
obj.studying()//coder studying

        (2)如果使用this,上面的代码会是下面的写法

//当我们通过obj去调用running、eating、studying这些方法时,this就是指向obj对象
const obj = {
  name: "coder",
  running: function() {
    console.log(this.name + " running");//使用this
  },
  eating: function() {
    console.log(this.name + " eating");
  },
  studying: function() {
    console.log(this.name + " studying");
  }
}
obj.running()//coder running
obj.eating()//coder eating
obj.studying()//coder studying

        所以我们会发现,在某些函数或者方法的编写中,this可以让我们更加便捷的方式来引用对象,在进行一些API设计时,代码更加的简洁和易于复用。当然,上面只是应用this的一个场景而已,开发中使用到this的场景到处都是,这也是为什么它不容易理解的原因。

1.2 this指向什么

        (1)在浏览器中,我们会说this在全局作用域下指向window

        (2)一般在开发中,我们都是在函数中使用this,this会因函数的调用方式和调用位置不同而有不同的值。例如下面代码,采用三种方式进行函数调用,它产生了三种不同的结果:

// 定义一个函数
function foo() {
  console.log(this);
}

// 1.调用方式一: 直接调用
foo(); // window

// 2.调用方式二: 将foo放到一个对象中,再调用
const obj = {
  name: "coder",
  foo: foo
}
obj.foo() // obj对象

// 3.调用方式三: 通过call/apply调用
foo.call("abc"); // String {"abc"}对象

        根据上面函数执行的结果,可以得出以下结论:

        a、函数在调用时,JavaScript会默认给this绑定一个值;

        b、this的绑定,与定义的位置(编写的位置)没有关系;

        c、this的绑定,与调用方式以及调用的位置有关系;

        d、this是在运行时被绑定的;

        this的绑定规则是怎么样的呢?接下来让我们来探索其中的秘密

二、this绑定规则(重点)

2.1 默认绑定(函数独立调用)

        独立的函数调用我们可以理解成函数没有被绑定到某个对象上进行调用;

        (1)案例一:普通函数调用

//该函数直接被调用,并没有进行任何的对象关联;
//这种独立的函数调用会使用默认绑定,通常默认绑定时,函数中的this指向全局对象(window);
function foo() {
  console.log(this); // window
}

foo();

        (2)案例二:函数调用链

//所有的函数调用都没有被绑定到某个对象上;
function test1() {
  console.log(this); // window
  test2();
}

function test2() {
  console.log(this); // window
  test3()
}

function test3() {
  console.log(this); // window
}
test1();

        (3)案例三:将函数作为参数,传入到另一个函数中

function foo(func) {
  func()
}

function bar() {
  console.log(this); // window
}

foo(bar);

        (4)案例四:将对象的函数作为函数参数进行调用

function foo(func) {
  func()
}
cosnt obj = {
  name: "coder",
  bar: function() {
    console.log(this); // window
  }
}

foo(obj.bar);//相当于把obj.bar赋值给func,func = function(){ console.log(this) }

        上面的四个案例,为什么打印的结果都是window呢?那是因为在真正调用函数的位置,并没有进行任何的对象绑定,只是一个独立函数的调用。

2.2 隐式绑定

        函数的调用是通过某个对象发起的。

        (1)案例一:通过对象调用函数

//foo的调用位置是obj.foo()方式进行调用的
//那么foo调用时this会隐式的被绑定到obj对象上
function foo() {
  console.log(this); // obj对象
}
cosnt obj = {
  name: "why",
  foo: foo
}

obj.foo();

        (2)案例二:对象的某个属性值引用函数,将该对象作为另一个对象的某个属性值,然后进行函数调用

//我们通过obj2又引用了obj1对象,再通过obj1对象调用foo函数;
//那么foo调用的位置上其实还是obj1被绑定了this;
function foo() {
  console.log(this); // obj对象
}
const obj1 = {
  name: "obj1",
  foo: foo
}
const obj2 = {
  name: "obj2",
  obj1: obj1
}

obj2.obj1.foo();

        (3)案例三:隐式丢失

//foo最终被调用的位置是bar,而bar在进行调用时没有绑定任何的对象,也就没有形成隐式绑定;
function foo() {
  console.log(this);//wiodow
}
const obj1 = {
  name: "obj1",
  foo: foo
}

// 将obj1的foo赋值给bar
const bar = obj1.foo;//只是将函数赋值给bar,等价于:const bar = foo
bar();

        总结:隐式绑定有一个前提条件,必须在调用的对象内部有一个对函数的引用(比如一个属性);如果没有这样的引用,在进行调用时,会报找不到该函数的错误;正是通过这个引用,间接的将this绑定到了这个对象上。

2.3显示绑定

        隐式绑定需要对象的某个属性引用函数,使得对象在调用函数时,间接的将this绑定到这个对象上。如果我们不希望对象的某个属性来引用函数,同时又想函数在调用时,this指向对象,该如何做呢?那就是进行显示绑定

2.3.1 call、apply、bind

        在进行演示之前,我们先来看看call、apply和bind在被调用时,内部发生了什么,我们来进行手写还原其内部工作流程:

        (1)call函数还原

// 给所有的函数添加一个cyCall的方法
Function.prototype.cyCall = function(thisArg, ...args) {
  // 1.获取需要被执行的函数
  let fn = this  //this指的是sum函数

  // 2.对thisArg转成对象类型(防止它传入的是非对象类型)
  thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg): window

  // 3.调用需要被执行的函数
  thisArg.fn = fn //注意:此时,将函数sum和thisArg建立引用关系
  let result = thisArg.fn(...args)//注意:此时,sum函数中的this指向thisArg
  delete thisArg.fn

  // 4.将最终的结果返回出去
  return result
}

//定义函数
function sum(num1, num2) {
  console.log("sum函数被调用", this, num1, num2)
  return num1 + num2
}

// 默认进行隐式绑定
const result = sum.cyCall("abc", 20, 30)
console.log(result)

        (2)apply函数还原

// 在函数原型上添加cyApply函数
Function.prototype.cyApply = function(thisArg, argArray) {
  // 1.获取到要执行的函数
  let fn = this //this指的是sum函数

  // 2.处理绑定的thisArg
  thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg): window

  // 3.执行函数
  thisArg.fn = fn //注意:此时建立引用
  // argArray = argArray ? argArray: []
  argArray = argArray || []
  let result = thisArg.fn(...argArray) //注意:此时,sum函数的this指向thisArg
  delete thisArg.fn

  // 4.返回结果
  return result
}

//定义一个函数
function sum(num1, num2) {
  console.log("sum被调用", this, num1, num2)
  return num1 + num2
}

const result = sum.cyApply("abc", [20, 30])
console.log(result)

        (3)bind函数还原

//在函数原型上添加cyBind
Function.prototype.cyBind = function(thisArg, ...argArray) {
  // 1.获取到真实需要调用的函数
  let fn = this //this指向sum

  // 2.绑定this
  thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg): window

  function proxyFn(...args) {
    // 3.将函数放到thisArg中进行调用
    thisArg.fn = fn //注意:此时,sum函数与thisArg建立引用关系
    // 特殊: 对两个传入的参数进行合并
    let finalArgs = [...argArray, ...args]
    let result = thisArg.fn(...finalArgs) //此时,sum函数的this指向thisArg
    delete thisArg.fn

    // 4.返回结果
    return result
  }
  return proxyFn //注意:此时返回的是函数
}


function sum(num1, num2, num3, num4) {
  console.log('sum函数被调用',this,num1, num2, num3, num4)
}
const newSum = sum.cyBind("aaa", 10, 20, 30, 40)
const result = newSum()
console.log(result)

        通过call、apply将函数的this绑定到对象上,此时函数中的this就是绑定的对象。

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

foo.call({name: "coder"}); // {name: "coder"}
foo.call({name: "why"}); // {name: "why"}

        通过bind函数,会将一个函数总是显示的绑定到一个对象上。

function foo() {
  console.log(this);
}
const obj = {
  name: "coder"
}

cosnt bar = foo.bind(obj);
bar(); // obj对象
bar(); // obj对象

2.3.3 内置函数

        有些时候,我们会调用一些JavaScript的内置函数,或者一些第三方库中的内置函数。这些内置函数会要求我们传入函数作为参数;我们自己并不会显示的调用这些函数,而是这些内置函数在内部进行调用;在调用的过程中,会将传参函数的this显示的绑定到某个对象上。

        (1)案例一:serTimeout,内部使用apply显示的将传入的函数绑定到window对象上

setTimeout(function() {
  console.log(this); // window
}, 1000);

        (2)案例二:数组的forEach,内部默认进行函数独立调用,所以函数的this指向window

cons names = ["abc", "cba", "nba"];
names.forEach(function(item) {
  console.log(this); // 三次window
});

        (3)案例三:div元素点击事件

        一个class为box的div元素,进行点击,点击事件中this指向box对象

const box = document.querySelector(".box");
box.onclick = function() {
  console.log(this); // box对象
}

2.4 new绑定

        JavaScript中的函数可以当做一个类的构造函数来使用,也就是使用new关键字。使用new关键字来调用函数时,会执行如下的操作:

        (1)创建一个全新的对象;

        (2)这个新对象会被执行Prototype连接;

        (3)这个新对象会绑定到函数调用的this上(this的绑定在这个步骤完成);

        (4)如果函数没有返回其他对象,表达式会返回这个新对象;

// 创建Person
function Person(name) {
  console.log(this); // Person {}
  this.name = name; // Person {name: "why"}
}

const p = new Person("why");
console.log(p);

2.5 四个规则的优先级

2.5.1 默认规则的优先级最低(函数独立调用)

        默认规则的优先级是最低的,因为存在其他规则时,就会通过其他规则的方式来绑定this

2.5.2 显示绑定优先级高于隐式绑定

function foo() {
  console.log(this);
}
const obj1 = {
  name: "obj1",
  foo: foo
}
const obj2 = {
  name: "obj2",
  foo: foo
}

// 隐式绑定
obj1.foo(); // obj1
obj2.foo(); // obj2

// 隐式绑定和显示绑定同时存在
obj1.foo.call(obj2); // obj2, 说明显式绑定优先级更高

2.5.3 new绑定优先级高于隐式绑定

function foo() {
  console.log(this);
}
const obj = {
  name: "why",
  foo: foo
}

new obj.foo(); // foo对象, 说明new绑定优先级更高

2.5.4 new绑定优先级高于bind

        new绑定和call、apply是不允许同时使用的,所以不存在谁的优先级更高;但new绑定优先级是大于bind的显示绑定

function foo() {
  console.log(this);
}
const obj = {
  name: "obj"
}

// const foo = new foo.call(obj);
const bar = foo.bind(obj);
const foo = new bar(); // 打印foo, 说明使用的是new绑定

        优先级总结:new绑定 > 显示绑定(bind)> 隐式绑定 > 默认绑定

三、this绑定规则之外

        上面讲述的绑定规则已经满足我们平时的开发工作,但总有一些语法是超出规则之外的,不受规则约束。

3.1 忽略显示绑定

//如果在显示绑定中,给函数出入null或者undefined参数,
//那么这个显示绑定会被忽略,使用函数独立调用规则:
function foo() {
  console.log(this);
}

const obj = {
  name: "coder"
}

foo.call(obj); // obj对象
foo.call(null); // window
foo.call(undefined); // window

const bar = foo.bind(null);
bar(); // window

3.2 函数间接引用

        将一个对象的函数属性值,赋值给另一个对象,此时只是把函数值给赋值过去,并没有完成this绑定,属于函数独立调用规则

function foo() {
  console.log(this);
}
const obj1 = {
  name: "obj1",
  foo: foo
}; 
const obj2 = {
  name: "obj2"
}

obj1.foo(); // obj1对象,隐式绑定

//obj2.foo = obj1.foo,相当于 obj2.foo = foo
//obj2.foo()相当于foo(),所以属于函数独立调用
(obj2.foo = obj1.foo)();  // window 

3.3 箭头函数

        箭头函数,即()=> {}这种形式的函数,是ES6中新增的一种函数形式。箭头函数不

不绑定this,即上面的绑定规则不适用于箭头函数。那么,箭头函数中的this指向谁呢?

        箭头函数中的this是根据上一层作用域来决定的。

        下面是模拟网络请求,将数据存储到对象的data数组中。

        (1)没有使用箭头函数时,需要获取外部的this(const  _that = this),才可以将数据放到data中:

const obj = {
  data: [],
  getData: function() {
    const _that = this;
    // 模拟网络请求获取数据
    setTimeout(function() {//普通函数
      const res = ["abc", "cba", "nba"];
      _that.data.push(...res);//将数组放到obj的data数组中
    }, 1000);
  }
}

obj.getData();

        (2)使用箭头函数时,就不需要(const _that = this)这个步骤,箭头函数中的this就是向上一层作用域中的this,即getData中的this,此时this 指向obj

const obj = {
  data: [],
  getData: function() {
    // 模拟网络请求获取数据
    //箭头函数不绑定this,需到上一层作用域中找this,即到getData函数中找this
    setTimeout(() => {
      const res = ["abc", "cba", "nba"];
      this.data.push(...res);//此时this就是obj
    }, 1000);
  }
}

obj.getData();




//若getData也是一个箭头函数,那么this是指向谁呢?答案是window
//为什么是window呢?我们一步一步从内向外层找
const obj = {
  data: [],
  
  getData: () => {
  //箭头函数不绑定this,需到上一层作用域中找this,即到全局作用域中找this,即window

    setTimeout(() => {
    // 箭头函数不绑定this,需到上一层作用域中找this,即到getData函数中找this

      console.log(this) //window  此时this是全局作用域中的this,即window
    }, 1000);
  }
}

obj.getData();

四、this测试(重点)

4.1 测试题一:

const name = "window";
const person = {
  name: "person",
  sayName: function () {
    console.log(this.name);
  }
};

function sayName() {
  const  sss = person.sayName;
  // 独立函数调用,没有和任何对象关联
  sss(); // window
  // 关联
  person.sayName(); // person
  (person.sayName)(); // person
  (b = person.sayName)(); // window
}

4.2 测试题二:

const name = 'window'
const person1 = {
  name: 'person1',
  foo1: function () {
    console.log(this.name)
  },
  foo2: () => console.log(this.name),
  foo3: function () {
    return function () {
      console.log(this.name)
    }
  },
  foo4: function () {
    return () => {
      console.log(this.name)
    }
  }
}

const person2 = { name: 'person2' }

// 隐式绑定,肯定是person1
person1.foo1(); // person1
// 隐式绑定和显示绑定的结合,显示绑定生效,所以是person2
person1.foo1.call(person2); // person2

// foo2()是一个箭头函数,不适用所有的规则
person1.foo2() // window
// foo2依然是箭头函数,不适用于显示绑定的规则
person1.foo2.call(person2) // window

// 获取到foo3,但是调用位置是全局作用于下,所以是默认绑定window
person1.foo3()() // window
// foo3显示绑定到person2中
// 但是拿到的返回函数依然是在全局下调用,所以依然是window
person1.foo3.call(person2)() // window
// 拿到foo3返回的函数,通过显示绑定到person2中,所以是person2
person1.foo3().call(person2) // person2

// foo4()的函数返回的是一个箭头函数
// 箭头函数的执行找上层作用域,是person1
person1.foo4()() // person1
// foo4()显示绑定到person2中,并且返回一个箭头函数
// 箭头函数找上层作用域,是person2
person1.foo4.call(person2)() // person2
// foo4返回的是箭头函数,箭头函数只看上层作用域
person1.foo4().call(person2) // person1

4.3 测试题三:

const name = 'window'
function Person (name) {
  this.name = name
  this.foo1 = function () {
    console.log(this.name)
  },
  this.foo2 = () => console.log(this.name),
  this.foo3 = function () {
    return function () {
      console.log(this.name)
    }
  },
  this.foo4 = function () {
    return () => {
      console.log(this.name)
    }
  }
}
const person1 = new Person('person1')
const person2 = new Person('person2')

// 隐式绑定
person1.foo1() // peron1
// 显示绑定优先级大于隐式绑定
person1.foo1.call(person2) // person2

// foo是一个箭头函数,会找上层作用域中的this,那么就是person1
person1.foo2() // person1
// foo是一个箭头函数,使用call调用不会影响this的绑定,和上面一样向上层查找
person1.foo2.call(person2) // person1

// 调用位置是全局直接调用,所以依然是window(默认绑定)
person1.foo3()() // window
// 最终还是拿到了foo3返回的函数,在全局直接调用(默认绑定)
person1.foo3.call(person2)() // window
// 拿到foo3返回的函数后,通过call绑定到person2中进行了调用
person1.foo3().call(person2) // person2

// foo4返回了箭头函数,和自身绑定没有关系,上层找到person1
person1.foo4()() // person1
// foo4调用时绑定了person2,返回的函数是箭头函数,调用时,找到了上层绑定的person2
person1.foo4.call(person2)() // person2
// foo4调用返回的箭头函数,和call调用没有关系,找到上层的person1
person1.foo4().call(person2) // person1

4.4 测试题四:

const name = 'window'
function Person (name) {
  this.name = name
  this.obj = {
    name: 'obj',
    foo1: function () {
      return function () {
        console.log(this.name)
      }
    },
    foo2: function () {
      return () => {
        console.log(this.name)
      }
    }
  }
}
const person1 = new Person('person1')
const person2 = new Person('person2')

// obj.foo1()返回一个函数
// 这个函数在全局作用于下直接执行(默认绑定)
person1.obj.foo1()() // window
// 最终还是拿到一个返回的函数(虽然多了一步call的绑定)
// 这个函数在全局作用于下直接执行(默认绑定)
person1.obj.foo1.call(person2)() // window
person1.obj.foo1().call(person2) // person2

// 拿到foo2()的返回值,是一个箭头函数
// 箭头函数在执行时找上层作用域下的this,就是obj
person1.obj.foo2()() // obj
// foo2()的返回值,依然是箭头函数,但是在执行foo2时绑定了person2
// 箭头函数在执行时找上层作用域下的this,找到的是person2
person1.obj.foo2.call(person2)() // person2
// foo2()的返回值,依然是箭头函数
// 箭头函数通过call调用是不会绑定this,所以找上层作用域下的this是obj
person1.obj.foo2().call(person2) // obj

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值