「JavaScript 底层运行机制」 -- 了解底层,轻松实现重构 CALL/APPLY/BIND


你活得不快乐的原因是:既无法忍受目前的状态,又没能力改变这一切。

哈哈,认真起来的样子,很有魅力嘛 ☺️☺️☺️

Bug 虐我千百遍,我待 Bug 如初恋。大家好,我是 Guyal,点个关注不迷路,小谷儿带你上高速;Skr~😉

在疫情期间,由于面试找工作被虐的体无完肤,所以下定决心,沉淀自己,等待蓄势待发;

我的目标是:吊打面试官...🙂

读完本篇文章,可以让你轻松手写实现自认为「比较优秀」的 CALL/APPLY 方法


前言

Function.prototype:call/apply/bind 都是用来改变this指向的,以后函数执行调用这三个方法就可以实现this的改变

CALL 的内部机制

插一波基础语法

定义: 使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数

语法function.call(thisArg, arg1, arg2...)

作用

  • 立即执行函数
  • 改变函数中的 this,并且给函数传递参数信息
  • call 方法是 Function.prototype 上的方法

调用 call 方法,实则调用的是 Function.prototype 上的方法

Function.prototype.call = function call(context) {}

来个EG

// Function.prototype.call = function call(context) {}
function fn(x, y) { 
  console.log(this, x, y);// {name: "guyal_"} undefined undefined
};

let obj = {
  name: 'guyal_'
};

obj 和 函数 fn 之间没有任何关联,所以无法基于 obj.fn 来改变 fn 中的 this,所以会报错

obj.fn(); // 报错 obj.fn is not a function  

call 方法来走一波

fn.call(obj);

得,输出的结果是我们想要的。call 方法帮我们解决了 this 的指向问题

{name: "guyal_"} undefined undefined

我们再来看 call 方法内部到底干了什么?

CALL 方法的执行顺序

实例基于__proto__找到 Function.prototype 上的 call 方法,把 call 方法执行

console.log(fn.__proto__ === Function.prototype);// true

  • call 方法中的 thisfn,「谁调用了 call 方法,this 就指向谁」
Function.prototype.call = function call(context, ...params) {
	console.log(this); // 输出函数 fn
}
  • context 存储的是传递的 obj
Function.prototype.call = function call(context, ...params) {
	console.log(this); // 输出 {name: "guyal_"},obj对象
}

👀 👀 👀

CALL 方法内部机制

  • fn 执行(也就是把 call 中的 this 执行)
  • fn 中的 this 改变为第一个参数 context (也就是 obj )
  • 在非严格模式下, context 不传递或者传递 null/undefined ,则 this 都改为window;严格模式下,不传是 undefined,否则传递谁,this 就改为谁
fn.call(obj);

fn.call(obj, 10, 20);

fn.call(null); 

fn.call(); 

在严格模式下输出结果

在非严格模式下输出结果

  • call 的第一个参数是 context,也就是执行 fn, 把 fn 中的 this 变为 10, 传递参数为 20
    • thisObject 类型,它会把值类型 10 自动转换为对象类型 10,即 Number{10}
fn.call(10, 20); // this: Number {10} x: 20 y: undefined

applycall 的唯一区别,就是在给 fn 传递参数的时候,apply 需要把所有需要传递的参数信息放在一个数组中,而 call 是一个个的传递进来,不论哪种方法的,最后的结果都是把这些参数一个个的传递给 fn

fn.apply(obj, [10, 20]);

重写 CALL

实现思路

function fn(x, y) { 
  console.log(this, x, y);
};

let obj = {
  name: 'guyal_'
};

Function.prototype.call = function call(context) {}
  1. 把需要执行的函数和需要改变的 THIS 关联在一起
    实现关联:在对象中添加一个属性,属性值就是当前要执行的函数 fn

context:传递的对象

this:需要执行的函数

Function.prototype.call = function call(context) {
	context.fn = this;
}
// 相当于
obj.fn = fn;
  1. 执行 obj.fn()
    输出的结果是:{name: "guyal_", fn: ƒ} undefined undefined,最终结果是 OK
Function.prototype.call = function call(context) {
	context.fn = this;
    context.fn();
}

那么此时obj 多了一个 fn 属性,我们得需要手动移除自己新增的属性

  1. 执行完之后移除自己新增的属性
Function.prototype.call = function call(context) {
    context.fn = this;
    context.fn();
	delete context.fn;
}
  1. 返回结果
Function.prototype.call = function call(context, ...params) {
    context.fn = this;
    var result = context.fn();
	delete context.fn;
	return result;
}

最终结果

浏览器控制台中输出的结果是函数执行的时候输出的,点击展开的结果是当前对内存中最新的结果

基本功能已实现。到最后的优化阶段~ 😉😉😉

  1. 处理传递的参数:context不传(undefined)或者为null
console.log(null == undefined);// true
context == null ? context = window : null;
  1. 只有函数和对象才可以设置属性和方法,必须保证传递的第一个参数 context 是函数或者对象
    匹配 typeof值为 object或者function,如果不是的话,使用 Object(context) 方法转换为复杂数据类型的值,否则就是null
!/^(object|function)$/.test(typeof context)?Object(context):null;
  1. 平时一般不会直接操作 this,要用一个变量来接收 this,直接操作这个变量
let self = this;
  1. 新增的属性名必须保证唯一性,防止污染原始对象中的成员
let key = Symbol('KEY');

附上最终代码

Function.prototype.call = function call(context, ...params) {
	let self = this,
    	result = null,
        key = Symbol('KEY');
        
        context == null ? context = window : null;
        !/^(object|function)$/.test(typeof context)? Object(context) : null;
       
        context.fn = self;
        result = context.fn(...params);
        delete context.fn;
        return result;
}

function fn(x, y) {
	console.log(this, x, y);
};

let obj = {
	name: 'guyal_'
};

fn.call(obj, 10, 20);

CALLAPPLY 的区别:

  • call()方法的作用和 apply() 方法类似,区别就是call()方法接受的是参数列表,而apply()方法接受的是一个参数数组。
  • call的性能要比apply好一些:尤其是传递三个及以上参数的时候

相同:call 和 apply 都是立即执行函数,并且改变函数中的 this,给函数传递参数信息

在这里不在详细分析,直接附上最终代码

Function.prototype.apply = function call(context, params) {
	let self = this,
    	result = null,
        key = Symbol('KEY'),
        args = [];
        
        context == null ? context = window : null;
        !/^(object|function)$/.test(typeof context)? Object(context) : null;
        if(Object.prototype.toString.call(params) !== '[object Array]' && typeof params !== 'undefined')throw new TypeError('CreateListFromArrayLike called on non-object');

        context.fn = self;

        if (!params) {
            result = context.fn();
        }else {
        	/*
            for (var i = 0, len = params.length; i < len; i++) {
                args.push('params[' + i + ']');
            }
            result = eval('context.fn(' + args + ')')
        }  */
        result = context.fn(params);
        
        delete context.fn;
        return result;
}
function fn(x, y) {
	console.log(this, x, y);
};

let obj = {
	name: 'guyal_'
};

fn.apply(obj);

重写 BIND:柯理化函数(预处理思想)

bind:并不会把函数立即执行,它是预先处理函数中的THIS和参数的

fn方法本身作为值绑定给BODY的点击事件,当触发BODY的点击操作,浏览器会帮助我们把fn函数执行「并且传递一个事件对象」,方法中的THIS指向BODY

document.body.onclick = fn;
// this:body  x:MouseEvent  y:undefined

立即把fn执行,把其执行的返回结果当作值,赋值给事件绑定,事件触发执行的是返回结果

document.body.onclick = fn();

假如我点击BODY的时候,把FN执行,并且让THIS指向OBJ,而且传递10/20X/Y

这样处理显然是不能实现的:因为call/apply都是把函数立即执行的,还没有等到点击的时候,函数都执行完了

document.body.onclick = fn.call(obj, 10, 20); 

我们可以这样实现:

  1. 创建一个匿名函数,绑定给点击事件,触发BODY点击行为的时候,执行的是匿名函数
    匿名函数中的 this 指的值 bodyev专递的是MouseEvent事件对象;
    创建匿名函数之后,我们就可以再匿名函数中,执行 fn,随意处理自己的逻辑
document.body.onclick = function (ev) {
    // 在匿名函数中,我们自己执行fn即可,接下来想改啥改啥
    fn.call(obj, 10, 20, ev);
}; 
  1. bind 不会把函数立即执行,它是预先处理函数中的THIS和参数
    哟,巧了,你说这不是巧了嘛。我们想要实现的功能和 bind 的作用一毛一样,看代码
document.body.onclick = fn.bind(obj, 10, 20);

重写 bind 代码实现

Function.prototype.bind = function bind(context, ...outerArgs) { 
  // this: fn
  // context: obj
  // ...outerArgs: [10, 20]
  // 第一次执行函数只是把参数预存储, 第二次执行的是匿名函数, 把预先存储的参数拿过来, 使用 call 方法改变 this 的指向「柯理化函数,预处理思想」
  let self = this;

  return function (...innerArgs) { 
    self.call(context, ...outerArgs.concat(...innerArgs));
   }
};

function fn(x, y, ev) { 
  console.log(this, x, y, ev);
 };

 let obj = {
  name: 'guyal_'
 };

document.body.onclick = function (ev) { 
  fn.call(obj, 10, 20, ev);
};

document.body.onclick = fn.bind(obj, 10, 20)

Ending

到这里本篇就可以结束了,如若有错误或者不严谨的地方,请多多提出。万分感谢~

Bug 虐我千百遍,我待 Bug 如初恋。大家再见,我是 Guyal,点个关注不迷路,小谷儿带你上高速;Skr~😉

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值