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方法思路:
- call执行的时候将this指向obj,参数传递给函数func并且执行func
- 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封装
- null == undefined --> true
- 【非严格模式下】context不传或者传递NULL/undefined都让this最后改变为window
- call实现的核心原理:给CONTEXT设置一个属性(属性名尽可能保持唯一,避免我们自己设置的属性修改默认对象中的结构,例如可以基于Symbol实现,也可以创建一个时间戳名字),属性值一定是我们要执行的函数(也就是THIS,CALL中的THIS就是我们要操作的这个函数);接下来基于CONTEXT.XXX()成员访问执行方法,就可以把函数执行,并且改变里面的THIS(还可以把PARAMS中的信息传递给这个函数即可);都处理完了,别忘记把给CONTEXT设置的这个属性删除掉(人家之前没有这个属性,现在你自己加了,使用完之后我们需要把它删了)
- 如果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. 字面量创建
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是基本类型值,默认是不能设置属性的,此时我们需要把这个基本类型值修改为它对应的引用类型值(也就是构造函数的结果)
- 不考虑context是Symbol和BigInt类型的基本数据类型时的写法:
Function.prototype.call = function call(context, ...params) {
//【非严格模式下】不传或者传递NULL/UNDEFINED都让THIS最后改变为WINDOW
context == 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 20
res = func.call(Symbol('1'), 10, 20);
// Uncaught TypeError: Symbol is not a constructor
res = func.call(BigInt('1'), 10, 20);
//Uncaught TypeError: BigInt is not a constructor at new Symbol (<anonymous>)
- 将Symbol和BigInt类型的基本数据类型转化为对象类型,如果也利用new Symbol(‘1’).constructor(Symbol(‘1’)),这种方法不可以
new Symbol('1').constructor(Symbol('1'))
// Uncaught TypeError: Symbol is not a constructor
new BigInt('1').constructor(BigInt('1'))
//Uncaught TypeError: BigInt is not a constructor at new BigInt (<anonymous>)
- 直接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>)
- 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最后改变为WINDOW
context == 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); //30
res = func.call(100, 30, 20);
/*
Number {100, Symbol(KEY): ƒ}
//展开如下:
__proto__: Number
[[PrimitiveValue]]: 100
*/
console.log(res); //50
res = 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); //90
func.call();//Window
func.call(null, 30, 60);//Window
利用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方法来分析上面的面试题图解:
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->func
context = 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}
- 考查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->func
let 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 -> func
context = 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的实现原理其实就是柯理化函数思想的应用:大函数返回一个小函数,在大函数中预先存储一些信息,然后在小函数中可以直接使用。