CALL的原理解析及相关习题

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最后改变为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>)
  1. 将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>)
  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最后改变为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原理来解析面试题
  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->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}
  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->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的实现原理其实就是柯理化函数思想的应用:大函数返回一个小函数,在大函数中预先存储一些信息,然后在小函数中可以直接使用。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值