为什么要使用this关键字
看个例子
function indetify() {
retun this.name.toUpperCase()
}
var obj = {
name: 'zz'
}
indetify.call(obj) // 'ZZ'
复制代码
这里函数identify中的this指向变量obj,如果不使用this的话,想实现这种效果,就需要显示的传入上下文对象,例如
function indetify(context) {
retun context.name.toUpperCase()
}
var obj = {
name: 'zz'
}
indetify(obj) // 'ZZ'
复制代码
当模式越来越复杂的时候,显示的传递上下文对象就会变得不太好管理,而this提供了一种更优雅的方式,隐式的"传递"一个对象的引用,因此可以将API设计的更加简洁并且易于复用。
对于this的误解
- this指向自身
- this指向函数作用域(需要明确的是,this在任何情况下都不指向函数的词法作用域)
this到底是什么
this是在运行的时候被绑定的,并不是编写的时候,它的上下文取决于函数调用的各种条件。当一个函数被调用时,会创建一个活动记录(即执行上下文),这个记录包含函数的调用栈(call Stack),调用方式,传入参数等信息,this就是这个记录的一个属性,在函数执行的过程中找到。
要想找到this的绑定对象,首先得找到函数的调用位置,调用位置就在当前执行函数的前一个调用中。
function baz () {
// 当前调用栈是baz
// 当前调用位置是全局作用域
console.log('baz')
bar() // bar的调用位置
}
function bar () {
// 当前的调用栈是baz -> bar
// 所以当前的调用位置在baz中
console.log('bar')
foo() // foo的调用位置
}
function foo () {
// 当前的调用栈是baz -> bar -> foo
// 所以当前的调用位置在bar中
console.log('foo')
}
baz() // baz的调用位置
复制代码
找到调用位置,接下来就要分析this的绑定规则了。
this绑定规则
- 默认绑定规则
function foo () {
console.log(this.a)
}
var a = 1
foo() // 1
复制代码
因为foo()是直接不带任何修饰的函数引用进行调用的,所以只能使用默认绑定。非严格模式下指向window,严格模式下绑定到undefined
- 隐私绑定规则
分析隐式绑定时,必须在一个对象内部包含一个指向函数的属性,通过这个属性间接引用函数
function foo () {
console.log(this.a)
}
var obj = {
a: 2,
foo: foo
}
obj.foo() // 2
复制代码
首先要声明的是,无论foo函数是先声明还是直接在obj对象中定义,这个函数严格来说,都不属于obj对象。
隐式绑定会遇到一个问题,就是会丢失绑定对象,也就是会应用默认绑定。比如
function foo () {
console.log(this.a)
}
var obj = {
a: 2,
foo: foo
}
var another = obj.foo // 函数别名
var a = '隐式丢失'
obj.foo() // 2
another() // '隐式丢失'
复制代码
这里虽然bar是obj.foo的一个引用,但实际上引用的是foo函数本身,因此bar()是不带任何修饰的函数调用,所以也是默认绑定。
还有一种更微妙的情况,发生在传入回调函数的时候
function foo () {
console.log(this.a)
}
var obj = {
a: 2,
foo: foo
}
function doFun (fn) {
// fn 其实是引用foo
// 相当于var fn = foo
fn()
}
var a = '又隐式丢失'
obj.foo() // 2
doFun(obj.foo) // '又隐式丢失'
复制代码
实际上传递参数,实际上就是一种赋值操作,所以结果和上面一样
- 显示绑定规则
通常情况,我们使用js提供的call和apply方法实现显示绑定
这俩个方法的第一个参数是一个对象,是给this准备的,在调用函数是将函数绑定到this,因此称为显示绑定。第二个参数就是一个参数列表或参数对象,就是传递函数的调用的参数。
function foo (name) {
console.log(this.a+name)
}
var obj = {
a: 1
}
foo.call(obj,'显示绑定') // 1'显示绑定'
复制代码
但是,显示绑定还是会出现绑定丢失的情况,能有办法解决吗?当然有
- 硬绑定
function foo () {
console.log(this.a)
}
var obj = {
a: 1
}
var bar = function () {
foo.call(obj)
}
var a = '会丢失吗'
bar() // 1
// 现在不会丢失了
setTimeOut(bar, 1000) // 1
bar.call(window) // 1
复制代码
我们创建了函数bar(),并且在内部手动调用foo.call(obj),强制将foo的this绑定到了obj,无论后面如何调用函数bar,都会手动在obj上调用foo
- API调用的“上下文”
js语言和宿主环境中许多新的内置函数,都提供了一个可选参数,通常称为“上下文”(context),作用和bind(..)一样,确保回调函数使用指定的this
function foo (el) {
console.log(el, this.id)
}
var obj = {
id: 'awesome'
}
// 调用foo的同时把this绑定到obj上
[1,2,3].forEach(foo, obj)
// 1'awesome' 2'awesome' 3'awesome'
复制代码
- new绑定规则
js中的new的机制和面向类的语言完全不同
我们习惯把new的函数叫做构造函数,实际上,只是使用new操作符时被调用的函数,不会实例化一个类,因为实例化的类与类之间是复制,是两位完全没关系的,但js不是。只能说这些函数是被new操作符调用的普通函数而已。
首先,我们看看new操作符会做些什么
-
创建(或者说构造)一个全新的对象
-
这个对象会被执行[[prototypt]]链(原型链)
-
新的对象会绑定到函数调用的this上
-
如果函数没有返回对象,那new表达式中的函数会自动返回这个新对象
function foo (a) {
console.log(a)
}
var bar = new foo(1)
bar() // 1
复制代码
优先级
既然this有这么多种绑定方式,肯定会存在绑定的优先级
首先,毫无疑问,默认绑定的优先级是最低的
- 那隐式绑定和显示绑定的优先级谁高?
funtion foo (){
console.log(this.a)
}
var obj1 = {
a: 1,
foo: foo
}
var obj2 = {
a: 2,
foo: foo
}
obj1.foo() // 1
obj2.foo() // 2
// 显示绑定改变了this的指向
obj1.foo.call(obj2) // 2
obj.foo.call(obj1) // 1
复制代码
很显然,显示绑定的优先级比隐式绑定的高。
- 隐式绑定和new绑定谁的优先级高
function foo (something) {
this.a = somethig
}
var obj1 = {
foo: foo
}
var obj2 = {}
obj1.foo(1)
console.log(obj1.a) // 1
obj1.foo.call(obj2, 2)
console.log(obj2.a) // 2
var bar = new obj1.foo(3)
console.log(obj2.a) // 2
console.log(bar.a) // 3
复制代码
看来new绑定的优先级是比隐式绑定高的,最后我们看一下new和显示绑定谁的优先级高,因为new和call/apply无法一起使用,所以没法通过new foo.call(obj1)来直接测试,我们选择用硬绑定来测试。
回忆一下硬绑定是如何工作的,Function.prototype.bind(..)会创建一个新的包装函数,这个函数会忽略它当前的this绑定,并把提供的对象绑定到this上。
- new和显示绑定谁的优先级高
function foo (something) {
this.a = somethig
}
var obj1 = {}
var bar = foo.bind(obj1)
bar(1)
console.log(bar.a) // 1
var baz = new bar(2)
console.log(obj1.a) // 1
console.log(baz.a) // 2
复制代码
bar被硬绑定到obj1上,但是new bar(2),并没有将obj1.a修改成2,相反,new修改了硬绑定(到obj1的)调用bar(..)中的this。这样看来new调用的函数,新创建的this替换了硬绑定的参数,所以new的优先级是最高的。
那我们判定优先级的方法就是从优先级由高往下去判定。
绑定例外
凡事都有例外,不是所有的绑定都遵循这个规则的。
-
如果是call(null)或者apply(undefined),这里对应的其实是默认绑定,因为其实,null和undefined只是基础的数据类型,并不是对象。
-
软绑定
硬绑定可以强制绑定到指定对象,但是这大大降低了函数的灵活性,之后无法使用隐式或显示绑定修改this的指向,所以我们来实现一下软绑定
// 实现软绑定
if (!Function.prototype.softBind) {
Function.prototype.softBind = function (obj) {
var fn = this
// 捕获所有curried参数
var curried = [].slice.call(arguments, 1)
var bound = function () {
return fn.apply(
(!this || this === (window || global)) ?
obj : this,
curried.concat.apply(curried, arguments)
)
}
bound.prototype = Object.create(fn.prototype)
return bound
}
}
// 验证
function foo () {
console.log("name: " + this.name)
}
var obj1 = { name: "obj1"}
var obj2 = { name: "obj2"}
var obj3 = { name: "obj3"}
var fooOBJ = foo.softBind(obj)
fooOBJ() // "obj"
obj2.foo = foo.softBind(obj)
obj2.foo() // "obj2"
fooOBJ.call(obj3) // "obj3"
setTimeOut(obj2.foo, 10) // "obj"
复制代码
可以看到,软绑定的foo()可以手动的将this绑定到obj2或者obj3,但如果应用默认绑定则将this绑定到obj1上。
- this词法
此前的4条规则使用与大部分函数,但在ES6中的箭头函数却不适用,因为箭头函数不是function关键字定义的,而是使用被称为“胖箭头”的操作符 => 定义的。箭头函数不使用this的四种标准规则,而是根据外层的作用域决定this的。所以叫做词法
function foo () {
// 返回一个箭头函数
return (a) => {
// this继承自foo()
console.log(this.a)
}
}
var obj1 = {
a: 2
}
var obj2 = {
a: 3
}
var bar = foo.call(obj1)
bar.call(obj2) // 2
复制代码
foo内部创建的箭头函数会捕获调用时foo()的this,由于foo()的this是绑定到obj1上的,bar(只是引用箭头函数)的this也会绑定到obj1上,箭头函数的绑定无法修改。类似于
function foo () {
var self = this
setTimeout(function () {
console.log(self.a)
}, 100)
}
复制代码
关于this的理解就说这么多,欢迎指正和交流。
转载于:https://juejin.im/post/5cf4e610f265da1b897aba93