fn1callcallfn2

描述function fn1(){ console.log(1);}function fn2(){ console.log(2);}fn1.call(fn2); // 输出1fn1.call.call(fn2); // 输出2问题看到这个题目,第一反应是蒙圈的。 fn1.call(fn2); 这个是理解的。fn1.call.call(fn2);这个蒙圈了。 理解有些绕,需要多念叨念叨琢磨琢磨。 call 方法是Function.prototype原型上天生自带的方法,所有的函数都可以调用的。 我觉得 call方法本身没有具体return什么出来,所以是undefined。 Function.prototype.call=function call(context){ // [native code] // call方法的功能 // 1. 把指定函数中的this指向context // 2. 把指定函数执行 // 那么call方法中的this,即为指定函数。也就是说 // 1. 把this中 的 this关键字指向context; // 2. 把指定函数执行this();};fn1.call(fn2);按照上面的理解 call 方法中的this是fn1把call方法中的this(fn1)中的this指向fn2调用 call方法中的this所以调用的是 fn1 ,此时fn1中的 this 指向的是 fn2。但是这个方法里面并没有使用this,而是直接输出了1。 fn1.call.call(fn2);按照上面的理解 call 方法中的 this 是 fn1.call【所有函数都可以调用call,调用的是原型上call方法】把call方法中的this (fn1.call) 中的this 指向fn2调用call方法中的this所以调用的是 fn2(这里有些绕,多念叨念叨琢磨琢磨),此时fn1.call中的this指向的是fn2。它改变了call方法(Function.prototype原型上的call)的this指向。此处调用了call方法中的this,即调用了fn2,输出了2。

July 3, 2019 · 1 min · jiezi

ES5-callapplybind方法总结包括理解this的指向问题

总结call,apply,bind方法的理解使用和区别。call,apply,bind这三个方法在JavaScript中是用来改变函数调用的this指向。那么改变函数this指向有什么用呢?我们先来看一段代码 var a= { name:'harden', fn:function () { console.log(this.name); }}var b = a.fn;a.fn();//hardenb();//undefined调用a.fn方法后得到了harden,但在b方法中我想得到harden,为什么却是undefined呢?原因是方法在执行的时候才能确定this到底指向谁,实际上this指向的是最终调用函数的对象。这里当b执行时,实际是window调用了fn函数,那么fn中的this就指向window。在开始讲call,apply,bind方法前,一起来总结一下this的指向问题。 理解JavaScript中的this指向问题。总体来说this指向可以概括为一句话:this指向在函数的定义时是不确定的,只有函数执行时才能确定this到底指向谁,实际上this的最终指向的是那个调用它的对象。但是这个说法在函数被很多对象包裹的时候并不成立,请看下面例子。简单来说就是:谁(哪个对象)调用的这个函数,那么这个函数中的this就指向这个对象。 例一 function a(){ var name= "harden"; console.log(this.name); //undefined console.log(this); //Window } a();因为this最终指向调用他的对象,在上述代码中其实是widow触发的这个方法,那么this就指向window,window中并没有定义a,那么就打印出undefined。例二: var a = { name:'harden', fn:function() { console.log(this.name);//harden console.log(this);//指向a(可以自己跑一下) }}a.fn()这里的this指向a,因为这里的fn函数是通过a.fn()执行的,那么this自然指向a。说到这我就有疑问了,如果我用 window.a.fn()执行函数,this不就指向window了吗?然后并不是这样的,请看下一个例子。补充一点:window是js的全局对象。例三: var a = { name:'harden', b:{ name:'james', fn:function() { console.log(this.name);//james console.log(this);//指向b } }}a.b.fn()我们看到最终是a调用的方法,那为什么this会指向b呢?现在总结三句话,来完全理解this的指向问题: 情况一:如果一个函数中有this,但是它没有被上一级的对象所调用,那么this指向的就是window(除去严格模式外)。情况二:如果一个函数中有this,这个函数有被上一级的对象所调用,那么this指向的就是上一级的对象。情况三:如果一个函数中有this,这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,this指向的也只是它上一级的对象,例子3可以证明。 构造函数中的this: function Fn(){ this.name = "harden"; } var a = new Fn(); console.log(a.name); //harden这里的a可以点出name,因为new关键字会改变this的指向。为什么new关键字会改变this呢,我自己有两种看法:1.在new的过程中会创建一个实例对象,通过apply等方法 通过 Fn.apply({}) 使this指向这个空对象,最后把fn方法中的材料加工完后返回给a。 ...

June 21, 2019 · 1 min · jiezi

JavaScript进阶之模拟call,apply和bind

原文:https://zhehuaxuan.github.io/… 作者:zhehuaxuan目的本文主要用于理解和掌握call,apply和bind的使用和原理,本文适用于对它们的用法不是很熟悉,或者想搞清楚它们原理的童鞋。 好,那我们开始! 在JavaScript中有三种方式来改变this的作用域call,apply和bind。我们先来看看它们是怎么用的,只有知道怎么用的,我们才能来模拟它。Function.prototype.call()首先是Function.prototype.call(),不熟的童鞋请猛戳MDN,它是这么说的:call()允许为不同的对象分配和调用属于一个对象的函数/方法。也就是说:一个函数,只要调用call()方法,就可以把它分配给不同的对象。如果还是不明白,不急!跟我往下看,我们先来写一个call()函数最简单的用法:function source(){ console.log(this.name); //打印 xuan}let destination = { name:“xuan”};console.log(source.call(destination));上述代码会打印出destination的name属性,也就是说source()函数通过调用call(),source()函数中的this对象可以分配到destination对象中。类似于实现destination.source()的效果,当然前提是destination要有一个source属性好,现在大家应该明白call()的基本用法,我们再来看下面的例子:function source(age,gender){ console.log(this.name); console.log(age); console.log(gender);}let destination = { name:“xuan”};console.log(source.call(destination,18,“male”));打印效果如下:我们可以看到可以call()也可以传参,而且是以参数,参数,…的形式传入。上述我们知道call()的两个作用:1.改变this的指向2.支持对函数传参我们看到最后还还输出一个undefined,说明现在调用source.call(…args)没有返回值。我们给source函数添加一个返回值试一下:function source(age,gender){ console.log(this.name); console.log(age); console.log(gender); //添加一个返回值对象 return { age:age, gender:gender, name:this.name }}let destination = { name:“xuan”};console.log(source.call(destination,18,“male”));打印结果:果不其然!call()函数的返回值就是source函数的返回值,那么call()函数的作用已经很明显了。这边再总结一下:改变this的指向支持对函数传参函数返回什么,call就返回什么。模拟Function.prototype.call()根据call()函数的作用,我们下面一步一步的进行模拟。我们先把上面的部分代码摘抄下来:function source(age,gender){ console.log(this.name); console.log(age); console.log(gender); //添加一个返回值对象 return { age:age, gender:gender, name:this.name }}let destination = { name:“xuan”};上面的这部分代码我们先不变。现在只要实现一个函数call1()并使用下面方式console.log(source.call1(destination));如果得出的结果和call()函数一样,那就没问题了。现在我们来模拟第一步:改变this的指向。假设我们destination的结构是这样的:let destination = { name:“xuan”, source:function(age,gender){ console.log(this.name); console.log(age); console.log(gender); //添加一个返回值对象 return { age:age, gender:gender, name:this.name } }}我们执行destination.source(18,“male”);就可以在source()函数中把正确的结果打印出来并且返回我们想要的值。现在我们的目的更明确了:给destination对象添加一个source属性,然后添加参数执行它。所以我们定义如下:Function.prototype.call1 = function(ctx){ ctx.fn = this; //ctx为destination this指向source 那么就是destination.fn = source; ctx.fn(); // 执行函数 delete ctx.fn; //在删除这个属性}console.log(source.call1(destination,18,“male”));打印效果如下:我们发现this的指向已经改变了,但是我们传入的参数还没有处理。第二步:支持对函数传参。 我们使用ES6语法修改如下:Function.prototype.call1 =function(ctx,…args){ ctx.fn = this; ctx.fn(…args); delete ctx.fn;}console.log(source.call1(destination,18,“male”));打印效果如下:参数出现了,现在就剩下返回值了,很简单,我们再修改一下:Function.prototype.call1 =function(ctx,…args){ ctx.fn = this || window; //防止ctx为null的情况 let res = ctx.fn(…args); delete ctx.fn; return res;}console.log(source.call1(destination,18,“male”));打印效果如下:现在我们实现了call的效果!模拟Function.prototype.apply()apply()函数的作用和call()函数一样,只是传参的方式不一样。apply的用法可以查看MDN,MDN这么说的:apply() 方法调用一个具有给定this值的函数,以及作为一个数组(或类似数组对象)提供的参数。apply()函数的第二个参数是一个数组,数组是调用apply()的函数的参数。function source(age,gender){ console.log(this.name); console.log(age); console.log(gender); return { age:age, gender:gender, name:this.name }}let destination = { name:“xuan”};console.log(source.apply(destination,[18,“male”]));效果和call()是一样的。既然只是传参不一样,我们把模拟call()函数的代码稍微改改:Function.prototype.apply1 =function(ctx,args=[]){ ctx.fn = this || window; let res = ctx.fn(…args); delete ctx.fn; return res;}console.log(source.apply1(destination,[18,‘male’]));执行效果如下:apply()函数的模拟完成。Function.prototype.bind()对于bind()函数的作用,我们引用MDN,bind()方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this对象,之后的一序列参数将会在传递的实参前传入作为它的参数。我们看一下代码:function source(age,gender){ console.log(this.name); console.log(age); console.log(gender); return { age:age, gender:gender, name:this.name }}let destination = { name:“xuan”};var res = source.bind(destination,18,“male”);console.log(res());console.log("==========================")var res1 = source.bind(destination,18);console.log(res1(“male”));console.log("==========================")var res2 = source.bind(destination);console.log(res2(18,“male”));打印效果如下:我们发现bind函数跟apply和call有两个区别:1.bind返回的是函数,虽然也有call和apply的作用,但是需要在调用bind()时生效2.bind中也可以添加参数明白了区别,下面我们来模拟bind函数。模拟Function.prototype.bind()和模拟call一样,现摘抄下面的代码:function source(age,gender){ console.log(this.name); console.log(age); console.log(gender); //添加一个返回值对象 return { age:age, gender:gender, name:this.name }}let destination = { name:“xuan”};然后我们定义一个函数bind1,如果执行下面的代码能够返回和bind函数一样的值,就达到我们的目的。var res = source.bind1(destination,18);console.log(res(“male”));首先我们定义一个bind1函数,因为返回值是一个函数,所以我们可以这么写:Function.prototype.bind1 = function(ctx,…args){ var that = this;//外层的this指向通过变量传进去 return function(){ //将外层函数的参数和内层函数的参数合并 var all_args = […args].concat([…arguments]); //因为ctx是外层的this指针,在外层我们使用一个变量that引用进来 return that.apply(ctx,all_args); }}打印效果如下:这里我们利用闭包,把外层函数的ctx和参数args传到内层函数,再将内外传递的参数合并,然后使用apply()或call()函数,将其返回。当我们调用res(“male”)时,因为外层ctx和args还是会存在内存当中,所以调用时,前面的ctx也就是source,args也就是18,再将传入的"male"跟18合并[18,‘male’],执行source.apply(destination,[18,‘male’]);返回函数结果即可。bind()的模拟完成!但是bind除了上述用法,还可以有如下用法:function source(age,gender){ console.log(this.name); console.log(age); console.log(gender); //添加一个返回值对象 return { age:age, gender:gender, name:this.name }}let destination = { name:“xuan”};var res = source.bind1(destination,18);var person = new res(“male”);console.log(person);打印效果如下:我们发现bind函数支持new关键字,调用的时候this的绑定失效了,那么new之后,this指向哪里呢?我们来试一下,代码如下:function source(age,gender){ console.log(this);}let destination = { name:“xuan”};var res = source.bind(destination,18);console.log(new res(“male”));console.log(res(“male”));执行new的时候,我们发现虽然bind的第一个参数是destination,但是this是指向source的。不用new的话,this指向destination。好,现在再来回顾一下我们的bind1实现:Function.prototype.bind1 = function(ctx,…args){ var that = this; return function(){ //将外层函数的参数和内层函数的参数合并 var all_args = […args].concat([…arguments]); //因为ctx是外层的this指针,在外层我们使用一个变量that引用进来 return that.apply(ctx,all_args); }}如果我们使用:var res = source.bind(destination,18);console.log(new res(“male”));如果执行上述代码,我们的ctx还是destination,也就是说这个时候下面的source函数中的ctx还是指向destination。而根据Function.prototype.bind的用法,这时this应该是指向source自身。我们先把部分代码抄下来:function source(age,gender){ console.log(this.name); console.log(age); console.log(gender); //添加一个返回值对象 return { age:age, gender:gender, name:this.name }}let destination = { name:“xuan”};我们改一下bind1函数:Function.prototype.bind1 = function (ctx, …args) { var that = this;//that肯定是source //定义了一个函数 let f = function () { //将外层函数的参数和内层函数的参数合并 var all_args = […args].concat([…arguments]); //因为ctx是外层的this指针,在外层我们使用一个变量that引用进来 var real_ctx = this instanceof f ? this : ctx; return that.apply(real_ctx, all_args); } //函数的原型指向source的原型,这样执行new f()的时候this就会通过原型链指向source f.prototype = this.prototype; //返回函数 return f;}我们执行var res = source.bind1(destination,18);console.log(new res(“male”));效果如下:已经达到我们的效果!现在分析一下上述实现的代码://调用var res = source.bind1(destination,18)时的代码分析Function.prototype.bind1 = function (ctx, …args) { var that = this;//that肯定是source //定义了一个函数 let f = function () { … //内部先不管 } //函数的原型指向source的原型,这样执行new f()的时候this就会指向一个新家的对象,这个对象通过原型链指向source,这正是我们上面执行apply的时候需要传入的参数 //f.prototype==>source.prototype f.prototype = this.prototype; //返回函数 return f;}f()函数的内部实现分析://new res(“male”)相当于运行new f(“male”);下面进行函数的运行态分析let f = function () { console.log(this);//这个时候打印this就是一个_proto_指向f.prototype的对象,因为f.prototype==>source.prototype,所以this.proto==>source.prototype //将外层函数的参数和内层函数的参数合并 var all_args = […args].concat([…arguments]); //正常不用new的时候this指向当前调用处的this指针(在全局环境中执行,this就是window对象);使用new的话这个this对象的原型链上有一个类型是f的原型对象。 //那么判断一下,如果this instanceof f,那么real_ctx=this,否则real_ctx=ctx; var real_ctx = this instanceof f ? this : ctx; //现在把真正分配给source函数的对象传入 return that.apply(real_ctx, all_args);}至此bind()函数的模拟实现完毕!如有不对之处,欢迎拍砖!您的宝贵意见是我写作的动力,谢谢大家。 ...

March 14, 2019 · 2 min · jiezi

JS中的call、apply、bind方法详解

bind 是返回对应函数,便于稍后调用;apply 、call 则是立即调用 。apply、call在 javascript 中,call 和 apply 都是为了改变某个函数运行时的上下文(context)而存在的,换句话说,就是为了改变函数体内部 this 的指向。JavaScript 的一大特点是,函数存在「定义时上下文」和「运行时上下文」以及「上下文是可以改变的」这样的概念。function fruits() {} fruits.prototype = { color: “red”, say: function() { console.log(“My color is " + this.color); }} var apple = new fruits;apple.say(); //My color is red但是如果我们有一个对象banana= {color : “yellow”} ,我们不想对它重新定义 say 方法,那么我们可以通过 call 或 apply 用 apple 的 say 方法:banana = { color: “yellow”}apple.say.call(banana); //My color is yellowapple.say.apply(banana); //My color is yellow所以,可以看出 call 和 apply 是为了动态改变 this 而出现的,当一个 object 没有某个方法(本栗子中banana没有say方法),但是其他的有(本栗子中apple有say方法),我们可以借助call或apply用其它对象的方法来操作。apply、call 区别对于 apply、call 二者而言,作用完全一样,只是接受参数的方式不太一样。例如,有一个函数定义如下:var func = function(arg1, arg2) { };就可以通过如下方式来调用:func.call(this, arg1, arg2);func.apply(this, [arg1, arg2])其中 this 是你想指定的上下文,他可以是任何一个 JavaScript 对象(JavaScript 中一切皆对象),call 需要把参数按顺序传递进去,而 apply 则是把参数放在数组里。 为了巩固加深记忆,下面列举一些常用用法:apply、call实例数组之间追加var array1 = [12 , “foo” , {name:“Joe”} , -2458]; var array2 = [“Doe” , 555 , 100]; Array.prototype.push.apply(array1, array2); // array1 值为 [12 , “foo” , {name:“Joe”} , -2458 , “Doe” , 555 , 100] 获取数组中的最大值和最小值var numbers = [5, 458 , 120 , -215 ]; var maxInNumbers = Math.max.apply(Math, numbers), //458 maxInNumbers = Math.max.call(Math,5, 458 , 120 , -215); //458number 本身没有 max 方法,但是 Math 有,我们就可以借助 call 或者 apply 使用其方法。验证是否是数组(前提是toString()方法没有被重写过)functionisArray(obj){ return Object.prototype.toString.call(obj) === ‘[object Array]’ ;}类(伪)数组使用数组方法var domNodes = Array.prototype.slice.call(document.getElementsByTagName(”"));Javascript中存在一种名为伪数组的对象结构。比较特别的是 arguments 对象,还有像调用 getElementsByTagName , document.childNodes 之类的,它们返回NodeList对象都属于伪数组。不能应用 Array下的 push , pop 等方法。但是我们能通过 Array.prototype.slice.call 转换为真正的数组的带有 length 属性的对象,这样 domNodes 就可以应用 Array 下的所有方法了。面试题定义一个 log 方法,让它可以代理 console.log 方法,常见的解决方法是:function log(msg) { console.log(msg);}log(1); //1log(1,2); //1上面方法可以解决最基本的需求,但是当传入参数的个数是不确定的时候,上面的方法就失效了,这个时候就可以考虑使用 apply 或者 call,注意这里传入多少个参数是不确定的,所以使用apply是最好的,方法如下:function log(){ console.log.apply(console, arguments);};log(1); //1log(1,2); //1 2接下来的要求是给每一个 log 消息添加一个"(app)“的前辍,比如:log(“hello world”); //(app)hello world该怎么做比较优雅呢?这个时候需要想到arguments参数是个伪数组,通过 Array.prototype.slice.call 转化为标准数组,再使用数组方法unshift,像这样:function log(){ var args = Array.prototype.slice.call(arguments); args.unshift(’(app)’); console.log.apply(console, args);};bind在讨论bind()方法之前我们先来看一道题目:var altwrite = document.write;altwrite(“hello”);结果:Uncaught TypeError: Illegal invocationaltwrite()函数改变this的指向global或window对象,导致执行时提示非法调用异常,正确的方案就是使用bind()方法:altwrite.bind(document)(“hello”)当然也可以使用call()方法:altwrite.call(document, “hello”)绑定函数bind()最简单的用法是创建一个函数,使这个函数不论怎么调用都有同样的this值。常见的错误就像上面的例子一样,将方法从对象中拿出来,然后调用,并且希望this指向原来的对象。如果不做特殊处理,一般会丢失原来的对象。使用bind()方法能够很漂亮的解决这个问题:this.num = 9; var mymodule = { num: 81, getNum: function() { console.log(this.num); }};mymodule.getNum(); // 81var getNum = mymodule.getNum;getNum(); // 9, 因为在这个例子中,“this"指向全局对象var boundGetNum = getNum.bind(mymodule);boundGetNum(); // 81bind() 方法与 apply 和 call 很相似,也是可以改变函数体内 this 的指向。MDN的解释是:bind()方法会创建一个新函数,称为绑定函数,当调用这个绑定函数时,绑定函数会以创建它时传入 bind()方法的第一个参数作为 this,传入 bind() 方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数。直接来看看具体如何使用,在常见的单体模式中,通常我们会使用 _this , that , self 等保存 this ,这样我们可以在改变了上下文之后继续引用到它。 像这样:var foo = { bar : 1, eventBind: function(){ var _this = this; $(’.someClass’).on(‘click’,function(event) { / Act on the event / console.log(_this.bar); //1 }); }}由于 Javascript 特有的机制,上下文环境在 eventBind:function(){ } 过渡到 $(’.someClass’).on(‘click’,function(event) { }) 发生了改变,上述使用变量保存 this 这些方式都是有用的,也没有什么问题。当然使用 bind() 可以更加优雅的解决这个问题:var foo = { bar : 1, eventBind: function(){ $(’.someClass’).on(‘click’,function(event) { / Act on the event */ console.log(this.bar); //1 }.bind(this)); }}在上述代码里,bind() 创建了一个函数,当这个click事件绑定在被调用的时候,它的 this 关键词会被设置成被传入的值(这里指调用bind()时传入的参数)。因此,这里我们传入想要的上下文 this(其实就是 foo ),到 bind() 函数中。然后,当回调函数被执行的时候, this 便指向 foo 对象。再来一个简单的栗子:var bar = function(){console.log(this.x);}var foo = {x:3}bar(); // undefinedvar func = bar.bind(foo);func(); // 3这里我们创建了一个新的函数 func,当使用 bind() 创建一个绑定函数之后,它被执行的时候,它的 this 会被设置成 foo , 而不是像我们调用 bar() 时的全局作用域。偏函数(Partial Functions)Partial Functions也叫Partial Applications,这里截取一段关于偏函数的定义:Partial application can be described as taking a function that accepts some number of arguments, binding values to one or more of those arguments, and returning a new function that only accepts the remaining, un-bound arguments.这是一个很好的特性,使用bind()我们设定函数的预定义参数,然后调用的时候传入其他参数即可:function list() { return Array.prototype.slice.call(arguments);}var list1 = list(1, 2, 3); // [1, 2, 3]// 预定义参数37var leadingThirtysevenList = list.bind(undefined, 37);var list2 = leadingThirtysevenList(); // [37]var list3 = leadingThirtysevenList(1, 2, 3); // [37, 1, 2, 3]和setTimeout一起使用function Bloomer() { this.petalCount = Math.ceil(Math.random() * 12) + 1;}// 1秒后调用declare函数Bloomer.prototype.bloom = function() { window.setTimeout(this.declare.bind(this), 100);};Bloomer.prototype.declare = function() { console.log(‘我有 ’ + this.petalCount + ’ 朵花瓣!’);};var bloo = new Bloomer();bloo.bloom(); //我有 5 朵花瓣!注意:对于事件处理函数和setInterval方法也可以使用上面的方法绑定函数作为构造函数绑定函数也适用于使用new操作符来构造目标函数的实例。当使用绑定函数来构造实例,注意:this会被忽略,但是传入的参数仍然可用。function Point(x, y) { this.x = x; this.y = y;}Point.prototype.toString = function() { console.log(this.x + ‘,’ + this.y);};var p = new Point(1, 2);p.toString(); // ‘1,2’var emptyObj = {};var YAxisPoint = Point.bind(emptyObj, 0/x/);// 实现中的例子不支持,// 原生bind支持:var YAxisPoint = Point.bind(null, 0/x/);var axisPoint = new YAxisPoint(5);axisPoint.toString(); // ‘0,5’axisPoint instanceof Point; // trueaxisPoint instanceof YAxisPoint; // truenew Point(17, 42) instanceof YAxisPoint; // true捷径bind()也可以为需要特定this值的函数创造捷径。例如要将一个类数组对象转换为真正的数组,可能的例子如下:var slice = Array.prototype.slice;// …slice.call(arguments);如果使用bind()的话,情况变得更简单:var unboundSlice = Array.prototype.slice;var slice = Function.prototype.call.bind(unboundSlice);// …slice(arguments);实现上面的几个小节可以看出bind()有很多的使用场景,但是bind()函数是在 ECMA-262 第五版才被加入;它可能无法在所有浏览器上运行。这就需要我们自己实现bind()函数了。首先我们可以通过给目标函数指定作用域来简单实现bind()方法:Function.prototype.bind = function(context){ self = this; //保存this,即调用bind方法的目标函数 return function(){ return self.apply(context,arguments); };};考虑到函数柯里化的情况,我们可以构建一个更加健壮的bind():Function.prototype.bind = function(context){ var args = Array.prototype.slice.call(arguments, 1), self = this; return function(){ var innerArgs = Array.prototype.slice.call(arguments); var finalArgs = args.concat(innerArgs); return self.apply(context,finalArgs); };};这次的bind()方法可以绑定对象,也支持在绑定的时候传参。继续,Javascript的函数还可以作为构造函数,那么绑定后的函数用这种方式调用时,情况就比较微妙了,需要涉及到原型链的传递:Function.prototype.bind = function(context){ var args = Array.prototype.slice(arguments, 1), F = function(){}, self = this, bound = function(){ var innerArgs = Array.prototype.slice.call(arguments); var finalArgs = args.concat(innerArgs); return self.apply((this instanceof F ? this : context), finalArgs); }; F.prototype = self.prototype; bound.prototype = new F(); return bound;};这是《JavaScript Web Application》一书中对bind()的实现:通过设置一个中转构造函数F,使绑定后的函数与调用bind()的函数处于同一原型链上,用new操作符调用绑定后的函数,返回的对象也能正常使用instanceof,因此这是最严谨的bind()实现。对于为了在浏览器中能支持bind()函数,只需要对上述函数稍微修改即可:Function.prototype.bind = function (oThis) { if (typeof this !== “function”) { throw new TypeError(“Function.prototype.bind - what is trying to be bound is not callable”); } var aArgs = Array.prototype.slice.call(arguments, 1), fToBind = this, fNOP = function () {}, fBound = function () { return fToBind.apply( this instanceof fNOP && oThis ? this : oThis || window, aArgs.concat(Array.prototype.slice.call(arguments)) ); }; fNOP.prototype = this.prototype; fBound.prototype = new fNOP(); return fBound; };有个有趣的问题,如果连续 bind() 两次,亦或者是连续 bind() 三次那么输出的值是什么呢?像这样:var bar = function(){ console.log(this.x);}var foo = { x:3}var sed = { x:4}var func = bar.bind(foo).bind(sed);func(); //? var fiv = { x:5}var func = bar.bind(foo).bind(sed).bind(fiv);func(); //?答案是,两次都仍将输出 3 ,而非期待中的 4 和 5 。原因是,在Javascript中,多次 bind() 是无效的。更深层次的原因, bind() 的实现,相当于使用函数在内部包了一个 call / apply ,第二次 bind() 相当于再包住第一次 bind() ,故第二次以后的 bind 是无法生效的。apply、call、bind比较那么 apply、call、bind 三者相比较,之间又有什么异同呢?何时使用 apply、call,何时使用 bind 呢。简单的一个栗子:var obj = { x: 81,}; var foo = { getX: function() { return this.x; }} console.log(foo.getX.bind(obj)()); //81console.log(foo.getX.call(obj)); //81console.log(foo.getX.apply(obj)); //81三个输出的都是81,但是注意看使用 bind() 方法的,他后面多了对括号。也就是说,区别是,当你希望改变上下文环境之后并非立即执行,而是回调执行的时候,使用 bind() 方法。而 apply/call 则会立即执行函数。再总结一下:apply 、 call 、bind 三者都是用来改变函数的this对象的指向的;apply 、 call 、bind 三者第一个参数都是this要指向的对象,也就是想指定的上下文;apply 、 call 、bind 三者都可以利用后续参数传参;bind 是返回对应函数,便于稍后调用;apply 、call 则是立即调用 。 ...

February 25, 2019 · 4 min · jiezi

「干货」细说 call、apply 以及 bind 的区别和用法未指定标题的文章

前言上一篇文章 《「前端面试题系列4」this 的原理以及用法》 中,提到了 call 和 apply。它们最主要的作用,是改变 this 的指向。在平时的工作中,除了在写一些基础类,或者公用库方法的时候会用到它们,其他时候 call 和 apply 的应用场景并不多。不过,突然遇到的时候,需要想一下才能转过弯来。所以今天,就让我们好好地探究一下,这两个方法的区别以及一些妙用。最后,还会介绍与之用法相似的 bind 的方法。call 和 apply 的共同点它们的共同点是,都能够改变函数执行时的上下文,将一个对象的方法交给另一个对象来执行,并且是立即执行的。为何要改变执行上下文?举一个生活中的小例子:平时没时间做饭的我,周末想给孩子炖个腌笃鲜尝尝。但是没有适合的锅,而我又不想出去买。所以就问邻居借了一个锅来用,这样既达到了目的,又节省了开支,一举两得。改变执行上下文也是一样的,A 对象有一个方法,而 B 对象因为某种原因,也需要用到同样的方法,那么这时候我们是单独为 B 对象扩展一个方法呢,还是借用一下 A 对象的方法呢?当然是借用 A 对象的啦,既完成了需求,又减少了内存的占用。另外,它们的写法也很类似,调用 call 和 apply 的对象,必须是一个函数 Function。接下来,就会说到具体的写法,那也是它们区别的主要体现。call 和 apply 的区别它们的区别,主要体现在参数的写法上。先来看一下它们各自的具体写法。call 的写法Function.call(obj,[param1[,param2[,…[,paramN]]]])需要注意以下几点:调用 call 的对象,必须是个函数 Function。call 的第一个参数,是一个对象。 Function 的调用者,将会指向这个对象。如果不传,则默认为全局对象 window。第二个参数开始,可以接收任意个参数。每个参数会映射到相应位置的 Function 的参数上。但是如果将所有的参数作为数组传入,它们会作为一个整体映射到 Function 对应的第一个参数上,之后参数都为空。function func (a,b,c) {}func.call(obj, 1,2,3)// func 接收到的参数实际上是 1,2,3func.call(obj, [1,2,3])// func 接收到的参数实际上是 [1,2,3],undefined,undefinedapply 的写法Function.apply(obj[,argArray])需要注意的是:它的调用者必须是函数 Function,并且只接收两个参数,第一个参数的规则与 call 一致。第二个参数,必须是数组或者类数组,它们会被转换成类数组,传入 Function 中,并且会被映射到 Function 对应的参数上。这也是 call 和 apply 之间,很重要的一个区别。func.apply(obj, [1,2,3])// func 接收到的参数实际上是 1,2,3func.apply(obj, { 0: 1, 1: 2, 2: 3, length: 3})// func 接收到的参数实际上是 1,2,3什么是类数组?先说数组,这我们都熟悉。它的特征有:可以通过角标调用,如 array[0];具有长度属性length;可以通过 for 循环或forEach方法,进行遍历。那么,类数组是什么呢?顾名思义,就是具备与数组特征类似的对象。比如,下面的这个对象,就是一个类数组。let arrayLike = { 0: 1, 1: 2, 2: 3, length: 3};类数组 arrayLike 可以通过角标进行调用,具有length属性,同时也可以通过 for 循环进行遍历。类数组,还是比较常用的,只是我们平时可能没注意到。比如,我们获取 DOM 节点的方法,返回的就是一个类数组。再比如,在一个方法中使用 arguments 获取到的所有参数,也是一个类数组。但是需要注意的是:类数组无法使用 forEach、splice、push 等数组原型链上的方法,毕竟它不是真正的数组。call 和 apply 的用途下面会分别列举 call 和 apply 的一些使用场景。声明:例子中没有哪个场景是必须用 call 或者必须用 apply 的,只是个人习惯这么用而已。call 的使用场景1、对象的继承。如下面这个例子:function superClass () { this.a = 1; this.print = function () { console.log(this.a); }}function subClass () { superClass.call(this); this.print();}subClass();// 1subClass 通过 call 方法,继承了 superClass 的 print 方法和 a 变量。此外,subClass 还可以扩展自己的其他方法。2、借用方法。还记得刚才的类数组么?如果它想使用 Array 原型链上的方法,可以这样:let domNodes = Array.prototype.slice.call(document.getElementsByTagName("*"));这样,domNodes 就可以应用 Array 下的所有方法了。apply 的一些妙用1、Math.max。用它来获取数组中最大的一项。let max = Math.max.apply(null, array);同理,要获取数组中最小的一项,可以这样:let min = Math.min.apply(null, array);2、实现两个数组合并。在 ES6 的扩展运算符出现之前,我们可以用 Array.prototype.push来实现。let arr1 = [1, 2, 3];let arr2 = [4, 5, 6];Array.prototype.push.apply(arr1, arr2);console.log(arr1); // [1, 2, 3, 4, 5, 6]bind 的使用最后来说说 bind。在 MDN 上的解释是:bind() 方法创建一个新的函数,在调用时设置 this 关键字为提供的值。并在调用新函数时,将给定参数列表作为原函数的参数序列的前若干项。它的语法如下:Function.bind(thisArg[, arg1[, arg2[, …]]])bind 方法 与 apply 和 call 比较类似,也能改变函数体内的 this 指向。不同的是,bind 方法的返回值是函数,并且需要稍后调用,才会执行。而 apply 和 call 则是立即调用。来看下面这个例子:function add (a, b) { return a + b;}function sub (a, b) { return a - b;}add.bind(sub, 5, 3); // 这时,并不会返回 8add.bind(sub, 5, 3)(); // 调用后,返回 8如果 bind 的第一个参数是 null 或者 undefined,this 就指向全局对象 window。总结call 和 apply 的主要作用,是改变对象的执行上下文,并且是立即执行的。它们在参数上的写法略有区别。bind 也能改变对象的执行上下文,它与 call 和 apply 不同的是,返回值是一个函数,并且需要稍后再调用一下,才会执行。最后,分享一个在知乎上看到的,关于 call 和 apply 的便捷记忆法:猫吃鱼,狗吃肉,奥特曼打小怪兽。有天狗想吃鱼了猫.吃鱼.call(狗,鱼)狗就吃到鱼了猫成精了,想打怪兽奥特曼.打小怪兽.call(猫,小怪兽)猫也可以打小怪兽了PS:欢迎关注我的公众号 “超哥前端小栈”,交流更多的想法与技术。 ...

January 27, 2019 · 2 min · jiezi

手写call、apply、bind及相关面试题解析

它们有什么不同?怎么用?call 接收多个参数,第一个为函数上下文也就是this,后边参数为函数本身的参数。 let obj = { name: “一个” } function allName(firstName, lastName) { console.log(this) console.log(我的全名是“${firstName}${this.name}${lastName}”) } // 很明显此时allName函数是没有name属性的 allName(‘我是’, ‘前端’) //我的全名是“我是前端” this指向window allName.call(obj, ‘我是’, ‘前端’) //我的全名是“我是一个前端” this指向objapplyapply接收两个参数,第一个参数为函数上下文this,第二个参数为函数参数只不过是通过一个数组的形式传入的。allName.apply(obj, [‘我是’, ‘前端’])//我的全名是“我是一个前端” this指向objbindbind 接收多个参数,第一个是bind返回值返回值是一个函数上下文的this,不会立即执行。 let obj = { name: “一个” } function allName(firstName, lastName, flag) { console.log(this) console.log(我的全名是"${firstName}${this.name}${lastName}"我的座右铭是"${flag}") } allName.bind(obj) //不会执行 let fn = allName.bind(obj) fn(‘我是’, ‘前端’, ‘好好学习天天向上’) // 也可以这样用,参数可以分开传。bind后的函数参数默认排列在原函数参数后边 fn = allName.bind(obj, “你是”) fn(‘前端’, ‘好好学习天天向上’)接下来搓搓手实现call、apply和bind实现call let Person = { name: ‘Tom’, say() { console.log(this) console.log(我叫${this.name}) } } // 先看代码执行效果 Person.say() //我叫Tom Person1 = { name: ‘Tom1’ } // 我们尝试用原生方法call来实现this指向Person1 Person.say.call(Person1) //我叫Tom1通过第一次打印执行和第二次打印执行我发现,如果Person1有say方法那么Person1直接执行Person1.say() 结果就是我是Tom1,是的call就是这么实现的。再看代码 Function.prototype.MyCall = function(context) { //context就是demo中的Person1 // 必须此时调用MyCall的函数是say方法,那么我们只需要在context上扩展一个say方法指向调用MyCall的say方法这样this console.log(this) context.say = this //Mycall里边的this就是我们虚拟的say方法 context.say() } // 测试 Person.say.MyCall(Person1)//我叫Tom1perfect!爆棚的满足感!不过拿脚趾头想想也不会这么简单,继续完善我们自己找茬1、call支持多个参数,有可能一个也不没有2、考虑多参数时要把参数传给扩展方法。3、给上下文定义的函数要保持唯一不能是say4、扩展完我们需要吧自定义函数删除接下来针对找茬问题一一解决 let Person = { name: ‘Tom’, say() { console.log(this) console.log(我叫${this.name}) } } Person1 = { name: ‘Tom1’ } //如果没有参数 Person.say.call()没有指定this,this指向window我们也要这样 Function.prototype.MyCall = function(context) { // 如果没有参数我们参考call的处理方式 context = context || window //context就是demo中的Person1 // 必须此时调用MyCall的函数是say方法,那么我们只需要在context上扩展一个say方法指向调用MyCall的say方法这样this context.say = this //Mycall里边的this就是我们虚拟的say方法 context.say() } Person.say.MyCall()没毛病!继续解决// 找茬2:我们默认定义context.say = this fn如果已经被占用 嘎嘎 sb了。 不怕 搞定它 // say需要是一个唯一值 是不是突然想到es6的新类型 Symbol fn = Symbol() 不过我们装逼不嫌事大 都说自己实现了 function mySymbol(obj) { // 不要问我为什么这么写,我也不知道就感觉这样nb let unique = (Math.random() + new Date().getTime()).toString(32).slice(0, 8) // 牛逼也要严谨 if (obj.hasOwnProperty(unique)) { return mySymbol(obj) //递归调用 } else { return unique } }//接下来我们一并把多参数和执行完删除自定义方法删除掉一块搞定 Function.prototype.myCall1 = function(context) { // 如果没有传或传的值为空对象 context指向window context = context || window let fn = mySymbol(context) context.fn = this //给context添加一个方法 指向this // 处理参数 去除第一个参数this 其它传入fn函数 let arg = […arguments].slice(1) //[…xxx]把类数组变成数组,arguments为啥不是数组自行搜索 slice返回一个新数组 context.fn(…arg) //执行fn delete context.fn //删除方法 } let Person = { name: ‘Tom’, say(age) { console.log(this) console.log(我叫${this.name}我今年${age}) } } Person1 = { name: ‘Tom1’ } Person.say.call(Person1,18)//我叫Tom1我今年18测试结果相当完美!实现apply接下来apply就简单多了,只有多参数时第二个参数是数组,就不一步步细说了。 Function.prototype.myApply = function(context) { // 如果没有传或传的值为空对象 context指向window if (typeof context === “undefined” || context === null) { context = window } let fn = mySymbol(context) context.fn = this //给context添加一个方法 指向this // 处理参数 去除第一个参数this 其它传入fn函数 let arg = […arguments].slice(1) //[…xxx]把类数组变成数组,arguments为啥不是数组自行搜索 slice返回一个新数组 context.fn(arg) //执行fn delete context.fn //删除方法 }实现bind这个和call、apply区别还是很大的,容我去抽根烟回来收拾它还是老套路先分析bind都能干些什么,有什么特点 1、函数调用,改变this 2、返回一个绑定this的函数 3、接收多个参数 4、支持柯里化形式传参 fn(1)(2) Function.prototype.bind = function(context) { //返回一个绑定this的函数,我们需要在此保存this let self = this // 可以支持柯里化传参,保存参数 let arg = […arguments].slice(1) // 返回一个函数 return function() { //同样因为支持柯里化形式传参我们需要再次获取存储参数 let newArg = […arguments] console.log(newArg) // 返回函数绑定this,传入两次保存的参数 //考虑返回函数有返回值做了return return self.apply(context, arg.concat(newArg)) } } // 搞定测试 let fn = Person.say.bind(Person1) fn() fn(18)是的,完美,实现了绑定this,返回函数,不立即执行,可以柯里化形式传参。简版的实现就算完成了欢迎吐槽or点赞! ...

January 18, 2019 · 2 min · jiezi

typescript 3.2 新编译选项strictBindCallApply

突发错误我的gels项目(https://github.com/zhoutk/gels),几天没动,突然tsc编译出错,信息如下:src/app.ts:28:38 - error TS2345: Argument of type ‘any[]’ is not assignable to parameter of type ‘[Middleware<ParameterizedContext<any, {}>>]’. Property ‘0’ is missing in type ‘any[]’ but required in type ‘[Middleware<ParameterizedContext<any, {}>>]’.28 m && (app.use.apply(app, [].concat(m)))我的源代码,是动态加载Koa的中间件,app是koa2实例 for (let m of [].concat(middleware)) { m && (app.use.apply(app, [].concat(m))) }问题分析几天前还是正常编译、正常运行的项目,突然出错,应该是环境变了。经查找,发现全局typescript已经升级到了最新版本,3.2.2,而项目中的版本是3.0.3。 将全局版本换回3.0.3,编译通过,问题找到。问题定位上typescrpt的github主页,找发布日志,发现3.2的新功能,第一条就是:TypeScript 3.2 introduces a new –strictBindCallApply compiler option (in the –strict family of options) with which the bind, call, and apply methods on function objects are strongly typed and strictly checked.大概意思是:TypeScript 3.2引入一个新的编译选项 –strictBindCallApply,若使用这个选项,函数对象的bind,call和apply方法是强类型的,并进行严格检测。解决方案因为是动态参数,想了半天我没法明确声明类型。因此,我在tsconfig.json配置文件中,设置"strictBindCallApply": false,将这个编译选项关闭,这样就可以将typescript升级到3.2.2了。哪位朋友若有能打开strictBindCallApply选项,又能通过编译的方案,烦请告知一下,谢谢!我若哪天找到方法,会马上更新本文章。 ...

January 2, 2019 · 1 min · jiezi