前言
this指向分为默认绑定,隐式绑定,显式绑定,new操作,箭头函数这几类,经常使用的改变this指向的方法就是call,apply, bind,这些我们经常用到的方法你是否对其实现方式好奇呢?其实搞懂这些并不难,我认为首先必须知道函数内部做了什么事情,他解决的是什么问题,然后我们才能针对性地去探索。
new
首先是最常见的new,new并不是一个函数,是一个运算符,用于创建一个对象的实例,这应该是大家熟知的。用法,new后面跟一个构造函数:
new constructor[([arguments])]
看一下MDN上的解释 new 关键字会进行如下的操作:
- 创建一个空的简单JavaScript对象(即{});
- 链接该对象(设置该对象的constructor)到另一个对象 ;
- 将步骤1新创建的对象作为this的上下文 ;
- 如果该函数没有返回对象,则返回this。
哟西,既然如此我们就围绕上面四点,动手肝一个myNew函数
function myNew() {
// 创建一个空的简单JavaScript对象(即{});
let obj = new Object()
// 链接该对象(设置该对象的constructor)到另一个对象 --> 就是改变obj原型的指向
// arguments是一个类数组,所以要这么调shift
let con = [].shift.call(arguments)
obj.__proto__ = con.prototype
// 将步骤1新创建的对象作为this的上下文 ;
// 如果该函数没有返回对象,则返回this。
let res = con.apply(obj, arguments)
// 如果构造函数person return了一个对象类型,就取return的值
return typeof res == 'object' ? res : obj
}
function person(name) {
this.name = name
}
let p = myNew(person, 'jack')
- 没有单独的this
- 不绑定arguments
- 箭头函数没有prototype属性
结合上述的代码实现,是不是清晰了一些呢?
我们还可以对代码进行优化,使用这种方式来改变和继承属性是对性能影响非常严重的,还会影响到所有继承来自该[[Prototype]]
的对象,更优的方案是: Object.create()
方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。
let obj = Object.create(con.prototype) //
instanceOf
引用自mdn:
instanceof的判断逻辑是: 从当前引用的proto一层一层顺着原型链往上找,能否找到对应的prototype。找到了就返回true。挺简单就可以实现一个简易的instanceof
/*obj 实例 con 构造函数*/
function _instanceOf(obj,con) {
let _obj = obj.__proto__
let _con = con.prototype
while(true) {
if(_obj === null) {
return false
}
if(_obj === _con) {
return true
}
_obj = _obj.__proto__
}
}
缺点: 不能完全精确的判断复杂类型的具体数据类型
- [] instanceof Array; //true
- [] instanceof Object; //true
Bind
先看看官方的例子:
const module = {
x: 42,
getX: function() {
return this.x;
}
};
const unboundGetX = module.getX;
console.log(unboundGetX()); // The function gets invoked at the global scope
// expected output: undefined
const boundGetX = unboundGetX.bind(module);
console.log(boundGetX());
// expected output: 42
由上述例子我们可以知道,使用bind作用有二:
- 返回一个新的函数,给函数添加运行时参数,能使一个函数拥有预设的初始参数,倒是有点像es6的默认参数的作用。
- 显示绑定this值 ,这解决了绑定隐式丢失问题, 即函数中的 this 丢失绑定对象。例如:使用setTimeout,常见情况时执行时this指向 window。当使用对象的方法时,需要 this 引用对象,你可能需要显式地把 this 绑定到回调函数以便继续使用对象。例如:
setInterval(obj.fn.bind(obj), 1000);
基此,我们可以尝试实现一个简单的bind:
function _bind() {
let fn = this //需要绑定的函数
let args = Array.from(arguments) //类数组 -> 数组
let obj = args.shift() //绑定的对象
return function () {
fn.apply(obj, Array.from(args).concat(Array.from(arguments)))
}
}
Function.prototype._bind = _bind
function fn(a, b) {
console.log(this); // obj
console.log(a + b); // 3
}
let obj = {
name: 'violetrosez'
}
let _fn = fn._bind(obj, 1)
_fn(2)
上述代码,看似满足了需求,但是当返回的函数被作为构造函数时,原函数调用的this指向了bind显示指定的对象,不能根据new的调用而绑定到new创建的对象。当被作为构造函数调用时,我们需要将绑定函数的作用域赋给新对象,并设置绑定函数继承目标函数的原型。修改后如下:
function _bind() {
let fn = this //函数
if (typeof fn !== 'function') {
throw new TypeError('what is trying to be bound is not callable');
}
let args = Array.from(arguments)
let obj = args.shift()
let fNOP = Object.create(fn.prototype)
let fBound = function () {
//如果没有判断,构造函数test在执行时也指向obj,而不会指向新建的实例p,此时p.name == undefined
fn.apply(this instanceof fn ? this : obj, Array.from(args).concat(Array.from(arguments)))
}
//使fBound.prototype是fN的实例,返回的fBound若作为new的构造函数,新对象的__proto__就是fN的实例
fBound.prototype = fNOP
return fBound
}
//测试
function test(name) {
this.name = name
}
let obj = {}
let _fn = test._bind(obj)
_fn('violetrosez')
console.log(obj.name); // violetrosez
let p = new _fn('zzzzz')
console.log(obj.name); // violetrosez
console.log(p.name); // zzzzz
call
已经有了上面的基础,我们再写这个有点得心应手了,过程都是一个思路,对参数进行处理
function _call(ctx, ...args) {
if (typeof this !== 'function') {
throw new TypeError('what is trying to be bound is not callable');
}
ctx = ctx || window
ctx.fn = this
let res = ctx.fn(...args)
delete ctx.fn //删除掉引用
return res
}
Function.prototype._call = _call
上面的做法其实是将 this 的默认绑定改为隐式绑定,ctx不存在的时候,我们使ctx指向全局对象,然后将函数作为要绑定的对象的一个方法执行,用完后删掉。这次我们使用es6剩余操作符处理参数,写法比上文更简洁了一些。
apply
apply和call唯一不同的就是他的剩余参数接收的是一个数组,所以把上面的代码改造一下即可。之前一直记混哪个是接受数组,后面干脆把apply的首字母a
当成array
去记忆了..
function _apply(ctx, args = []) {
if (typeof this !== 'function') {
throw new TypeError('what is trying to be bound is not callable');
}
if(args && !Array.isArray(args)) {
throw new TypeError('apply need accept array object');
}
ctx = ctx || window
ctx.fn = this
let res = ctx.fn(...args)
delete ctx.fn //删除掉引用
return res
}
Function.prototype._apply = _apply
后记
至此,我们完成了实现call、apply 和 bind的过程,其实这些本质上都是要改变 this 的指向,在实现过程中一定要时刻搞清楚this的指向,写代码的过程中,什么时候可以用箭头函数,什么时候需要显示绑定this,一定要心中有数。理解了原理之后,就能明白 this 的绑定顺序为什么是 new > 显示绑定 > 隐式绑定 > 默认绑定。 如有疑问或者错误,请各位批评指正,共同进步。求点赞三连QAQ
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!