关于JavaScript中this的指向的总结

对JavaScript中this指向的总结

前言

在常见的面向对象的编程语言如java、C#等中,this关键字通常只会出现在类的方法中,尤其是在类的实例对象中,this代表的是当前的实例对象

例如如下的C#代码:

//定义了一个名为StudyInfo的类
public class StudyInfo
{
    private string Name { get; set; }
    private int Count { get; set; }
    
    /// <summary>
    /// 构造函数
    /// </summary>
    public StudyInfo(string myName,int myCount)
    {
        this.Name = myName;
        this.Count = myCount;
    }

    public void getDetailMassage()
    {
        Console.WriteLine(this.Name);
        Console.WriteLine(this.Count);
    }
}

/// <summary>
/// 构建 类StudyInfo的第一个实例对象info1
/// </summary>
StudyInfo info1 = new StudyInfo("张三", 888);
/// <summary>
/// 构建 类StudyInfo的第一个实例对象info2
/// </summary>
StudyInfo info2 = new StudyInfo("李四", 999);

info1.getDetailMassage();
info2.getDetailMassage();

在这里插入图片描述

但是JavaScript中的this更加的灵活,无论是它出现的位置还是它代表的函数,所以我们需要好好的了解一下它。

javascript是一门什么语言?

也许当你看到在JavaScript中也是使用的this关键字,可能就会“顺其自然”的认为它是一门面向对象的语言。

但面向对象需要包含三大特性,封装、继承、多态,而javascript中只有封装。继承也是模拟继承,所以javascript是一门基于对象的脚本语言,而非面向对象

并且,从语言的分类上来说,Javascript是一门解释性语言,它是单线程的,同一时间只会做同一件事,会将每一个任务都分割成很多个片段,随机执行这些片段

语言的大致分类

  • 编译性语言:C、C++

    • 它是通篇翻译后再执行
    • 优点:快
    • 缺点:不好移植
  • 解释性语言:javascript、PHP

    • 它是一行一行执行的,单线程
    • 优点:好移植,直接能翻译成计算机可以看得懂的东西
    • 缺点:稍慢
  • oak语言:java

    • .java------>jvm虚拟机(javac指令)----->.class----->(java指令)执行代码

this的绑定规则

Q:在正式了解JavaScript中this的指向规则前,我们先来思考一个问题,为什么要使用this,它有什么好处呢?

A:在某些函数或者方法的编写中,this可以让我们更加便捷的方式来引用对象,在进行一些API设计时,代码更加的简洁和易于复用

规则1:默认绑定

使用场景:独立函数调用(也就是当函数没有被绑定到某个对象上进行调用)时,this的绑定规则是默认绑定。通常默认绑定时,函数中的this指向全局对象(window)

//普通函数调用
function exampleFun1(){
    console.log(this);  //window
}
exampleFun1();  //在函数调用的位置,是独立调用的,并没有绑定任何对象,this默认绑定


//函数调用链
function exampleFunPart1(){
    exampleFunPart3();  //window
    //在函数实际调用的位置,其实还是独立调用的,并没有绑定任何对象,this默认绑定
}

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

function exampleFunPart3(){
    exampleFunPart2();
}

exampleFunPart1();  


//将函数作为参数,传递到函数内部
function exampleFun2(func){
    func();
}

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

exampleFun2(exampleFun2_demo);   //window
//也是一样的,在函数实际调用的位置,其实还是独立调用的,并没有绑定任何对象,this默认绑定

规则2:隐式绑定

在开发中,另外一种比较常见的函数调用方式是通过某个对象来进行调用的,也就是在函数的实际调用位置中,是通过某个对象发起的函数调用

//例子1:通过对象来调用函数
function fun1(){
    console.log(this);
}

const testObj = {
    name:"happy",
    fun1
}
//通过对象来调用函数,此时 函数的this会隐式的绑定到对象上
testObj.fun1();   //{name: 'happy', fun1: ƒ}


//例子2:通过对象的对象来调用函数
function fun2(){
    console.log(this);
}
const testObj2 = {
    name:"testObj2",
    fun2
}

const testObj1 = {
    name:"testObj1",
    testObj2
}


//通过对象来调用函数,此时 函数的this会隐式的绑定到【调用它的对象】上
testObj1.testObj2.fun2();  //{name: 'testObj2', fun2: ƒ}

//例子3
function fun3(){
    console.log(this);
}

const testObj3 = {
    name:"happy",
    fun3
}
const getFun = testObj3.fun3;
//此时调用函数会隐式的绑定this到testObj3上吗? no,在实际函数调用的位置,是独立调用的,this默认绑定
getFun();  //window
//相当于就是把这个函数拿出来了,然后再调用

规则3:显示绑定

隐式绑定有一个前提条件:

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

如果我们不希望在 对象内部 包含这个函数的引用,同时又希望在这个对象上进行强制调用,该怎么做呢?

  • JavaScript所有的函数都可以使用call、apply、bind方法(这个和Prototype有关)。

  • 对于call和apply:

    • 它俩的区别其实非常简单,第一个参数是相同的,后面的参数,apply为数组,call为参数列表;

    • 这两个函数的第一个参数都要求是一个对象,这个对象的作用是什么呢?就是给this准备的。

    • 在调用这个函数时,会将this绑定到这个传入的对象上。

因为上面的过程,我们明确的绑定了this指向的对象,所以称之为 显示绑定

function myDemoFun(number1,number2){
    console.log(this.name);
    console.log(number1 + number2);
}

const myObj1 = {
    name:"我是obj1"
}

const myObj2 = {
    name:"我是obj2"
}

const myObj3 = {
    name:"我是obj3"
}


//使用call 来显示绑定
myDemoFun.call(myObj1,66,88);  //我是obj1 154

//使用apply 来显示绑定
myDemoFun.apply(myObj2,[33,22]);    //我是obj2  55 

//使用bind 来拿到显示绑定后的函数  之后就可以重复使用绑定后的函数
const afterBindFun = myDemoFun.bind(myObj3);
afterBindFun(11,22);
afterBindFun(4,22);
afterBindFun(2,22);
afterBindFun(88,22);

看几个例子

那么在了解了上述几种this规则后,我们来看看几种特殊的this,他们是使用以上的哪种规则

(1)JavaScript的内置函数 setTimeout/setInterval

有些时候,我们会调用一些JavaScript的内置函数,或者一些第三方库中的内置函数。这些内置函数会要求我们传入另外一个函数,我们自己并不会显示的调用这些函数,而是在JavaScript内部或者第三方库内部会帮助我们执行;

  • setTimeout或者 setInterval中会传入一个函数,这个函数中的this通常是window
//part1-传入的是个箭头函数
setTimeout(() => {
    console.log("我是一个定时器");
    console.log(this);  //window
},2000);

//part1-传入的是个函数声明
setTimeout(function(){
    console.log("我是一个定时器");
    console.log(this);  //window
},2000);

//part2
let counter = 0;
let myTimer = setInterval(() => {
    console.log("我是一个重复计数器");
    console.log(this);  //window
    counter++;
    if(counter >= 10){
        console.log("计数结束");
        clearInterval(myTimer);
    }
}, 1000);

至于为啥this的指向是window,我们可以从mdn文档中找到答案:

https://developer.mozilla.org/zh-CN/docs/Web/API/setTimeout

在这里插入图片描述

简单来说,就是我们传递进去的函数是独立调用的,他采用的是上述的第一种【默认绑定】

(2)高阶函数forEach
[1,2,3,4,5].forEach(function(item){
    console.log(this);  //window
    console.log(item);
});

也是一样的,在默认情况下,我们传递进去函数调用是独立调用的,他采用的是上述的第一种【默认绑定】

但是,forEach函数可以传递第二个参数来指定this的指向

在这里插入图片描述

let obj = {
    name:"objPart"
};
[1,2,3,4,5].forEach(function(item){
    console.log(this);  //obj
    console.log(item);
},obj);

不过,如果你传递进去的第一个函数是箭头函数,那么this的指向还是window,因为箭头函数没有自己的this,即使是显示绑定也无效,它会从作用域链上,向上寻找父级的this,后文会来继续补充说明箭头函数

let obj = {
    name:"objPart"
};
[1,2,3,4,5].forEach(el => {
    console.log(this);  //window
    console.log(el);
},obj);
(3) 元素的点击

当有一个div,然后我们去监听它的点击事件,其函数中的this的指向是指向谁?

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>关于this的指向的demo</title>
</head>
<body>
    <div class="box" style="width: 100px;height: 100px;background-color: pink;">box</div>
    <script>
        //获取box元素的实例对象
        let boxElement = document.getElementsByClassName("box")[0];
        //回调函数是个函数声明
        boxElement.onclick = function(){
            console.log("点击了box");
            console.log(this);  //box元素
        }

        //回调函数是个表达式
        boxElement.onclick = () =>{
            console.log("点击了box by arrow function");
            console.log(this);  //window
        }
    </script>

</body>
</html>

很明显,boxElement.onclick 函数的调用是由box对象来调用的,采用的是【隐式绑定】,但如果给的是个箭头函数,this的指向是window,和上述同理

规则4:new关键字

JavaScript中的函数可以当做一个类的构造函数来使用,也就是使用new关键字

当执行new命令时,其后面的函数依次执行以下步骤:

  • (1)创建一个空对象,作为要返回的实例对象
    • let obj = {};
  • (2)将这个空对象的原型,指向构造函数的prototype属性
    • obj.proto_ = Fun.prototype
  • (3)将这个空对象赋值给函数内部的this关键字
    • this = obj
  • (4)开始执行构造函数内部的代码
  • (5) 最后返回时,如果构造函数有return的对象,那么就会返回该对象,否则就会隐式返回原本要返回的实例对象

构造函数之所以叫构造函数,就是操作一个空对象,将其构造成需要的样子

function MyPerson1(){
    this.myName = "person1";
    this.getNumber = function(){
        return 999;
    }
    console.log(this);  //MyPerson1
}

let person1 = new MyPerson1();

绑定规则的优先级

结论是: new关键字 > 显示绑定 > 隐式绑定 > 默认绑定

默认绑定的优先级最低

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

显示绑定优先级大于隐式绑定
let myTestObj1 = {
    name: "testObj1",
    getName:function(){
        console.log(this.name);
    }
}

let myTestObj2 = {
    name: "testObj2",
    getName:function(){
        console.log(this.name);
    }
}

//隐式绑定和显示绑定同时存在
myTestObj1.getName.call(myTestObj2);  //testObj2
new关键字的优先级 大于 隐式绑定
function Foo(){
    console.log(this);
}

let testObj = {
    Foo
}

new testObj.Foo();   //Foo(){}
new关键字的优先级大于 显示绑定
new关键字是不允许和call apply一起用
//new关键字是不允许和call apply一起用,是会报错的
function Foo(){
    console.log(this);
}

let testObj = {
    name:"testObj"
}

let foo = new Foo.call(testObj);  //报错:TypeError: Foo.call is not a constructor   
//ok  很明显在这里语法分析就已经报错了


//不过 如果你让 Foo.call() 作为一个整体,先执行,那么就是可以的
function Foo(){
    console.log(this);
    return function FooSon(){
        console.log(this);
    }
}

let testObj = {
    name:"testObj"
}

let foo = new (Foo.call(testObj));  //FooSon


new 关键字和bind
function Foo(){
    console.log(this);
}

let testObj = {
    name:"testObj"
}
let foo = new (Foo.bind(testObj));  //Foo

箭头函数

正常来说,在我们的日常情况下,就是应用上述的四种规则,但存在一个例外,它就是 ES6中新增的箭头函数

  • 箭头函数不会创建自己的this,它只会从自己的作用域链上找父级执行上下文的this
  • 箭头函数没有prototype,没有arguments,也不能当构造函数

其他关于箭头函数的细节,可以自行查找资料,这里就不赘述了

最后想要补充和本篇内容相关的两个面试考点

new 和 object.create的区别

首先来简单了解一下 原型和原型链

原型和原型链

最初,由于同一个构造函数的多个实例之间无法共享属性,从而造成对系统资源的浪费的现象存在,所以设计出了javascript原型对象—>prototype

  • 所有引用类型(函数,数组,对象)都拥有__proto__属性

  • 所有函数都拥有prototype属性(显示原型)

  • 原型对象:拥有prototype属性的对象,在定义函数时就会被创建

  • 实例对象._ proto_ === 构造函数.prototype

  • 原型链
    javascript规定,所有对象的原型都有自己的原型对象,并且原型对象也是对象,也会有自己的原型,因此就会形成原型链,原型链的尽头是null
    —> Object.prototype._ proto_ === null

根据上述所说的来理解这个图
在这里插入图片描述

new和object.create的区别
**object.create()的实现方式**
Object.create = function(Fun){
	const F = function(){}
	F.prototype = Fun;
	return new F();
}
//所以说通过这种方式得到的实例对象.__proto__ === Fun

比较newObject.create
构造函数保留原构造函数的属性丢失原构造函数的属性
原型链原构造函数的prototype属性就是原构造函数
作用对象functionfunction和object

关于Object.create更详细的了解可以查看MDN文档:

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/create

手写 call apply bind

//手写call
Function.prototype.myCall = function(thisArg,...args){
    //1.获取需要执行的函数
    let fn = this;
    //2.将thisArg转为对象类型,做一层防呆
    thisArg = (thisArg != null && thisArg != undefined) ? Object(thisArg) : window;
    //3.将需要调用的函数作为对象的属性绑定到thisArg对象上,但由于thisArg本身就有fu属性,为了避免冲突,在这里使用symbol
    const fn_Symbol = Symbol("fn");
    //由于这里func是Symbol变量不再是字符串,所以要用中括号获取属性
    thisArg[fn_Symbol] = this;
    //4.调用函数,拿到结果
    let res = thisArg[fn_Symbol](...args);
    //5.删除添加到对象thisArg上的这个函数
    delete thisArg[fn_Symbol];
    //返回调用的结果
    return res;
}

//手写apply   与call相比,只有传参的形式不同
Function.prototype.myApply = function(thisArg,args){
    //1.获取需要执行的函数
    let fn = this;
    //2.将thisArg转为对象类型,做一层防呆
    thisArg = (thisArg != null && thisArg != undefined) ? Object(thisArg) : window;
    //3.将需要调用的函数作为对象的属性绑定到thisArg对象上,但由于thisArg本身就有fu属性,为了避免冲突,在这里使用symbol
    const fn_Symbol = Symbol("fn");
    //由于这里func是Symbol变量不再是字符串,所以要用中括号获取属性
    thisArg[fn_Symbol] = this;
    //4.对用户传递进来的参数做一个防呆
    args = args || [];
    //4.调用函数,拿到结果
    let res = thisArg[fn_Symbol](...args);
    //5.删除添加到对象thisArg上的这个函数
    delete thisArg[fn_Symbol];
    //返回调用的结果
    return res;
}


//手写bind
Function.prototype.myBind = function(thisArg,...argArray){
     //1.获取需要执行的函数
     let fn = this;
     //2.将thisArg转为对象类型,做一层防呆
     thisArg = (thisArg != null && thisArg != undefined) ? Object(thisArg) : window;
     //将需要调用的内容放到一个函数中  形成了闭包
     function proxyFun(){
        //3.将需要调用的函数作为对象的属性绑定到thisArg对象上,但由于thisArg本身就有fu属性,为了避免冲突,在这里使用symbol
        const fn_Symbol = Symbol("fn");
        //由于这里func是Symbol变量不再是字符串,所以要用中括号获取属性
        thisArg[fn_Symbol] = this;
        //4.合并参数列表
        let allArgs = [...argArray,...args];
         //5.调用函数 拿到结果
        var result = thisArg[fn_Symbol](allArgs);
         //6.删除添加到对象thisArg上的这个函数
        delete thisArg[fn_Symbol];
         //7. 返回结果
        return result;
     }
     return result;
}


做几个练习题

答案放最后面

练习题1

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

练习题2

var name = 'window'
var 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)
    }
  }
}

var person2 = { name: 'person2' }

person1.foo1(); 
person1.foo1.call(person2); 

person1.foo2();
person1.foo2.call(person2);

person1.foo3()();
person1.foo3.call(person2)();
person1.foo3().call(person2);

person1.foo4()();
person1.foo4.call(person2)();
person1.foo4().call(person2);

练习题3

var 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)
    }
  }
}
var person1 = new Person('person1')
var person2 = new Person('person2')

person1.foo1()
person1.foo1.call(person2)

person1.foo2()
person1.foo2.call(person2)

person1.foo3()()
person1.foo3.call(person2)()
person1.foo3().call(person2)

person1.foo4()()
person1.foo4.call(person2)()
person1.foo4().call(person2)

练习题4

var 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)
      }
    }
  }
}
var person1 = new Person('person1')
var person2 = new Person('person2')

person1.obj.foo1()()
person1.obj.foo1.call(person2)()
person1.obj.foo1().call(person2)

person1.obj.foo2()()
person1.obj.foo2.call(person2)()
person1.obj.foo2().call(person2)

练习题1解析

function sayName() {
  var sss = person.sayName;
  sss(); //【独立调用】  window
  person.sayName();   //【隐式绑定】 person
  (person.sayName)(); //【隐式绑定】person
  (b = person.sayName)(); //【独立调用】window
}

练习题2 解析

// 隐式绑定,肯定是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

练习题3解析

// 隐式绑定
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 解析

// 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

相关参考资料

(1)https://mp.weixin.qq.com/s/hYm0JgBI25grNG_2sCRlTA

Over~ see you next time!
在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值