深度解析 JavaScript 中call方法原理

一丶深度解析 JavaScript 中call方法原理

call 方法是 Function 类原型上的一个公共方法,我们知道所有的函数都是 Function 类的一个实例(对象),当然,call函数本身也是Function类的一个实例。
任何一个函数都可以调用call方法,包括call方法本身。函数调用call方法时,是把函数作为Function的实例也就是对象身份的。

function fn1() {}
console.log(Function.prototype.call.__proto__ === Function.prototype);//true
console.log(fn1.mycall === fn1.__proto__.mycall);//true
console.log(fn1.mycall === Function.prototype.mycall);//true

  1. call的产生背景
    在JavaScript中,call和apply作用是一样的,都是为了改变某个函数运行时的上下文(context)而存在的,换句话说,就是为了改变函数体内部this关键字的指向而产生的。那么为什么要改变函数体中的this指向呢?让我们看看下面这个需求吧。
//类Person的原型上有公共方法say,那么类的实例p1就可以直接调用该方法
function Person(name) {
    this.name = name;
};
Person.prototype = {
    say: function () {
        console.log('hello', this.name);
    }
};
var p1 = new Person('king');
p1.say();//输出 hello king

当另外一个对象想使用Person中的say方法时不想重新写,使用call和apply可以实现“劫持”别人的方法。

//对象other跟类Person没有任何关系,那么如何让other直接调用Person原型上的方法呢?
function Person(name) {
    this.name = name;
};
Person.prototype = {
    say: function () {
        console.log('hello', this.name);
    }
};
var other = {
    name: 'other'
};
var p1 = new Person('king');
p1.say();//输出 hello king
p1.say.call(other);//输出 hello other
 - call方法的定义
  • call方法的定义
    call方法使用一个指定的this值和单独给出的一个或多个参数来调用一个函数。
    语法:fun.call(thisArg, arg1, arg2, …)
    作用:让函数 fun 执行,执行时,函数体中的 this 使用指定的参数thisArg。
    参数: thisArg 在 fun 函数运行时,fun函数体中的 this 值 。
    参数: arg1, arg2, … 指定的参数列表。
    A.call(B,x,y);让函数call执行,函数call执行,主要有两个作用

  • 改变函数A函数体中的this指向,使之指向B,也就是this =B;

  • 让A函数执行,把第二个及以后接受的参数值,传递给A函数,也就是A(x,y);

var obj = {
name: 'king'
}
function fn(a, b) {
console.log(a + b);
console.log(this.name);
}
fn.call(obj, 1, 2);//输出 3  king

  1. call方法源码分析
    我们无法看到call的真正源码,只能模拟一下,但是模拟的跟真正call还是有一点差别,我们来看看差别吧。
//网上ES6版本的call源码
Function.prototype.es6Call = function A(context) {
var context = context || window;
context.fn = this;
let args =[...arguments].slice(1);//类数组转为数组
const result = context.fn(...args);
delete context.fn;
return result;
}

通过下面这个例子,我们看看模拟的call和真正浏览器中的call的区别吧。

let fn = function (a, b) {
console.log(a + b);
console.log(this);
};
let obj = {}
fn.es6Call(obj, 10, 20);//网上ES6版本的call源码
//输出 30
//输出 {fn: ƒ}

fn.call(obj, 10, 20);//使用浏览器的内置call调用
//输出 30
//输出 {}

分析其原因不难发现,根据函数es6Call执行后,函数fn被强制挂载到对象obj上;而浏览器内置版,输出的对象obj仍然是空的,并没有挂载函数fn。而且,往更深的层次去思考,call绝不是这样(或类似这样)实现的,call是底层语言为浏览器封装好的函数,我们使用JS确实不好模拟。虽然es6Call输出与浏览器内置的call有一点差异,但是并不影响我们学习,因为es6Call输出的结果仍然是正确的(正确输出a+b=30,以及this为对象obj,只是为obj多挂载了一个属性而已,而且该属性在es6Call方法执行完毕后已经删除了)。所以,网上模拟的CALL方法依然很优秀,而且输出结果是正确的,值得我们学习。
#### 4、call方法体中做了哪些事情
我们通过call的定义,以及网上ES6b版本的call源码,可以分析出call方法体中到底做了什么事情,虽然不能用代码实现成一模一样的,但是可以用文字描述成一模一样的。
```javascript
//以此例分析浏览器内置call函数体中做了哪些事情
let fn = function (a, b) {
    console.log(a + b);
    console.log(this);
};
let obj = {}
fn.call(obj, 10, 20);
------------------------------------------
//我们假设浏览器内置的call方法,也就是匿名函数的名字为A;
Function.prototype.call =function A(obj){
    //=>1.A函数体中的THIS为函数fn;(因为是fn.call函数执行,call方法前面有".",所以执行主体就是fn);
    //=>2.把THIS(也就是函数fn)函数体中的"this关键字"修改为第一个参数值,也就是对象obj;
    //=>3.把THIS(也就是函数fn)执行,把第二个及以后接受的参数值,传递给THIS(10,20),也就是fn(10,20);
}

5、理解call方法体中的this
通过第4节,我们知道浏览器内置的call方法一共做了三件事
1、明确this是谁,this是谁是由方法执行时决定的,call方法执行,函数中的this取决于执行的主体,谁 执行的,this就是谁(执行主体:方法执行,看方法名前面是否有".",有的话, 前面是谁this就是谁,没 有this就是window)。
2、this一定是个函数,把this函数体中的"this关键字"修改为第一个参数值(假设为obj)。
3、把this函数执行,并且把第二个及以后接受的参数值,传递给this函数(比如this(x,y,z))。
通过分析,我们知道,关键是搞定call方法体中的this,我们来看个例子,来观察this。

//理解call方法体中的this
Function.prototype.mycall = function A(context) {
    console.log(this === fn1);
    console.log(this === obj.f1);
    console.log(this);
    //...方法体中的具体实现
};
var fn1 = function fn() { console.log(1); }
function fn2() { console.log(2); }
obj = { f1: fn1 };
obj.f1.mycall(fn2);//输出 true true --- ƒ fn() {console.log(1);}
//第1步:明确this
//按照我们对于this的理解("."),this应该为 obj.f1
//通过输出,我们才知道,this是obj.f1(对象的属性f1),也是变量fn1
//更是变量fn1或者obj.f1(对象的属性f1) 所指向的堆内存所存储的内容也就是函数fn
//其实this也是一个变量,this的值指向函数fn而已
//所以this = fn1 = obj.f1比较的是它们的值是否相等,它们的值都是fn,所以相等
//我们知道call函数中的,下一步就是让this执行
//那么,this执行到底是fn()还是fn1()还是obj.f1();
//答案是让this的值执行,也就是fn();
//this() ===fn() ==fn1() !=obj.f1();
---------------------------------------------------
//第2步:把this函数体中的"this关键字"修改为第一个参数值(也就是fn2)。
//根据网上的ES6b版本的call源码,把this函数挂载到fn2上面即可
//fn2.f=this =fn;
---------------------------------------------------
//第3步:把this函数执行,并且把第二个及以后接受的参数值,传递给this函数(比如this(x,y,z))。
//fn2.f();即可,f()执行,前面有".",所以f函数体中的this就是fn2啦。

二、call方法的应用

通过第一部分的讲解,我们已经理解了call方法体中到底干了什么,以及call方法体中的this,那么,让我们看个测试题目,加深一下对call的理解吧。

function fn1() {
    console.log(1);
}
function fn2() {
    console.log(2);
}
fn1.call(fn2);
fn1.call.call(fn2);
Function.prototype.call(fn1);
Function.prototype.call.call(fn1);

根据call方法体中做了哪些事情,我们来分析输出结果,一个一个分析即可

fn1.call(fn2);
//解释1--ES6版call源码 方法call执行
//第1步:明确call方法体中的this
//call方法执行,call方法前面有".",所以执行主体就是fn1,那么this就是fn1的值还是函数fn1
//第2步:让this函数体的'this'指向第一个传递的参数fn2;
//fn2.f1=this=fn1
//第3步:让fn2.f1执行
//fn2.f1();===>输出1
---------------------------------------------------
//解释2--浏览器内置call方法原理 方法call执行
//第1步:明确call方法体中的this;
//call方法执行,call方法前面有".",所以执行主体就是fn1,那么this就是fn1的值还是函数fn1;
//第2步:让this函数体中的'this'指向第一个传递的参数fn2;
//第3步:this函数执行,也就是fn1执行(并且让函数体中的this为fn2),所以输出1
fn1.call.call(fn2);
//解释1--ES6版call源码 方法call执行
//第1步:明确call方法体中的this
//call方法执行,call方法前面有".",所以执行主体就是'fn1.call',那么this就是fn1.call的值;
//fn1.call的值为call函数本身;
//第2步:让this函数体的'this'指向第一个传递的参数fn2;
//fn2.f = this=call函数本身;
//第3步:让fn2.f执行
//fn2.f();也就是call方法再次执行,并且执行主体为fn2
----------------------------------
//第1步:明确call方法体中的this
//f方法执行,f方法前面有".",所以执行主体就是'fn2',那么this就是fn2的值,也就是fn2;
//第2步:让this函数体的'this'指向第一个传递的参数,没有传递参数,所以为window
//window.f = this=fn2;
//第3步:让window.f执行,也就是fn2执行,所以输出 2
---------------------------------------------------
//解释2--浏览器内置call方法原理 方法call执行   
//第1步:明确call方法体中的this;
//call方法执行,call方法前面有".",所以执行主体就是'fn1.call',那么this就是fn1.call的值;
//fn1.call 通过作用域链查找,fn1.call就是call函数本身;所以,this就是call函数
//第2步:让this函数体的'this'指向第一个传递的参数fn2;
//第3步:this函数执行,this()===call();call函数再次执行,并且函数体的this为fn2
----------------------------------
//第1步:明确call方法体中的this,已经知道函数体的this为fn2;
//第2步:让this函数体中的'this'指向第一个传递的参数,没有传递参数,所以为window;
//第3步: this函数执行,也就是fn2();所以输出 2
Function.prototype.call(fn1);
//解释1--ES6版call源码 方法call执行
//第1步:明确call方法体中的this
//call方法前面有".",所以执行主体就是Function.prototype,那么this就是Function.prototype的值;
//Function.prototype的值为一个空的匿名函数;
//第2步:让this函数体的'this'指向第一个传递的参数fn1;
//fn1.f =this =Function.prototype;
//第3步:让fn1.f执行,fn1.f();因为是匿名函数,所以没有任何输出
---------------------------------------------------
//解释2--浏览器内置call方法原理 方法call执行
//第1步:明确call方法体中的this;
//call方法前面有".",所以执行主体就是Function.prototype,那么this就是Function.prototype的值;
//Function.prototype的值为一个空的匿名函数;
//第2步:让this函数体的'this'指向第一个传递的参数fn1;
//第3步:this函数执行,this(),也就是空的匿名函数执行,所以没有任何输出
Function.prototype.call.call(fn1);
//解释1--ES6版call源码 方法call执行
//第1步:明确call方法体中的this
//call方法前面有".",所以执行主体就是Function.prototype.call;
//那么this就是Function.prototype.call的值;该值认为方法call本身;
//第2步:让this函数体的'this'指向第一个传递的参数fn1;
//fn1.f =this=call函数本身;
//第3步:让fn1.f执行,fn2.f();也就是call方法再次执行,并且执行主体为fn2
----------------------------------
//第1步:明确call方法体中的this
//fn1.f();f方法前面有".",所以执行主体就是'fn1',那么this就是fn1的值,也就是fn1;
//第2步:让this函数体的'this'指向第一个传递的参数,没有传递参数,所以为window
//window.f = this=fn1;
//第3步:让window.f执行,所以输出 1

---------------------------------------------------
//解释2--浏览器内置call方法原理 方法call执行
//第1步:明确call方法体中的this;
//call方法前面有".",所以执行主体就是Function.prototype.call;
//那么this就是Function.prototype.call的值;该值就是方法call本身;
//第2步:让this函数体的'this'指向第一个传递的参数fn1;
//第3步:this函数执行,this()===call();call函数再次执行,并且函数体的this为fn1
----------------------------------
//第1步:明确call方法体中的this,已经明确为fn1;
//第2步:让this函数体的'this'指向第一个传递的参数,没有传递参数,所以为window
//第3步:this函数执行,也就是fn1执行,所以输出 1

理解了call,再看看这个例子吧,因为分析方法与上面一样,就不再分析,直接给出结论即可

var obj = {
    name: 'king'
}
function fn() {
    console.log(this);
    console.log(this.name);
}
fn.call(obj);//输出 this为obj的值{ name: 'king' },this.name为 king
Function.prototype.call.call(fn, obj); //输出 this为obj的值,this.name为 king
//A.call.call(B,obj); 其实是让函数B执行,并且B函数体中的'this关键字'指向obj。

Function.prototype.call.call(fn, obj);执行过程,我们画张图看看其中的执行过程吧,以es6Call版本的call源码

Function.prototype.es6Call = function A(context) {
    var context = context || window;
    context.fn = this;
    let args = [...arguments].slice(1); //类数组转为数组
    const result = context.fn(...args);
    delete context.fn;
    return result;
}
var obj = {
    name: 'king'
}
function fn() {
    console.log(this);
}
Function.prototype.es6Call.es6Call(fn, obj);//输出 this为obj的值,this.name为 king

在这里插入图片描述

三、bind方法的应用

bind()方法主要就是将函数绑定到某个对象,bind()会创建一个函数,函数体内的this对象的值会被绑定到传入bind()第一个参数的值,例如,f.bind(obj),实际上可以理解为obj.f(),这时,f函数体内的this自然指向的是obj。

var a = {
    b : function(){
        var func = function(){
            console.log(this.c);
        }.bind(this);
        func();
    },
    c : 'Hello!'
}
a.b(); //Hello!
--------------------
var a = {
    b : function(){
        var func = function(){
            console.log(this.c);
        }
        func.bind(this)();
    },
    c : 'Hello!'
}
a.b();//Hello!
function Bar() {
    this.name = 'king';
}
Bar.prototype.f = function () {
    console.log(this.name);
}
let bar = new Bar();

function test(f) {
    f();
}
test(bar.f);//输出 undefined
function Bar() {
    this.name = 'king';
}
Bar.prototype.f = function () {
    console.log(this.name);
}
let bar = new Bar();

function test(f) {
    f();
}
test(bar.f.bind(bar));//输出 king

四、apply方法的应用

apply 方法与 call 的原理是一样的,只是 apply 传递的参数是数组而已。
const { EventEmitter } = require("events");
let myEvent = new EventEmitter();
myEvent.on("BCD", (data) => {
  console.log("345");
  console.log(data);
});
myEvent.on("ABC", (data) => {
  console.log("123");
  console.log(data);
});
myEvent.emit.apply(myEvent, ["ABC", "BCD"]);
//输出 123 BCD
  • 3
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值