深入学习js之call、apply、bind模拟实现

题外篇

如何改变this得指向,常见的四种操作如下

  • 使用call、apply、bind
  • 在执行函数内部使用let that = this
  • es6中使用箭头函数
  • 对象实例化 new操作

关于this的指向

在一个函数上下文中,this由调用者提供,由调用函数的方式来决定。如果调用者函数,被某一个对象所拥有,那么该函数在调用时,内部的this指向该对象。如果函数独立调用,那么该函数内部的this,则指向undefined。但是在非严格模式中,当this指向undefined时,它会被自动指向全局对象。

参考来自 波波老师

call

根据 MDN 的解释:

call()方法调用一个函数, 其具有一个指定的this值和分别地提供的参数(参数的列表)。

语法:

fun.call(thisArg, arg1, arg2, ...)

thisArg:fun函数运行时指定得this

this得值可能有如下几种可能:

  • 在严格模式下,传null,undefined or 不传,this默认指向window对象
  • 传其他函数的函数名称,如fn,this指向fn函数
  • 传其他对象,this指向这个对象

看个例子

let obj = {
    val:'call'
}
function fn () {
    console.log(this.val,'testCall');
}
fn.call(obj) //'call','testCall'

模拟第一版

先考虑可以正常执行,后面的传参暂时不考虑

琢磨一下上面例子的代码执行过程

call()在执行过程中,我们想象一下它大概会经历一些几个阶段(真实原理不做介绍)

  • fn方法复制到obj对象中
  • 改变fn函数的this指向
  • fn函数执行
  • fnobj对象删除

分析:
那么我们在模拟代码的场景下
fn.call(obj)的执行过程可以想象成如下步骤:

1、将fn复制到obj对象中,那么如下也就修改了fn中this的指向

obj = {
   val:'call',
   fn:function(){
       console.log(this.val,'testCall')
  }
  
2、 执行fn()

obj.fn()

3、删除fn这个key

delete obj.fn

模拟开始
Function.prototype.call2 = function(args){

    //此时的args就是 上面的obj
    //1,此时使用this来获取调用call的方法
    args.fn = this;
    
    //第二步 调用执行fn()
    args.fn();
    
    //第三步 删除方法
    delete args.fn
    
}

//测试下
let obj = {
    val:'call2'
}
function fn () {
    console.log(this.val,'testCall2');
}
fn.call2(obj) //'call2','testCall2'

模拟二版

MDN文档上介绍过,call可以接受多个参数,那么在第二版的时候我们加上入参这个功能

栗子

let obj = {
    val:'call'
}
function fn (name) {
    console.log(this.val,name);
}
fn.call(obj,'alan') //'call','alan'

分析:

  • 跟第一阶段相比就是多了一个传参,有疑惑的地方,可能不知道穿几个参数,不慌,可以从Arguments中获取第二个开始到最后结束的参数就行了
模拟开始
// 第二版
Function.prototype.call2 = function(...args) {

    //利用es6的 rest 来获取函数的传参,以及传入thisArg;
    let [thisArg,...arr] = args ;
    
    // 获取调用的函数方法
    thisArg.fn = this;
    
    // 用解构执行函数
    thisArg.fn(...arr)
    
    //删除
    delete thisArg.fn
    
}

let obj = {
    val:'call'
}

function fn (name) {
    console.log(this.val,name);
}

fn.call2(obj,'alan') //'call','alan'

解释:

  • es6rest (形式为...变量名),这样可以得到一个数组,即args此时为数组,那么上文中的thisArg就是传递的第一个参数。
  • fn(..arr)使用了es6 spread ,他就好比是reset的逆运算,这样操作以后不管传递了几个参数都可以正常处理

模拟第三版

文章开头介绍过,如果在严格模式下,传null,undefinedor 不传,thisArg默认指向window对象,还有一种场景如果fn方法有返回值的情况。

栗子 1

var val = 'call'
function fn () {
    console.log(this.val);
}
fn.call() //'call'
fn.call(null);//'call'
fn.call(undefind);//'call'

分析:
如果不传值或传null等值,处理起来不算麻烦,稍微在我们原来的版本上做一些修改就好,看如下代码

// 3.1
Function.prototype.call2 = function(...args) {
    let thisArg,arr = [];
    if(args.length === 0 || !args[0]){
        thisArg = window;
    } else{
        //利用es6的解构来获取函数的传参,以及传入thisArg;
        [thisArg,...arr] = args ;
    }
   
    // 获取调用的函数方法
    thisArg.fn = this;
    // 用解构执行函数
    thisArg.fn(...arr)
    //删除
    delete thisArg.fn
}
fn.call2() //'call'
fn.call2(null);//'call'
fn.call2(undefind);//'call'

栗子2

let obj = {
    val:'call'
}
function fn (name) {
    console.log(this.val,name);
    return {
        val:this.val,
        name:name
    }
}
fn.call(obj,'alan') //'call','alan'
//
{
    val:'call',
    name:'alan'
}

终极版本

// 3.2
Function.prototype.call2 = function(...args) {
    let thisArg,arr = [];
    if(args.length === 0 || !args[0]){
        thisArg = window;
    } else{
        //利用es6的解构来获取函数的传参,以及传入thisArg;
        [thisArg,...arr] = args ;
    }
   
    // 获取调用的函数方法
    thisArg.fn = this;
    // 用解构执行函数
    let result = thisArg.fn(...arr)
    //删除
    delete thisArg.fn
    return result
}

let obj = {
    val:'call'
}
function fn (name) {
    console.log(this.val,name);
    return {
        val:this.val,
        name:name
    }
}
fn.call2(obj,'alan') //'call','alan'
//
{
    val:'call',
    name:'alan'
}

apply

apply的实现方式跟call基本相似,就是在传参上,apply接受的是数组,直接就贴一下代码

Function.prototype.apply2 = function(thisArg,arr) {

    if(!thisArg){
        thisArg = window;
    } 
   
    // 获取调用的函数方法
    thisArg.fn = this;
    // 用解构执行函数
    let result = thisArg.fn(...arr)
    //删除
    delete thisArg.fn
    
    return result
}
let obj = {
    val:'apply'
}
function fn (name) {
    console.log(this.val,name);
    return {
        val:this.val,
        name:name
    }
}
fn.apply2(obj,['alan']) //'apply','alan'

bind

根据 MDN 的解释:

bind()方法创建一个新的函数,在调用时设置this关键字为提供的值。将给定参数列表作为原函数的参数序列的前若干项。

语法:

fun.bind(thisArg,arg1,arg2......)
  • bind()方法会创建一个新的函数,一般叫绑定函数

  • 可以接受参数,这个地方注意,它可以在bind的时候接受参数,同时bind()返回的新函数也可以接受参数

栗子

var obj = {
    val: 'bind'
};

function fn() {
    console.log(this.val);
}

// 返回了一个函数
var bindObj = fn.bind(obj); 

bindObj(); // bind

模拟第一版

照旧,暂时不考虑传参

分析:

  • bindObj()的执行结果跟使用call一样的,不同的是它需要调用返回的方法bindObj

琢磨上述代码执行过程,这个时候我们对比一下call的模拟来看

  • bindObj像是call模拟过程中的fn,而后bindObj()就像是fn()
  • bind返回的函数,我们可以想象成call()调用只有返回的函数而不会执行,只是apply(),call()是立即执行,而bind需要再次调用执行
模拟开始
Function.prototype.bind2 = function (args) {

  //通过this拿到调用方法
  let that  = this;
  
  //使用一个闭包来存储call方法的结果
  return function () {
      return that.call(args);
  }

}

var obj = {
    val: 'bind'
};

function fn() {
    console.log(this.val);
}

// 返回了一个函数
var bindObj = fn.bind2(obj); 

bindObj(); // bind

模拟第二版

考虑下传参的场景,开头介绍过,传参有两种场景

栗子

let obj = {
    val:'bind'
};
function fn(name,sex){
    let o = {
        val:this.val,
        name:name,
        sex:sex
    }
    console.log(o)
}
let bindObj = fn.bind(obj,'alan'); 

bindObj('man'); //{ val: 'bind', name: 'alan', sex: 'man' }

栗子分析:

  • 首先bind的时候接受了一个参数name,同时返回了一个函数
  • 执行放回的函数的时候传入了第二个参数sex

模拟分析:

  • 首先考虑bind方法传参的场景,我们可以借用之前在call函数中的方法,使用es6 rest的方法。获取从第二个开始到结束的所有参数
  • 考虑bind返回的函数传参,可以在写的时候,将bind传参跟后续的传参合并

模拟开发

// 2.1
Function.prototype.bind2 = function (args) {
    //通过this拿到调用方法
    let that = this;
    
    // 获取bind2函数从第二个参数到最后一个参数
    let allArgs = Array.prototype.slice.call(arguments, 1);

    return function () {
        // 这个时候的arguments是指bind返回的函数传入的参数
        var bindArgs = Array.prototype.slice.call(arguments);
        return that.apply(args, allArgs.concat(bindArgs));
    }

}
//2.2 es6实现
Function.prototype.bind2 = function (...args) {
  //利用es6的 rest 来获取函数的传参,以及传入thisArg;allArgs就是第二个参数到最后一个参数的数组
  let [thisArg,...allArgs] = args ;
  let that = this;
  return function (...bindArgs) {
      return that.apply(thisArg, allArgs.concat(bindArgs));
  }

}
let obj = {
    val:'bind'
};
function fn(name,sex){
    let o = {
        val:this.val,
        name:name,
        sex:sex
    }
    console.log(o)
}
let bindObj = fn.bind2(obj,'alan'); 
bindObj('man'); //{ val: 'bind', name: 'alan', sex: 'man' }

说明

  • Array.prototype.slice.call(arguments)是如何将arguments转换成数组的,首先调用call之后,this就指向了arguments,或许我们可以假象一下slice的内部实现是:创建一个新的数组,然后循环遍历this,将this的没一个值赋值给新的数组然后返回新数组。

结束语

大佬如果看到文中如有错误的地方欢迎指出支出,我会及时修改。

参考

http://es6.ruanyifeng.com/?search=spread&x=0&y=0

https://github.com/mqyqingfeng/Blog/issues/11

https://www.jianshu.com/p/d647aa6d1ae6

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容