讲解call、apply及bind对this修改指针指向方法的使用、区别及实现。
前言
本文主要内容是对call、apply及bind的功能及使用方法进行介绍,之后会通过js原生实现这三种方法,让我们更深入地了解其中的作用与原理。但由于多数介绍的实现方法的过程中对this、arguments、rest、解构赋值及扩展运算符有一定涉及,本文会在先导中先做一个简单介绍,可能会对之后正文对bind等方法内容的讲解有一定理解上的帮助。
先导
-
this
很多对call、apply及bind的讲解中都会对this有较为详细的介绍,本文在前人的基础上简略做个介绍。需要重点突出重复 的就关于this的一个问题:this永远指向最后被调用的地方。
以下是一个简单的例子:
var name = 'windowPart' const testObj = { name: 'objPart', func: function(){console.log(this.name)} } // testObj调用func testObj.func() // objPart this -> obj // outer最终调用func const outer = testObj.func outer() // windowPart this -> window
简单就能看出在对象被调用后this会改变其指向,指到最后被调用的地方。因此在开发过程中对this指向多加留意是很必要的,一不留心可能就会出现bug,此时有对call、apply及bind方法能熟练掌握的话就能派上很大的用场。
-
arguments、rest、...扩展运算符及解构赋值
-
...扩展运算符与解构赋值
-
对于...扩展运算符,该运算符主要用于函数调用,通常可用于对数组的解构,ES8将其引入对象,对于整形、布尔值、字符串等等,扩展运算符也可将其扩展赋值,其原理是将其他类型的变量转化为对象后,将其展开,类似于Object.assign()方法。
以下是简单用法:
let arr = [1, 2, 3] console.log(...arr) // 1 2 3
-
而对于解构赋值,其可以在数组、对象、字符串甚至数值和布尔值中运用。
用法如下:
let arr = [1, 2, 3] let [a, b, c] = arr console.log('a:', a) // a: 1 console.log('b:', b) // b: 2 console.log('c:', c) // c: 3</code></pre>
基于这样“匹配赋值”的模式,我们可以完成更多复杂的解构赋值,例如嵌套解构等等。
-
但对于解构在对象上的运用,需要注意的是,其是取出参数对象的所有可遍历属性,并且在完成类似深拷贝时,申明的变量名必须为扩展对象中存在的key值,此处相当于调用了一次get(keyname)方法,其简单运用如下:
let obj = { me: '我', you: '你' } let {me, err} = {...obj} console.log(me) // 我 console.log(err) // undefined
-
-
arguments与rest
-
arguments是一种类数组对象,其只能够在函数内部调用,主要包含着该函数的参数,其中也有所指代的Argument对象的一些其他属性,例如简单的length属性,此处不做详解。
所谓类数组对象,其在基本使用上与数组并无异同,但对于自身属性,arguments不能使用数组中push、pop等方法,基本使用如下:
function argsFunc () { console.log('arguments:', arguments) } argsFunc(1, 2, 3) // arguments: [Arguments] { '0': 1, '1': 2, '2': 3 }
值得注意的是类数组对象通过扩展运算符可以很方便转化为数组,通过以下例子可以理解:
function argsFunc (...arguments) { console.log('展开后的arguments:', arguments) } argsFunc(1, 2, 3) // 展开后的arguments: [ 1, 2, 3 ]
-
ES6引入的rest参数,相当于数组扩展运算符的逆运算,在函数参数中,运用rest可以将arguments对象进行解构取值,在定义函数时若其中存在可以归为一类的参数,此时我们加以运用rest会显得很亮眼,对函数的书写有很大的精简作用,相较于类数组对象arguments,rest作为数组去包含参数会有更加优秀的使用效率。
以下是一个简单的使用:
function restFunc (str, ...rest) { console.log('str:', str) console.log('rest:', rest) } restFunc('Me', 1, 2) // str: 'Me' // rest: [ 1, 2 ]
-
-
正文
-
call、apply及bind的使用
当被定义的函数在外部调用时,通过call、apply或bind方法将指向window的this指定回该函数中this应该指向的对象,这是保证this指向的情况之一。
简单使用如下:
var name = 'windowPart' const testObj = { name: 'objPart', func: function (...rest) { console.log('this.name:', this.name) console.log('args:', rest) } } let arr = [1, 2] // testObj调用func testObj.func(...arr) // objPart this -> obj // 修改this指向后outer调用func const outer = testObj.func outer.call(testObj, ...arr) outer.apply(testObj, arr) outer.bind(testObj, ...arr)() //以上输出均为: // this.name: objPart // args: [ 1, 2 ]
在coding时,对于用到this的地方,一定要多加注意this指向丢失问题,不论后期在何处调用一定先将this绑定好,上例为更好理解是在调用处指回函数内部this本应指回的对象。在开发过程中,我们不仅可以在赋值给全局变量后调用时通过call、apply及bind方法将丢失指向了window的this绑定回,也可如以下方法不通过赋值给全局变量后调用并在用到this时就提前绑定好避免this丢失的情况发生:
// 方法一:通过bind(this)绑定好上一级this var name = 'windowPart' const testObj = { name: 'objPart', func: function () { setTimeout(function(){ console.log('绑定后 this.name:', this.name) // this -> testObj }.bind(this), 1000) } } testObj.func() // 绑定后 this.name: objPart //方法二:通过_this保留上级this var name = 'windowPart' const testObj = { name: 'objPart', func: function () { let _this = this setTimeout(function(){ console.log('绑定后_this.name:', _this.name) // _this -> testObj console.log('未绑定 this.name:', this.name) // this -> window }, 1000) } } testObj.func() // 绑定后_this.name: objPart // 未绑定 this.name: windowPart //方法三:通过箭头函数this指向上一级对象 var name = 'windowPart' const testObj = { name: 'objPart', func: function () { setTimeout(() => { console.log('箭头函数内 this.name:', this.name) // this -> testObj }, 1000) } } testObj.func() // 箭头函数内 this.name: objPart
-
call、apply及bind的异同
通过上一示例,可以看出对于call、apply及bind的区别有以下:
- call与apply之间: 第一个参数均为this应指向的对象,而对于其余参数,call需要展开传递,apply则需要将其以数组形式传递;
- call与bind之间: 第一个参数均为this应指向的对象这一相同之处不变,call及bind其与参数传递形式也相同(展开传递),但这两种方法在返回值上有一个细节的不同,call方法直接将需要修改this指向指定回的函数直接执行,返回该函数内部的返回值;而bind方法,则返回一个function对象 即 需要修改内部this指向指定回的函数,最后再被调用才会最后执行该函数。
以上如果不好理解,通过下一节自己js手写这三个方法会有很清晰的认识。ヾ(◍°∇°◍)ノ゙加油
-
js手写call、apply及bind
-
call方法实现
const arr = [1, 2] function testFunc (num1, num2) { console.log('this.name:', this.name) console.log('num1:', num1) console.log('num2:', num2) } const testObj = { name: 'objName' } Function.prototype.myCall = function (testObj, ...rest) { if(arguments.length == 2 && Array.isArray(arguments[1])) { try{ throw new Error('myCall参数错误!') } catch (e) { console.log(e.name + ': ' + e.message) } return } console.log(this) // 此处输出便于理解输出一下this内容 // this -> 最后调用myCall方法的[Function: testFunc] testObj._fn = this var ret = testObj._fn(...rest) delete testObj._fn return ret } testFunc.myCall(testObj, ...arr) // this.name: objName // num1: 1 // num2: 2
if()部分就是稍微写的细节一点的一个对参数的判断问题。其中最需要解释的一点,在testObj中申明一个_fn,将testFunc赋给_fn,然后通过ret调用testObj._fn(...rest)可以简便的将testFunc作为testObj一个内部属性,从而达到修改testFunc内部this指向testObj的效果。需要注意的是testObj内部利用完的_fn要在最后进行回收处理。
简化原理如下:
const func = function () { console.log(this.name) } const testObj = { name: 'testObj', _fn: func } testObj._fn()
-
apply方法实现
对于apply的js原生实现,仅仅与call在参数传递上有细微的不同,读懂call的原生实现,可以尝试自己完成apply的过程。
const arr = [1, 2] function testFunc (argsArr) { console.log('this.name:', this.name) console.log('argsArr:', argsArr) } const testObj = { name: 'objName' } Function.prototype.myApply = function (testObj, rest) { if(!Array.isArray(rest)) { try{ throw new Error('myApply参数错误!') } catch (e) { console.log(e.name + ': ' + e.message) } return } testObj._fn = this var ret = testObj._fn(rest) delete testObj._fn return ret } testFunc.myApply(testObj, arr) // this.name: objName // argsArr: [ 1, 2 ]
-
bind方法实现
对于bind方法,在使用时就曾提及过一个不同:其返回的是一个函数,所以与call及apply相比在使用上会多出一个bind方法返回的函数可以作为构造函数的情况,以下对bind方法的实现我将由浅入深,逐渐完善,更方便理解。
1. Version 1: 仅考虑 将bind返回函数作为普通函数使用 的情况。
const arr = [1, 2] const testObj = { name: 'objName' } function testFunc(...argsArr) { console.log('this.name:', this.name) console.log('argsArr:', argsArr); } Function.prototype.myBind = function (testObj, ...rest) { if(arguments.length == 2 && Array.isArray(arguments[1])) { try{ throw new Error('myCall参数错误!') } catch (e) { console.log(e.name + ': ' + e.message) } return function () {} } const _this = this let resFn = function () { _this.apply(testObj, rest) } return resFn } testFunc.myBind(testObj, ...arr)() // this.name: objName // argsArr: [1, 2]
2. Version 2: 添加考虑 将bind返回函数作为构造函数使用 的情况。
当bind返回函数作为构造函数使用时 即 new Func() ,此时我们需要注意两个地方:
(1). 因为new不仅自身优先级大且new对this指向改变优先级大于bind方法的问题,会将内部this的指向实例,此处我们需要在做一个判断,对内部调用apply方法需绑定的地方做一个选择。其实此时bind指定的this值会失效,但传入值依然有效。
(2). 对于prototype,在这个情况下,函数被作为构造函数返回就需要将实例需继承该原型中的值。
const arr = [1, 2] const testObj = { name: 'objName' } function testFunc(...argsArr) { console.log('this.name:', this.name) console.log('argsArr:', argsArr) } Function.prototype.myBind = function (testObj, ...rest) { if(arguments.length == 2 && Array.isArray(arguments[1])) { try{ throw new Error('myCall参数错误!') } catch (e) { console.log(e.name + ': ' + e.message) } return function () {} } const _this = this let resFn = function () { _this.apply(this instanceof resFn ? this : testObj, rest) } resFn.prototype = this.prototype return resFn } testFunc.myBind(testObj, ...arr)() // this.name: objName // argsArr: [1, 2] new (testFunc.myBind(testObj, ...arr)) // this.name: undefined // argsArr: [1, 2]
3. Version 3: 优化代码。
对于实例需继承该原型中的值,原型链上的操作,若如上
resFn.prototype = this.prototype
定义,会产生引用赋值共用一个内存地址的情况,发生以下问题:Function.prototype.testBind = function () { let retFunc = function () { } retFunc.prototype = this.prototype return retFunc } function Test1 () {} let Test2 = Test1.testBind() Test2.prototype.a = function () {} const test = new Test2 console.log(Test2.prototype) // {a: ƒ, constructor: ƒ} console.log(Test1.prototype) // {a: ƒ, constructor: ƒ}
因此这个时候我们需要一个空函数中转一下或者使用
Object.create()
,防止对父级原型链的污染。const arr = [1, 2] const testObj = { name: 'objName' } function testFunc(...argsArr) { console.log('this.name:', this.name) console.log('argsArr:', argsArr) } Function.prototype.myBind = function (testObj, ...rest) { if(arguments.length == 2 && Array.isArray(arguments[1])) { try{ throw new Error('myCall参数错误!') } catch (e) { console.log(e.name + ': ' + e.message) } return function () {} } const _this = this let resFn = function () { _this.apply(this instanceof resFn ? this : testObj, rest) } resFn.prototype = Object.create(this.prototype) // const TempFunc = function () {} // TempFunc.prototype = this.prototype // resFn.prototype = new TempFunc() return resFn } const BindFunc = testFunc.myBind(testObj, ...arr) BindFunc.prototype.a = function () {} var test = new BindFunc console.log(BindFunc.prototype) console.log(testFunc.prototype)
-
相关文章
谢谢以上作者大大~
第一篇文章~完结撒花~*★,°*:.☆( ̄▽ ̄)/$:*.°★*