this
this 机制是 JavaScript 特有的,它是为了解决在对象内部的方法中使用对象内部的属性的需求。
如果不使用 this 来实现对对象内部的属性 JavaScript 可以使用对象.属性访问到:
let people = {
name: 'litangmm',
logName: function(){
console.log(people.name)
}
}
这种方法,方法与对象强耦合。对象变量变化,方法必须接着改变。如果有一个匿名对象数组,那么对象位置改变,方法就得改。
let peoples = [
{name:'litangmm',logName: function(){console.log(peoples[0].name)}},
{name:'litangmm1',logName: function(){console.log(peoples[1].name)}},
{name:'litangmm2',logName: function(){console.log(peoples[2].name)}},
]
于是,this 出现了。
this 是和执行上下文绑定的。this,是运行时确定的。
- 在全局环境下,this 指向全局的 window。
- 作为函数执行时,如果是作为对象的函数执行,则指向该对象本身,否则指向全局的window。
- 1 2都是非严格模式下的,全局模式下,this 拿不到全局 window 而是
undefined
。
还有 bind apply call new,它们和this紧密相关。
bind apply call 都是属于函数的原型方法,可以改变函数执行的 this 的指向。它们的原理都是:
作为函数执行时,如果是作为对象的函数执行,则指向该对象本身。
只不过在一些细节上实现不同。下面,通过手写它们的实现来理解它们的异同。
call 和 apply
执行效果
- 改变 thisarg
- 接收函数入参
- 执行函数
说明
- 对于 thisarg ,在非严格模式下,如果 thisarg 为 null 或 undefined ,thisarg 会为 全局 window。
- 对于函数入参,call 接收 的是多个参数值,apply接收的是一个参数列表。
// mycall.js
Function.prototype.mycall = function(thisarg,...args){
let context = thisarg || window
context.fn = this
const res = context.fn(...args)
delete context.fn
return res
}
function logNameAndAge(age, sex){
console.log(this.name,age,sex)
}
logNameAndAge.mycall({name:"litangmm"},21,'man') // litangmm 21 man
context.fn = this
这里做了一件事,就是在 context
上定义了一个属性fn
,赋值为 this
。**this
是调用mycall
的函数A。**这样,在下一行,函数A就作为 context
对象的对象函数被调用,函数A执行的this
自然指向context
了。
当然,这代码还是有优化空间的:
- 判断 this 是否为 函数。
- 如果传入的 thisarg 上有 fn 属性,那么该属性会被覆盖。
我们考虑这些情况,实现apply
。
// myapply.js
Function.prototype.myapply = function(thisarg,args){
if(typeof this !== 'function'){
throw new TypeError('error')
}
let context = thisarg || window
const fn = Symbol()
context.fn = this
const res = context.fn(...args)
delete context.fn
return res
}
function logNameAndAge(age, sex){
console.log(this.name,age,sex)
}
logNameAndAge.myapply({name:"litangmm"},[21,'man']) // litangmm 21 man
说明:
- 通过
typeof
判断调用的是否是一个函数。 - 通过
Symbol
来解决属性冲突。
bind
执行效果
- 改变 thisarg。
- 接收函数入参。
- 返回函数。
对于返回函数
- 可以接收入参。
- 可以作为构造函数使用。
- 作为构造函数使用时,bind 绑定的
this
会被忽略。
bind
实现的难点在于
bind
执行时接收可以接收参数(类似call接收多个参数),返回的函数还可以接收参数。- 判断返回函数是作为构造函数被使用。
// mybind.js
Function.prototype.mybind = function(thisarg,...args1){
if(typeof this !== 'function'){
throw new TypeError('error');
}
const that = this;
let fBound = function(...args2){
return that.apply(this instanceof fBound ? this: thisarg,args1.concat(args2));
}
let fNop = function(){};
fNop.prototype = that.prototype;
fBound.prototype = new fNop();
return fBound;
}
bind 本质是一个函数装饰器:
- 参数拼接。
- 如果是普通调用,使用bind的this。
- 当做返回函数作为构造函数,则使用返回函数的this。
说明
之所以可以通过this instanceof F
来判断是被当作构造函数被使用的,可以参考new 的原理:
-
创建一个空对象。
-
指定空对象的
__proto__
为构造函数的prototype
。 -
绑定该空对象到构造函数。
-
执行绑定后的构造函数。
-
如果构造函数有返回值,则返回返回值,否则返回对象。
!注意2步,因为返回的构造函数 是fBound
,而,我们希望得到的that
上的原型,所以,有代码
fBound.prototype = that.prototype;
这代码是存在问题的,因为fBound.prototype=== that.prototype === 原型对象地址
,所以操作fBound.prototype
修改会改变 that.prototype
。所以,我们可以使用原型链来实现fBound
与 that
的连接。
let fNop = function(){};
fNop.prototype = that.prototype;
fBound.prototype = new fNop();
这样,fBound.prototype
就是一个对象,对象的__proto__
指向that.prototype
。可以参考这篇文章。
!注意3、4步,构造函数执行的时候,空对象的__proto__
已经指向了构造函数的prototype
。所以,绑定后的构造函数的this
是可以在原型链上找到构造函数的。