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永远指向最后调用它的对象,重要的话说三遍
有以下几个注意点
- 在全局作用域内,不是严格模式的时候
'use strict',this->window,验证上面的三句话 - 箭头函数体内的
this对象,就是定义该函数时所在的作用域指向的对象 - 构造函数,对,就是
const func=new function(){...this...}里面的this会转为指向调用它的func,还是那句话,最后是func调用它的 - 在对象的方法调用中,其内部的this指向调用的对象本身,显然,是对象在调用this
- 在事件处理中,其内部的this指向产生这个事件源的对象 ,是事件在调用this
- 在定时器中其内部的this指针指向window对象,这是因为定时器这个macrotask在window环境下运行。
- 还有就是在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...的理解实现,结果写的有点多,还望读者大大们喜欢,有什么不对或者不好的地方还望指出。
结语
本次的文章到这里就结束啦!♥♥♥读者大大们认为写的不错的话点个赞再走哦 ♥♥♥ 我们一起学习!一起进步!