从零开始实现call

72 阅读8分钟

call apply bind

前要

初学javascript时,因为前面大一学过了c,c++,又自学了java,对面向对象对象编程深以为然,写代码无论三七二十一都是psvm,class,interface,这对自学前端的我来说是一件挺让人崩溃的事情,因为有时候我不知道为什么要有一些奇奇怪怪的方法api,只感觉难以理解,看不懂,现在我知道为什么了,因为在我的旧的思维里它是破坏了封装性,继承性的,这样的代码在java里面是在无理取闹,但是在javascript中则不然,js本身是一门弱类型语言,它不强求封装,继承,我们有时候就是需要写出像变量提升,回调,call/apply,未定义的变量等等代码,因为它很方便,简短的代码就能实现复杂的功能(虽然确实不好懂)。

就比如本次文章要说的call,apply,我第一次了解它们的时候我是很费解的。首先它们本身不是语义性很强的方法,其次我不明白为什么在这个函数(实际上也是一个对象)里面,call另一个函数(对象),然后莫名其妙的就能执行成功了,明明函数里面执行的代码的某些变量是undefined却能够正确的执行。

因此本次文章我也是想分享一下它们的作用和使用方法和原理,以及简单实现一下它们。

前置知识

本来只是做一些前置知识,结果越做越多,因为它们都是一个依赖另一个的原理,所以干脆把我作为一个学完java之后自学js学到的知识的总结分享在前面

prorotype与object

js也是有类似java的所有子类的父类,而在js中,我们把类型称为原型prototype,而这个父类也是叫做Object,和java是一样的。

function

function其实它就和java里面的calss基本上是一样的。看完下面这段代码在和java对比一下就懂了。

function func() {  //这也就是声明一个函数,也是声明一个类
  this.age = 18;
  console.log(this.age);
}

func();
const funcObj = new func();   //实例化,funcObj是指向了一个对对象的引用
console.log(`func is ${typeof func} but funcObj is ${typeof funcObj}`);

// 结果
// 18
// 18
// func is function but funcObj is object

由此我们可以得出结论,普通函数和构造函数的声明是一样的,但是使用的时候不一样,但在类编程又有点不同,这里不细说了。

propotype与__proto__

分开理解,首先prototype(如果想要是想要弄懂原型与原型链的小伙伴,接下来的内容得细细看,好好琢磨哦)

prototype是函数特有的属性,至于为什么可以看阮一峰大佬的文章Javascript继承机制的设计思想,我也从中收获良多。我们可以这样看,它就类似与java声明了一个类,那么将这个类实例化后就得到了一个对象。而我们知道,这个对象的类型是么?很明显就是这个类。而在js中,这个类型就是prototype,(因为js起初想要c++那种效果,又想简化,就做成这个样子了,确实不太好理解)例如下面的代码

public class Type {
//这就是js声明一个函数
    String name="";
    //函数里面this.name=name
    public Type(String name){
        this.name=name;
    }

    @Override
    public String toString() {
        return this.name.toString();
    }

    public static void main(String[] args) {
        Type type=new Type("father");
        //将函数实例化,js也是new 操作
        
        System.out.println(type.getClass());  //class type
         //getClass实际上与type.prototype是一样的
         
        System.out.println(type.toString());  //father
       
    }
}

再看js代码,这里选用类编程的写法,方便理解

class Father {
  constructor(name) {
    this.name = name;
  }
}

const person1 = new Father("p1");
console.log(Father.prototype);
console.log(person1.name);

其次就是关于继承,当初js也想要有继承这个功能,所以就基于prototype实现了继承,其实也很简单

声明函数A,prototype不是有A函数本身的所有属性吗?那我另一个函数B想要继承A是不是直接继承A的prototype的内容就可以了?所以就得到了继承的概念,也就是说prototype是一个链状向上连接的,直到最顶上的Object,因此我们当使用B.name时,如果B函数没有,那么就会往prototype链上去寻找,知道找到,否则输出undifined。

根据上面,我们又得到另一个东西,依然是链式的,那么得有链吧?从而有了__proto__(这在新的规范中删除了,但是我们还是了解一下)。相较于prototype而言,__proto__更加难以理解,这也是它被删除的原因,不好规范,用处也可以被替代了,

这是一个看似简单实际上却很绕的概念,但是基于上面的代码和解释,我们能用类似java的方式更简单的理解它。

我们声明的任何函数,可以被看做是一个类class, prototype就是每一个类都有的类型,在js中任何函数(类)身上都有一个属性他就是prototype(java我没有深入学习了解),而__proto__类似与java需要获取一个对象的类型会用getClass方法,__proto__也是一样的,只不过不同的是它内部是会做一个向上查找

关于__proto__,推荐一篇文章(由于太过复杂,篇幅有限,不细说了)Why系列: A.proto.proto.proto === null - 掘金 (juejin.cn)

总的来说,__proto__一般情况指向的是该对象的构造函数的prototype(一个指针!有指向)

this指针

再来说this指针,它是一个指向一个作用域的指针,像function(){...//执行的代码},这里括弧{}里面就是一个作用域,如果this指向了这里,那么通过this.attribute将会在这个作用域去寻找,找不到的话就会返回undefind。

关于this最值得记住的一句话:this永远指向最后调用它的对象 this永远指向最后调用它的对象 this永远指向最后调用它的对象,重要的话说三遍

有以下几个注意点

  1. 在全局作用域内,不是严格模式的时候'use strict',this->window,验证上面的三句话
  2. 箭头函数体内的this对象,就是定义该函数时所在的作用域指向的对象
  3. 构造函数,对,就是const func=new function(){...this...}里面的this会转为指向调用它的func,还是那句话,最后是func调用它的
  4. 在对象的方法调用中,其内部的this指向调用的对象本身,显然,是对象在调用this
  5. 在事件处理中,其内部的this指向产生这个事件源的对象 ,是事件在调用this
  6. 在定时器中其内部的this指针指向window对象,这是因为定时器这个macrotask在window环境下运行。
  7. 还有就是在call,apply,bind里面,我们慢慢说。

作用

关于call,apply,bind的作用有很多种说法,但实际上都是一个意思

  • 可以编写能够在不同对象上使用的方法
  • 改变函数执行时的上下文
  • 使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数

如果你像我一样也是从java过来第一次看到,可能也会很懵,我用大白话来解释一下 也就是一个函数A将他要执行的代码放到另一个被call的对象B内部去执行,也就是去到了被call的B的作用域下执行,可以说是将A内this的指向改为B内部作用域

使用

使用很简单,结合案例展示分析

call的案例与分析

let obj = {
  name: "p2",
  age: 20,
};

function Person1() {
  this.name = "p1";
  this.age = 18;
}
const person1 = new Person1();

function useCall(args) {
  console.log( `person name is ${this.name} and person age is 
        ${this.age} and he/she is ${args}`
  );
}

useCall.call(person1, "taeacher"); //person name is p1 and person age is 18 and he/she is taeacher
useCall.call(obj, "taeacher"); //person name is p2 and person age is 20 and he/she is taeacher

可以看到call的使用就是 function身上的call(obj,arg1,arg2,arg3...)从而将本函数usercall的this指向分别改为obj和person1身上去了。

而apply的不同之处仅在不是多个参数arg,而是一个数组args[],即apply(obj,args[])

bind改动更为特殊,使用和call一样,但是bind可以多次传参,而且改变this指向后不会立即执行,而是返回一个永久改变this指向的函数。

const bindFn = fn.bind(obj); // this 也会变成传入的obj ,bind不是立即执行
bindFn(1,2) // this指向obj
fn(1,2) // this指向window

实现

call的实现

Function.prototype._call = function (param) {
  var args = [...arguments].slice(1);
  //   将除了第一个参数以外的所有参数取出来
  param.fn = this;
  //   改变this指向
  const result = param.fn(...args);
  //   再在这里解构参数
  delete param.fn;
  //   删除fn
  return result;
};

let obj = {
  age: 18,
  sex: "男",
};

function myCall(arg) {
  console.log(this.age + "  " + this.sex + [...arguments]);
}

myCall._call(obj, 2, 3, 4);

而apply的实现基本和call一样,传入数组处理一下就可

bind的实现(比较复杂,要求比较多,可以多次传参,还要返回一个永久改变this的函数)

Function.prototype._bind = function (param) {
    // 判断调用对象是否为函数,不是抛出类型异常
    if (typeof this !== "function") {
        throw new TypeError("Error");
    }

    // 获取参数
    const args = [...arguments].slice(1),
    fn = this;
    // 获取this

    // 返回一个新的函数
    return function Fn() {
        // 需要判断this的类型,如果是Fn传入新的函数,不是则传param执行
        return fn.apply(this instanceof Fn ? new fn(...arguments) : param, args.concat(...arguments)); 
    }
}

总结

这也是心血来潮的一期,写一写我对call...的理解实现,结果写的有点多,还望读者大大们喜欢,有什么不对或者不好的地方还望指出。

结语

本次的文章到这里就结束啦!♥♥♥读者大大们认为写的不错的话点个赞再走哦 ♥♥♥ 我们一起学习!一起进步!