CALL的原理解析

首先来来了解一下如何获取除第一个参数以外其它参数:以下两种方法都是利用ES6的规则

//1.方法一:利用剩余运算符
Function.prototype.call = function call(context, ...params) {context == null ? context = window : null;//params就是除了第一个参数外剩余其它参数组成的数组
}
//2.方法二:利用ES6的新方法 Array.form()
Function.prototype.call = function call(context) {context == null ? context = window : null;let params = Array.from(arguments).slice(1);
}

func通过原型链找到Function.prototype上的call方法,并且执行call方法,在call执行的时候,将this指向传递的第一个参数obj【context】,将参数传递给func并且执行func。
在非严格模式下:如果第一个参数(不传/传undefined/传null),context都是window
自己实现call方法思路:

  1. call执行的时候将this指向obj,参数传递给函数func并且执行func
  2. this指向obj:只要按照成员访问这种方式执行,就可以让FUNC中的THIS变为OBJ【前提OBJ中需要有FUNC这个属性】,当然属性名不一定是FUNC,只要属性值是这个函数即可
obj.func();//func执行时this指向obj  成员访问
obj.$$xxx = func;//给obj添加一个属性,属性值是func
obj.$$xxx(10,20);//这个属性调用的时候,就是func执行的时候,func中的this指向obj

所以实现call方法利用成员访问的方法,给obj这个对象添加一个属性,这个属性名可以随便取,但是属性值必须是func这个函数,这个obj.func()时,func中的this就指向了obj。在obj.func执行的同时传递参数,obj.$$xxx(10,20);

不考虑context是基本类型值时的call封装
  1. null == undefined --> true
  2. 【非严格模式下】context不传或者传递NULL/undefined都让this最后改变为window
  3. call实现的核心原理:给CONTEXT设置一个属性(属性名尽可能保持唯一,避免我们自己设置的属性修改默认对象中的结构,例如可以基于Symbol实现,也可以创建一个时间戳名字),属性值一定是我们要执行的函数(也就是THIS,CALL中的THIS就是我们要操作的这个函数);接下来基于CONTEXT.XXX()成员访问执行方法,就可以把函数执行,并且改变里面的THIS(还可以把PARAMS中的信息传递给这个函数即可);都处理完了,别忘记把给CONTEXT设置的这个属性删除掉(人家之前没有这个属性,现在你自己加了,使用完之后我们需要把它删了)
  4. 如果CONTEXT是基本类型值,默认是不能设置属性的,此时我们需要把这个基本类型值修改为它对应的引用类型值(也就是构造函数的结果)
Function.prototype.call = function call(context, ...params) {context == null ? context = window : null;let key = Symbol(''), //Symbol里可以随便写或者不写,这个值都是唯一的。result;//测试内置的call方法,发现call执行后返回的值是context函数执行后返回的值,call的返回结果取决于context函数执行后的返回结果context[key] = this;result = context[key](...params);//...展开运算符delete context[key];return result;}
let obj = {name: 'obj'
}
function func(x, y) {console.log(this, x, y);return x + y;
}
let res = func.call(obj, 10, 20);//{name: "obj", Symbol(): ƒ} 10 20
console.log(res);//30
//打印结果:
//未展开时:
{name: "obj", Symbol(): ƒ}
//展开结果如下:
name: "obj"
__proto__: Object

未展开时有之前添加的Symbol()属性,展开后发现Symbol()属性已经被删除了。一开始输出的时候func还没执行完,添加的Symbol()属性还没删除,但是展开后,谷歌浏览器的特点是显示最新的属性,所以此时就没有后来删除的Symbol()属性了。执行call的时候临时加一个属性,然后再删除,这样就不会影响了。

//当第一个参数传递基本类型值时报错
console.log(func.call(1, 10, 20));//Uncaught TypeError:context[key] is not a function
context是基本类型值时的call封装(Symbol/BigInt除外)

创建一个值的两种方法:

  1. 对于引用数据类型来讲,两种方式没啥区别;
  2. 对于值类型,字面量方式创建的是基本类型值,但是构造函数方式创造的是对象类型值;再但是,不管基本类型还是对象类型都是所属类的实例,都可以调用原型上的方法;(基本值无法给其设置属性,但是引用值是可以设置属性的)
//1. 字面量创建
let num1 = 10;
let obj1 = {};
new num1.constructor(num1);//将基本类型的值num1转化为对象类型//2. 构造函数创建
let num2 = new Number(10);
let obj2 = new Object();//控制台输出
num1;//=> 10
num2;//=> Number {10}
typeof num1;//=> "number"
typeof num2;//=> "object"//不管基本类型还是对象类型都是所属类的实例,都可以调用原型上的方法
/*
查看Number.prototype上的方法:
constructor: ƒ Number()
toExponential: ƒ toExponential()
toFixed: ƒ toFixed()
toLocaleString: ƒ toLocaleString()
toPrecision: ƒ toPrecision()
toString: ƒ toString()
valueOf: ƒ valueOf()
__proto__: Object
*/
//num1和num2都调用下Number.prototype上的toFixed:方法
num1.toFixed(2);//=>"10.00"
num2.toFixed(2);//=>"10.00"
//1.num1和num2都是Number的一个实例,都能调用Number.prototype上的方法,用的时候没有区别。num1.toFixed(2)时浏览器会默认把num1转化为对象类型然后再调用方法num1+10;//=>20
num2+10;//=>20
num2.valueOf();//=>10
//浏览器会自己帮我们把num2这个对象类型调用其原始值(valueOf),来进行数学运算
//num1和num2虽然用起来没什么区别,但是这是两种不同类型的值。基本类型的num1无法添加属性,
num1.xx=10
num1.xx;//=>undefined//想知道num1是谁的实例可以用 num1.constructor(num1顺着原型链__proto__可以找到它构造函数的原型上的constructor属性,这个constructor就指向这个构造函数即当前类)
num1.constructor;//=> ƒ Number() { [native code] }
//所以将num1转为对象类型可以
new num1.constructor(num1);//=>Number {10}
//相当于 new Number(num1);
  • 如果CONTEXT是基本类型值,默认是不能设置属性的,此时我们需要把这个基本类型值修改为它对应的引用类型值(也就是构造函数的结果)
  1. 不考虑context是Symbol和BigInt类型的基本数据类型时的写法:
Function.prototype.call = function call(context, ...params) {//【非严格模式下】不传或者传递NULL/UNDEFINED都让THIS最后改变为WINDOWcontext == undefined ? context = window : null;// CONTEXT不能是基本数据类型值,如果传递是值类型,我们需要把其变为对应类的对象类型if (!/^(object|function)$/.test(typeof context)) {context = new context.constructor(context);}let key = Symbol('KEY'),result;context[key] = this;result = context[key](...params);delete context[key];return result;
};
function func(x, y) {console.log(this, x, y);return x + y;
}
let res = func.call(100, 10, 20);
/*
Number {100, Symbol(KEY): ƒ} 10 20
//展开如下:展开的时候就没有属性Symbol(KEY)了
__proto__: Number
[[PrimitiveValue]]: 100
*/res = func.call(true, 10, 20);
//Boolean {true, Symbol(KEY): ƒ} 10 20res = func.call(Symbol('1'), 10, 20);
// Uncaught TypeError: Symbol is not a constructorres = func.call(BigInt('1'), 10, 20);
//Uncaught TypeError: BigInt is not a constructor at new Symbol (<anonymous>)
  1. 将Symbol和BigInt类型的基本数据类型转化为对象类型,如果也利用new Symbol(‘1’).constructor(Symbol(‘1’)),这种方法不可以
new Symbol('1').constructor(Symbol('1'))
// Uncaught TypeError: Symbol is not a constructornew BigInt('1').constructor(BigInt('1'))
//Uncaught TypeError: BigInt is not a constructor at new BigInt (<anonymous>)
  1. 直接new也不可以(new Symbol(‘1’).constructor(Symbol(‘1’))就是new Symbol(‘1’))
new Symbol('1');
//Uncaught TypeError: Symbol is not a constructor at new Symbol (<anonymous>)new BigInt('2');
//Uncaught TypeError: BigInt is not a constructor at new BigInt (<anonymous>)
  1. Symbol和BigInt直接添加属性也不可以
let a = Symbol('a');
a.xxx=10;
a.xxx;//=>undefined
dir(a);//=>Symbol(a)let b = BigInt('1');
b.xx=20;
b.xx;//=>undefined

首先看下原生的call方法中第一个参数传递Symbol和BigInt时的输出

let res = func.call(Symbol('2'), 10, 20);
/*
Symbol {Symbol(2)} 10 20
//展开如下:
description: "2"
__proto__: Symbol
[[PrimitiveValue]]: Symbol(2)
*/let res1 = func.call(BigInt('1'), 20, 30);
/*
BigInt {1n} 20 30
//展开如下:
__proto__: BigInt
[[PrimitiveValue]]: 1n
*/

最终找到的解决方案:将Symbol和BigInt类型的基本数据类型转化为对象类型要用Object(context);

Object(Symbol('2'));
/*
Symbol {Symbol(2)}
//展开如下:
description: "2"
__proto__: Symbol
[[PrimitiveValue]]: Symbol(2)
*/Object(BigInt('1'));
/*
BigInt {1n}
//展开如下:
__proto__: BigInt
[[PrimitiveValue]]: 1n
*/typeof BigInt('1');//=> "bigint"
typeof Symbol('2');//=> "symbol"let symbol = Object(Symbol('2')),bigint = Object(BigInt('1'));typeof symbol;//"object"
typeof bigint;//"object"
context是基本类型值时的call封装(包括全部的基本类型)

包括全部的基本类型:字符串、数字、布尔值、Symbol、BigInt(null/undefined时context为window)
下面是自己封装的call方法的最终完整版以及测试

Function.prototype.call = function call(context, ...params) {//【非严格模式下】不传或者传递NULL/UNDEFINED都让THIS最后改变为WINDOWcontext == undefined ? context = window : null;// CONTEXT不能是基本数据类型值,如果传递是值类型,我们需要把其变为对应类的对象类型if (!/^(object|function)$/.test(typeof context)) {if (/^(symbol|bigint)$/.test(typeof context)) {context = Object(context);} else {context = new context.constructor(context);}}let key = Symbol('KEY'),result;context[key] = this;result = context[key](...params);delete context[key];return result;
};let obj = {name: "obj"
};function func(x, y) {console.log(this);return x + y;
}
let res = func.call(Symbol('10'), 10, 20);
/*
Symbol {Symbol(10), Symbol(KEY): ƒ}
//展开如下:
description: "10"
__proto__: Symbol
[[PrimitiveValue]]: Symbol(10)
*/
console.log(res); //30res = func.call(100, 30, 20);
/*
Number {100, Symbol(KEY): ƒ}
//展开如下:
__proto__: Number
[[PrimitiveValue]]: 100
*/
console.log(res); //50res = func.call('hello', 30, 60);
/*
String {"hello", Symbol(KEY): ƒ}
//展开如下:
0: "h"
1: "e"
2: "l"
3: "l"
4: "o"
length: 5
__proto__: String
[[PrimitiveValue]]: "hello"
*/
console.log(res); //90func.call();//Window
func.call(null, 30, 60);//Window
利用call原理来解析面试题
  1. 阿里面试题
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方法来分析上面的面试题图解:

2. 根据要求的输出结果自己写change方法(实际考察自己实现call方法)

~function(){function change(){//=>实现你的代码};Function.prototype.change=change;
}();
let obj = {name:'Alibaba'};
function func(x,y){this.total=x+y;return this;
}
let res = func.change(obj,100,200);
//res => {name:'Alibaba',total:300}

分析上面习题:首先给对象增加属性和方法时

let obj = {name: 'cat',age: 1
}
//等价于下面的写法
obj.like = fish;
obj.hobby = eating;
//等价于下面的写法
obj['like'] = fish;
obj['hobby'] = eating;

执行obj的方法func时发生的事情

let obj = {name: 'cat',func() {this.age = 2;}
}
console.log(obj);//{name: "cat", func: ƒ}
obj.func();//func函数点前时obj,所以func函数执行的时候其中的this指obj,this.age=2 相当于 obj.age=2;
console.log(obj);//{name: "cat", age: 2, func: ƒ}

现在让我们回到此题上来:
首先我们来看要求的返回结果是【res => {name:‘Alibaba’,total:300}】,也就是说【func.change(obj,100,200);】这行代码中func调用change方法,在调用change方法的时候this指向obj,并且将除了obj之外的参数传递给函数func,并且执行func函数。实现这个原理其实就是给obj添加一个属性,将func作为属性值赋值给obj的这个属性即obj.xxx=func。这样obj.xx()调用的时候,xx即func中的this就指向obj了。这个实现原理与call是一致的,所以此题实际考察的就是自己实现call方法。

~function () {function change(context, ...args) {//this->funccontext = context == null ? window : context;if (!(/^(object|function)$/).test(typeof context)) {if ((/^(symbol|bigint)$/).test(typeof context)) {context = Object(context);} else {context = new context.constructor(context);}}let key = Symbol('key'),result;context[key] = this;result = context[key](...args);delete context[key];return result;};Function.prototype.change = change;
}();
let obj = { name: 'Alibaba' };
function func(x, y) {this.total = x + y;return this;
}
let res = func.change(obj, 100, 200);
//res => {name:'Alibaba',total:300}
  1. 考查call实现原理的相关习题
var name = 'HelloWorld';
function A(x,y){var res=x+y;console.log(res,this.name);
}
function B(x,y){var res=x-y;console.log(res,this.name);
}
B.call(A,40,30);
B.call.call.call(A,20,10);
Function.prototype.call(A,60,50);
Function.prototype.call.call.call(A,80,70);

结合第2题实现的call方法来分析上面的面试题图解:

4. 由下面代码输出的结果来自己写BIND方法

~function(){//=>bind方法在IE6~8中不兼容,接下来我们自己基于原生JS实现这个方法function bind(){};Function.prototype.bind=bind;
}();
var obj = {name:'zhufeng'};
function func(){console.log(this,arguments);//=>当点击BODY的时候,执行func方法,输出:obj [100,200,MouseEvent事件对象]
}
document.body.onclick = func.bind(obj,100,200);

分析:bind调用的时候不是立即就执行的,是把this执行obj预先存储起来,把参数传递给func预先存储起来,等点击body触发事件时才执行之前预先存储的东西(this指向obj,func传入参数执行)–bind的实现原理其实是返回来一个匿名函数,在这个匿名函数中执行call/apply函数

document.body.onclick = function anonymous() {func.call(obj, 100, 200);//call是立即就执行的
}

由以上代码来自己实现bind方法

Function.prototype.bind = function bind(context = window, ...args) {//context不传的时候默认为window//this->funclet that = this;//需要返回一个匿名函数return function anonymous(...inners) {//this->当前是body这个对象//在匿名函数中执行 func.call(obj,100,200);或者func.apply(obj,[100,200]);that.apply(context, args.concat(inners))}
}

自己重写bind方法

~function(proto){function bind(context=window,...outerArgs){let _this=this;return function(...innerArgs){let args=outerArgs.concat(innerArgs);_this.call(context,...args);}}proto.bind = bind;
}(Function.prototype);

最后回到题目上来,此时自己以及实现了bind方法

~function () {function bind(context, ...args) {//this -> funccontext = context == null ? window : context;let _this = this;return function anonymous(...inners) {_this.call(context, ...args.concat(inners));}};Function.prototype.bind = bind;
}();
var obj = {name:'zhufeng'};
function func(){console.log(this,arguments);//=>当点击BODY的时候,执行func方法,输出:obj [100,200,MouseEvent事件对象]
}
document.body.onclick = func.bind(obj,100,200);

bind的实现原理其实就是柯理化函数思想的应用:大函数返回一个小函数,在大函数中预先存储一些信息,然后在小函数中可以直接使用。

CALL的原理解析及相关习题相关推荐

  1. CAS单点登录原理解析及相关配置

    1.基于Cookie的单点登录的回顾 基于Cookie的单点登录核心原理: 将用户名密码加密之后存于Cookie中,之后访问网站时在过滤器(filter)中校验用户权限,如果没有权限则从Cookie中 ...

  2. JavaScript面向对象——封装及相关原理解析

    <JavaScript设计模式>面向对象编程--封装及相关原理解析 说明:本人编写js习惯不写分号:文章中的源码可根据自己的编程风格修改. 面向对象 面向对象编程就是将你的需求抽象成一个对 ...

  3. 电路原理计算机解法,电路原理学习指导与习题全解

    图书简介 本书是孙玉坤.陈晓平主编的<电路原理>配套的教学指导书,各章的排列次序.标题都与教材相同.每章均由四个部分组成:第一部分为基本知识点,概括性地说明本章的主要概念.基本理论和分析方 ...

  4. Tomcat 架构原理解析到架构设计借鉴

    ‍ 点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 Tomcat 架构原理解析到架构设计借鉴 Tomcat 发展这 ...

  5. 秋色园QBlog技术原理解析:Web之页面处理-内容填充(八)

    文章回顾: 1: 秋色园QBlog技术原理解析:开篇:整体认识(一) --介绍整体文件夹和文件的作用 2: 秋色园QBlog技术原理解析:认识整站处理流程(二) --介绍秋色园业务处理流程 3: 秋色 ...

  6. Android之Butterknife原理解析

    转载请标明出处:[顾林海的博客] 个人开发的微信小程序,目前功能是书籍推荐,后续会完善一些新功能,希望大家多多支持! ##前言 Butterknife是一个专注于Android系统的View注入框架, ...

  7. Android 插件化原理解析——Activity生命周期管理

    之前的 Android插件化原理解析 系列文章揭开了Hook机制的神秘面纱,现在我们手握倚天屠龙,那么如何通过这种技术完成插件化方案呢?具体来说,插件中的Activity,Service等组件如何在A ...

  8. Android代码入侵原理解析(一)

    Original 2017-05-06 付超红 滴滴安全应急响应中心 2017年初,在滴滴安全沙龙上,滴滴出行安全专家--付超红,针对App的攻与防进行了分享.会后大家对这个议题反响热烈,纷纷求详情求 ...

  9. Android 插件化原理解析——Hook机制之AMSPMS

    在前面的文章中我们介绍了DroidPlugin的Hook机制,也就是代理方式和Binder Hook:插件框架通过AOP实现了插件使用和开发的透明性.在讲述DroidPlugin如何实现四大组件的插件 ...

最新文章

  1. 学习UI设计能做什么
  2. Table——高淇JAVA300讲笔记之Guava
  3. 怪事,硬盘上的FC3_cd1的ISO莫名其妙被改变了
  4. php joomla,基于MySQL / PHP和Joomla的因特网管理信息系统设计
  5. linux中vi基础知识,Vim入门基础知识集锦
  6. 计组之中央处理器:2、指令周期数据流(指令周期、机器周期、时钟周期、取指周期、间址周期、执行周期、中断周期、单指令周期、多指令周期、流水线方案)
  7. linux启动mysql_Linux安装mysql
  8. pillow模块 (PIL) 生成验证码
  9. A-AUTOバッチ管理ツール(HOLD之后,如何再次启动)
  10. 如何检查字符串是否以指定的字符串开头? [重复]
  11. python代码翻译器-Python编程学习 -- 用十几行代码实现一个翻译器
  12. 教你在macOS Big Sur 11.0 系统中在右键菜单中添加发送共享到微信或QQ的功能?
  13. 通俗易懂的理解机器学习中的正则化项,正则化防止过拟合的的原理
  14. Python 目录及文件操作(os.模块)
  15. PyTorch 激励函数
  16. 通用接口测试用例设计
  17. 高性能计算机重要的配件,电脑配件大盘点,为高性能保驾护航!
  18. 无法导入C:\Users\Administrator\Desktop\***.reg:未将所有数据都成功写入到注册表中。某些项是由系统或其他进程打开的,或者你没有足够的权限执行此操作。
  19. 我所理解的SOLID原则
  20. 装X神器微信朋友圈小尾巴

热门文章

  1. qtcpsocket 占用内存的释放
  2. 2021.08.09【NOIP提高B组】模拟 QYQ在艾泽拉斯
  3. IDEA maven 右键不能run 解决办法
  4. 【基于云计算的资产管理系统】实现资产管理的高效、便捷、安全
  5. Linux下安装Mysql(rpm包安装)
  6. spring中的控制反转和依赖注入之间的关系
  7. SAP VF01开发票的时候报错:科目确定错误
  8. 牛客网错题总结(2)
  9. android QMI机制---概论
  10. MAC截图快捷键有哪些?