搞懂this,如此简单

245 阅读10分钟

在没有学习this之前,总觉得它是特别复杂的机制,但是为什么要有this?它到底指的是什么呢,有什么作用呢?其实很简单,让我们花几分钟直接搞定它。

一、为什么要有this

首先,我们先举个简单的例子,分析下面这串代码。

function identify(context){
    return context.name.toUpperCase()
}

function speak(context){
    var gretting = "hello, I am "+context.name
    console.log(gretting);
}

var me = {
    name:"Tom"
}

speak(identify(me)); // 输出“hello,I am TOM”

在运行结果中,我们可以得到想要的输出的结果,但是你会发现需要传的参数在到处显示传递。假如我们写了20个或者30个函数显示传递,会让代码显得非常冗杂。那我们可以怎么优化呢?继续看下面的代码:

function identify(){
    return this.name.toUpperCase()
}

function speak(){
    // identify.call(me) --> 让identify中的this指向me
    var gretting = "hello, i am "+identify.call(me)
    console.log(gretting);
}

var me = {
    name:"Tom"
}

speak(); //输出“hello,I am TOM”

代码中的identify()函数没有显示传递参数me,仿佛是使用了this代替了me。可是,为什么this写在identify()函数的,却可以代替me呢?

仔细看代码第七行,我们在调用函数时,写的是identity.call(me)。大胆的猜测一下,应该是调用了这个call(),才导致identify()函数中的this指向了me,确保了结果的输出。到这里,我们先不管this为什么指向me,我们只这代码的优化来看,就是this可以让函数引用到me对象,才能访问me的name属性,最后才有结果输出。

这我们一下就知道了,原来this存在的意义是可以让函数自动引用合适的上下文对象。

二、this指的是什么

在英语中我们知道,this是一个代词,而在js中永远代指某一个域,且this只存在于域中才有意义。那this到底是什么呢?首先我们看在js文件中,直接打印this会得到什么。

image.png

会看见this是一个空对象。但是如果在函数创建一个对象,并在让其独立执行,在函数中打印this会指向什么呢?会是函数内创建的对象吗?

function foo(){
    let person = {
        name:"美玲",
        age:18
    }
    // 写在了函数作用域中,是foo的this
    console.log(this);
}
foo();  // 得到global对象,全局

由此,我们可以知道,在全局中this代指一个空对象global,全局作用域。

还有一个疑问:同样的代码,我们在浏览器中执行会是同样的结果呢?

image.png

我复制了代码去尝试了,上面打印的结果很明显,在浏览器中this指向的是window对象。

三、this的绑定规则

在知道了this在全局中指向的是什么,我们就来搞清楚,在不同的环境下this又指向了谁?也可以称之为this绑定了谁。

1. 默认绑定

所谓默认绑定是什么呢?做个猜想:我们另外写了一个bar()函数,并在其中调用foo()函数,打印的结果会不会是foo呢?

function foo(){
    let person = {
        name:"美玲",
        age:18
    }
    // 写在了函数作用域中,是foo的this
    console.log(this);
 }
 
 function bar(){
    let person = {
        name:"墩墩",
        age:1
    }
    foo();  // 调用foo()
}
// 函数的独立调用;非独立调用 XXX.foo()
bar();  // 结果是global

我们运行代码,结果意料之外,没想到this还是指向的全局。我们仔细分析代码发现,函数在运行过程中都是独立调用,并没有被其他对象调用,所以官方定义的只要函数是独立调用,this指向window。这就有人要问了,什么是非独立调用呢?不着急~我们继续探讨。

2. 隐式绑定

除了直接调用函数,我们还能使函数作为对象的方法被调用,这就是函数非独立调用的一种,叫做方法调用。那方法调用中的this又指向什么?

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

const obj  = {
    a:1,
    // key:value  右边是种数据结构; foo是引用,不是调用,foo()调用
    // foo:foo
    foo: function foo(){
        console.log(this);
    }
}
// obj 读取了key,得到一个值是函数体,再写一个()就是调用。
// 方法调用
obj.foo() // 输出{ a: 1, foo: [Function: foo] }

我们定义的obj对象中的foo属性的值是foo函数对象,当obj访问foo属性并调用函数体时,foo()的this指向调用它的obj对象。所以我们可以知道:this的指向,我们不care到底this在哪里,只看函数是怎么调用的当函数的引用有上下文对象时(当函数被某一个对象所拥有且调用时),this指向该上下文对象,这就是隐式绑定。

小tips:对象内部的结构是{key:value},左边是指针名字随便取;右边是一个值,需要在内存中保存的。所以代码中的obj的foo属性值除了写成函数体的样子,也可以简写成foo:foo,右边的foo是引用,非函数的调用:foo()。

3. 隐式丢失

其实函数作为方法被调用时,不止会有一个,有可能会存在多个对象调用的情况。

function foo(){
    console.log(this.a);
}
const obj  = {
    a:1,
    foo: foo
}
const obj2 = {
    a:2,
    obj:obj
}
// 就近原则
obj2.obj.foo(); // 返回1

上述代码中可以得到,对象obj和obj2中均有a属性,而foo的调用也是非独立调用。其实我们看也觉得foo可以说是被obj调用了,也好像可以说是被obj2调用了,那它到底绑定obj还是obj2呢?打印结果告诉我们,this.a = obj.a,所以this绑定的是离foo最近的obj对象。而这我们就称为隐式丢失,即 当函数的引用有一连串上下文对象,this指向最近的那个对象

4. 显示绑定

顾名思义,显示绑定就是我们可以看见的人为绑定this的过程。怎么控制的呢?

function fn(x,y){
    console.log(this.a,x+y);
}
var obj = {
    a:1
}

// call 把this指向直接指向obj; call是个函数体,call在调用
fn.call(obj,2,3);  // 1,5

fn.apply(obj,[2,3]);  // 1,5

let bar = fn.bind(obj,2); 
bar(3,4)  // 1,5

其实官方已经帮我们定义了三个函数,可以将this的绑定修改:call apply bind ,显示的将函数的this绑定到一个对象上。运行上面代码,你可以发现得到的结果全部是this.a == obj.a == 1 ,x+y= 5。那官方肯定不会吃饱了让三个函数一模一样。

我们直接看三个函数的区别:

  1. call() ,fn函数可以直接调用,直接传入需要绑定的对象obj,以及函数所需要的参数;
  2. apply(),与call类似,apply与call区别就是接收参数类型不一样,接收的参数需要写成数组形式
  3. bind(), bind与call区别比较大,一个是bind()运行后一定会返回一个函数体,所以我们调用时要定义一个函数体进行赋值;二是可以发现我在调用bind时我并没有直接传递两个参数,而是在定义的函数体中继续传递参数,但是结果是一样的,所以函数调用bind后,会先在bind里面找需要的参数,再去另外定义的函数体bar上找。

5. new 绑定

有关注我的可以知道我前面写了一篇文章,关于js中的原型的知识点:

《面试官:是不是所有对象都有隐式原型?》juejin.cn/post/743926…

有兴趣你们可以点开看看,其中关于new的解读,我写的时候是还没学习this的指向问题,所以我直接就解读成了创建了一个this对象。

但是this作为关键词不可以被创建成新的变量了,而为什么要在这儿解读生成this对象呢?首先我们要知道,当函数内部存在return,且返回的是一个引用类型的数据时,new 的执行结果就是这个引用数据类型。现在我们在手写一个构造函数Person()的代码如下:

function Person(){
    this.name = "美玲";
    this.age = 18;
}
let p = new Person();
console.log(p); // Person {name:"美玲", age:18 }

可以从返回的结果发现,Person()中的this按道理应该是指向的全局,但在new的过程中,this指向实例对象p,所以才能使得构造函数调用时将name和age属性添加进对象p中。那我们已经知道显示绑定可以改变this的指向,不难得出new的过程中使用了Person.call() 的操作。

new的原理:

  1. 创建一个空对象obj
  2. 让构造函数的this指向创建的对象obj
  3. 构造函数内代码正常执行
  4. obj对象的隐式原型等于构造函数的显示原型(obj.__ proto__ = Fn.prototype)
  5. return 对象obj

6. 箭头函数

箭头函数我们并不陌生,但是它的作用域的this指向谁呢?

let Foo = () => {
    this.name = '美美'
}
// 有问题的代码
// let foo = new Foo()

分析这段代码,第五行为什么是错误代码?因为new的过程中需要显示绑定this指向对象,但是箭头函数里没有this,写在了箭头函数中的this那也是它外层的非箭头函数的,所以第五行错误。因此,箭头函数不能作为构造函数使用

那我通过一段代码,测试一下你学的如何?

function foo(){
    let bar = function(){
        // this 是bar的
        let baz = () =>{
          console.log(this)
        }
        baz()
    }
    bar()
}
foo()  // global

其实不难喔,层层分析,因为this在箭头函数不存在,所以this是外层的bar()函数,而bar又被独立调用,所以this指向全局global。

四、手写call

在this的显示绑定规则中我们提到了call函数的调用可以手动修改this的指向,那它是如何实现的呢?让我们一起来分析一下。

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

var obj = {
    a:1
}
// call 把this指向直接指向obj
foo.call(obj,2,3);
  1. call方法能够被foo调用,那一定是写在构造函数Function的显示原型中;
  2. foo也是一个函数对象,所以foo.call()中,call()是方法调用,call的this指向foo函数对象;
  3. foo在call中是非独立调用,让obj调用它,让foo的this指向obj;
  4. foo调用时传递的参数call并不清楚有几个,所以call的参数写成...args的形式;
  5. 如果foo()是有返回值的,call的调用还是会返回foo的结果,所以我们在call中也要写返回值。

根据上面几点分析,我写的myCall函数代码如下:

 Function.prototype.myCall = function (...args){
    // 获取参数
    const context = args[0];
    const arg = args.slice(1);  // 从第一个开始切 [2,3,4]

    // 将foo引用到obj上
    context.fn = this

    // 让obj 调用foo  参数传递给foo,用...arg 结构出来参数
    // 如果foo有return呢? 调用foo会有执行结果
    const res = context.fn(...arg)

    // 移除obj上的foo
    delete context.fn
    return res
  }
  let res2 = foo.myCall(obj,2,3,4) // myCall被foo调用,myCall的this指向foo
  console.log(res2)

以上就是我今天要分享的this有关的知识点啦~如果有不对的地方欢迎来评论区指正喔!

期待下次与你分享~

期待.gif