「面试重点」聊一聊JS中call、apply、bind里的小心思

引子

面试的重点难点的坑来啦!~/(ㄒoㄒ)/~~不出意外,this在ES5中是比较头疼和让初学者恐惧的一块,尽管在 ES6 中可能会极大避免 this 产生的错误,但是为了前端初学者能够在使用上能够将call,apply,bind等容易混淆的this指向问题,最好还是了解一下call、apply、bind 三者的区别,以及它们在底层中是如何来实现的~

call、apply、bind它们 究竟藏在哪里

所有函数能调call、apply.bind的方法前提是function是Function的实例,而Function.prototype上面有这三个方法

Function.prototype = {
    call
    apply
    bind
}
call / apply / bind 的使用

call / apply

用法:第一个参数就是改变的this指向,写谁就是谁(特殊:非严格模式下,传递null/undefined指向的也是window)
区别:执行函数,传递的参数方式有区别,call是一个个传递,apply是把需要传递的参数放到数组中整体传递

func.call([context],10,20);
func.apply([context],[10,20])

bind

用法:bind不是立即执行函数,属于预先改变this和传递一些内容 => "柯里化思想"
区别:call/apply都是改变this的同时直接将函数执行,而bind需要手动执行

let obj = {
    fn(x, y) {
        console.log(this, x, y);
    }
}

obj.fn.call();              // window 严格模式下: undefined
obj.fn.call(null);          // ...
obj.fn.call(undefined);     // ...
obj.fn.call(window, 10, 20);  // window
obj.fn.apply(window, [10, 20]);  // window

例:在1秒钟之后,执行fn函数,让其函数里的this变为window

错误写法:
setTimeout(obj.fn.call(window, 10, 20));

原因:
fn.call()自动执行,执行之后将结果(window)赋值给setTimeout再让浏览器执行,显然是错误的,因setTimeout第一个参数应为要执行的函数,而非window等表达式

正确写法:
setTimeout(obj.fn.bind(window, 10, 20));
call apply bind的实现

实现Function.prototype.bind(柯里化函数思想)

注:重写bind需要在Function.prototype定义,因为是Function原型上的方法
柯里化思想:一个大函数里面返回一个小函数,返回的小函数供外面调取使用,在执行大函数执行时形成的执行上下文不能销毁,形成闭包,保护大函数里面的变量,等到anonymous(下文提到)执行时,再调取大函数里面的变量
基础版

~ function anonymous(proto) {
    // context: bind更改之后的this指向
    function bind(context) {
        // context may be null or undefined
        if (context == undefined) {
            context = window;
        }

        <!--arguments { 0:context, 1:10, 2:20, length:3}-->
        <!--获取传递的实参集合-->
        var args = [].slice.call(arguments, 1);

        需要最终执行的函数(例: obj.fn)
        var _this = this;

        <!--bind()执行会返回一个新函数-->
        return function anonymous() {
             _this.apply(context, args);
        }

        proto.bind = bind;
    }
}(Function.prototype);

let obj = {
    fn(x, y) {
        console.log(this, x, y);
    }
}

现在bind原理懂了之后,我们来回顾一下这个题
回顾:在1秒钟之后,执行fn函数,让其函数里的this变为window
bind结合setTimeout实现
原理:
1、1s之后先执行bind的返回结果anonymous
2、在anonymous中再把需要执行的obj.fn执行,把之前存储的context/args传递给函数




setTimeout(obj.fn.bind(window, 10, 20));
setTimeout(anonymous, 1000);  

完整版

// document.body.onclick = obj.fn.bind(window, 10, 20);
document.body.onclick = anonymous;

:给当前元素的某个事件行为绑定方法,当事件触发执行完这个方法之后,方法中有一个默认事件对象ev(MouseEvent),ev作为anonymous的形参对象anonymous(ev),因为最终执行的是obj.fn,所以为了方便拿到ev

~ function anonymous(proto) {
    // context: bind更改之后的this指向
    function bind(context) {
        // context may be null or undefined
        if (context == undefined) {
            context = window;
        }

        <!--arguments { 0:context, 1:10, 2:20, length:3}-->
        <!--获取传递的实参集合-->
        var args = [].slice.call(arguments, 1);

        需要最终执行的函数(例: obj.fn)
        var _this = this;

        <!--bind()执行会返回一个新函数-->
        return function anonymous(ev) {
            args.push(ev);
             _this.apply(context, args);
        }

        proto.bind = bind;
    }
}(Function.prototype);

let obj = {
    fn(x, y,ev) {
        console.log(this, x, y,ev);
    }
};

由于anonymous不一定绑给谁,所以不一定有ev,但也还有可能是其他东西,所以...

...
...
    return function anonymous() {
        var amArg = [].slice.call(arguments, 0);
        args = args.concat(amArg);
         _this.apply(context, args);
    }

    proto.bind = bind;

bind核心逻辑(es6写法)

function bind (context = window, ...args) {
    return (...amArg) => {
        args = args.concat(amArg);
        _this.apply(context, args);
    }
}

经测试:apply在传递多个参数的情况下,性能不如call,故改写call

function bind (context = window, ...args) {
    return (...amArg) => {
        args = args.concat(amArg);
        _this.call(context, ...args);
    }
}

es6实现Function.prototype.call/apply

obj.fn.call(window, 10, 20)为例

原理:context.$fn = this

步骤:

1、把当前函数(要更改的函数obj.fn),作为context一个属性,赋给this
2、context.&fn(),this自然指向context
3、防止对象属性被窜改,及时delete context.$fn
4、call()执行之后应返回一个function,赋值给result
PS:(如果在面试的时候想写详细点可以限定context数据类型为引用类型,排除掉基本类型的可能)




~ function anonymous(proto) {
    // 只有当context不传,或传undefined时,才是window
    function call(context = window, ...args) {
    // 所以应该null情况考虑进去
        context === null ? context = window : null;
        let type = typ context;
        if (type !== "object" && type !== "function" && type !== "symbol"){
            // => 基本类型值
            switch(type) {
                case 'number':
                    context = new Number(context);
                    break;
                case 'string':
                    context = new String(context);
                    break;
                case 'boolean':
                    context = new Boolean(context);
                    break;
            }
        };

        <!--必须保证context是引用类型-->
        <!--this是call之前要执行的函数(obj.fn)-->
        // 关键步骤
        context.$fn = this;
        let result = context.$fn(...args);
        delete context.$fn;
        return result;
    }
    proto.call = call; 

    function apply(context = window, args) {
        context.$fn = this;
        let result = context.$fn(...args);
        delete context.$fn;
        return result;
    }
    proto.apply = apply; 
}(Function.prototype);

let obj = {
    fn(x, y) {
        console.log(this, x, y);
    }
};

obj.fn.call(window,10,20);  // Window {parent: Window, opener: null, top: Window, length: 0, frames: Window, …} 10 20
obj.fn.call(1,10,20);   // Number {1, $fn: ƒ} 10 20
obj.fn.call(true,10,20);  // Boolean {true, $fn: ƒ} 10 20

obj.fn.apply(true,[10,20]); // Boolean {true}__proto__: Boolean[[PrimitiveValue]]: true (2) [10, 20] undefined

强化练习

call的无限调用

function call(context = window, ...args) {
    // 必须保证context是引用类型
    context.$fn = this;
    let result = context.$fn(...args);
    delete context.$fn;
    return result;
}

call 引用类型 堆地址AAAFFF000
function fn1() { console.log(1); }
function fn2() { console.log(2); }
fn1.call(fn2);             // 执行的是fn1 => 1
fn1.call.call(fn2);        // 最终让fn2执行 => 2 (包括多个call)
Function.prototype.call(fn1);
Function.prototype.call.call(fn1);

fn1.call.call(fn2);

1、先让最后一个call执行,
最后一个call中的this是fn1.call,context是fn2
    this => fn1.call => AAAFFF000
    context => fn2
    args => []
最后一个call开始执行
    fn2.$fn = AAAFFF000 
    result = fn2.$fn(...[]) (AAAFFF000) 执行,

接着让call第二次执行
    this => fn2
    context => undefined
    args => []
    undefined.$fn = fn2
    result = undefined.$fn => (fn2())

最终让fn2执行

写在最后

  • 文中如有错误,欢迎在评论区指正,如果这篇文章帮到了你,欢迎点赞和关注
原文链接:juejin.im

上一篇:得到 Hybrid 架构的演进之路
下一篇:实现“乞丐版”的DOM事件流机制

相关推荐

  • 🙋Hanjst汉吉斯特改进+enSafeExpression安全表达式等

    Hanjst汉吉斯特模版语言及模版引擎,近期持续改进升级。 这次改进主要是增加了对安全输出表达式兼容,由于涉及到对软件开发过程中的效率和软件运行效率的平衡和取舍,所以多写了几句,以描述这个权衡利弊对...

    3 个月前
  • 🙋Hanjst汉吉斯特升级:+showImageAsync及性能改进等

    自2019年元旦🙋Hanjst汉吉斯特 模板语言及其编译引擎发布,已经过去一年多了。 这期间随着 🙋Hanjst汉吉斯特 的推广应用,我们也陆续发布了如下一些更新内容: 🛠️Hanjst/汉吉...

    4 个月前
  • 🙋Hanjst汉吉斯特优化+JsonDataFromScript等

    近日继续对 🙋Hanjst汉吉斯特优化改进。这次的改进思考是从服务器端返回的 HanjstJsonData的容器设计问题。目前的做法是服务器端的HanjstJsonData放入终端页面的一个Div元...

    2 个月前
  • 😉我用 Nuxt.js 仿了个掘金

    前言 首先肯定是要夸夸掘金啦,最开始从 CSDN 到 博客园 再到 掘金,个人感觉掘金的技术氛围非常的nice,真是个宝藏社区👏。技术文章大多以前端为主,对前端开发者非常友好,质量也是歪瑞古的。

    3 个月前
  • 😀一个原生js弹幕库

    danmujs 😀一个原生js弹幕库,基于 CSS3 Animation 地址、核心代码 本项目基于 rcbullets,项目约70%的代码基于rcbullets,首先要感谢这个项目的作者,如...

    6 个月前
  • 🕵️‍♀️由原型到JS中的“模拟类”

    讲述了有关 JavaScript 中原型相关知识,又引出了 JavaScript 中的“类“究竟是什么?,以及一系列相关问题。 一、前置知识 1、JavaScript 的面向对象(OOP) ​ 面向...

    4 个月前
  • 🔥《吊打面试官》系列 Node.js 必知必会必问!

    (/public/upload/f204a3b224d986128f1b4d9b8d06cd17) 前言 codeing 应当是一生的事业,而不仅仅是 30 岁的青春🍚 本文已收录 Git...

    5 个月前
  • 💖CSS + JS 送学妹满屏幕小爱心

    故事开始 午饭时间,暗恋已久的学妹拉着我的衣袖:“学长学长,你能不能让这些爱心变成五颜六色的吗~”。 我在旁边笑开了花~~~ image.png(/public/upload/04aaa24e...

    4 个月前
  • (干货👍)从详细操作js数组到浅析v8中array.js

    前言 最近在写面试编程题,经常用到数组,经常想偷个懒,用它提供的方法,奈何还是对数组方法使用不熟练,导致写了很多的垃圾代码,很多地方稍加修改的话肯定变得简洁高效优雅👊 所以✍这篇文章本着了解一下Ja...

    1 个月前
  • (vuejs学习)2、使用ElementUI(*)

    1.element安装 开发环境是win10,一到node官网下载node的.msi包(https://npm.taobao.org/mirrors/node/v10.16.0/nodev10.16....

    1 年前

官方社区

扫码加入 JavaScript 社区