笔记: 你不知道JS上卷 - 关于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 的调用位置
绑定规则
默认绑定
可以把这条规则看作是无法应用其他规则时的默认规则。
function foo() {
console.log(this.a)
}
var a = 2;
foo(); // 2
分析一下foo()函数的调用位置, 在代码中,foo()是直接使用不带任何修饰的函数引用进行调用的,因此只能使用 默认绑定,无法应用其他规则。
如果使用严格模式(strict mode),那么全局对象将无法使用默认绑定,因此 this 会绑定 到 undefined:
function foo() {
'use strict'
console.log(this.a)
}
var a = 2
foo() // TypeError: this is undefined
这里有一个微妙但是非常重要的细节,虽然 this 的绑定规则完全取决于调用位置,但是只 有 foo() 运行在非 strict mode 下时,默认绑定才能绑定到全局对象;严格模式下与 foo() 的调用位置无关:
function foo() {
console.log(this.a)
}
var a = 2
;(function () {
'use strict'
foo() // 2
})()
隐式绑定
调用位置是否有上下文对象,或者说是否被某个对象拥有或者包
含
function foo() {
console.log(this.a)
}
var obj = { a: 2, foo: foo }
obj.foo() // 2
首先需要注意的是 foo()的声明方式,及其之后是如何被当作引用属性添加到 obj 中的。 但是无论是直接在 obj中定义还是先定义再添加为引用属性,这个函数严格来说都不属于 obj 对象。
然而,调用位置会使用 obj上下文来引用函数,因此你可以说函数被调用时 obj 对象“拥 有”或者“包含”它。
多层调用时, 考虑函数调用栈就会迎刃而解了
function foo() {
console.log(this.a)
}
var obj2 = { a: 42, foo: foo }
var obj1 = { a: 2, obj2: obj2 }
obj1.obj2.foo() // 42
隐式丢失
下面这种情况下, 就直接考虑: 函数的调用位置就好了
function foo() {
console.log(this.a)
}
var obj = { a: 2, foo: foo }
var bar = obj.foo // 函数别名!
var a = 'oops, global' // a 是全局对象的属性
bar(); // "oops, global"
同理, 下面这种情况也是直接考虑函数的调用位置就好
function foo() {
console.log(this.a)
}
function doFoo(fn) {
// fn 其实引用的是 foo
fn(); // <-- 调用位置!
}
var obj = { a: 2, foo: foo }
var a = 'oops, global' // a 是全局对象的属性
doFoo( obj.foo ); // "oops, global"
还有一种特殊情况
function foo() {
console.log(this.a)
}
var obj = { a: 2, foo: foo }
var a = 'oops, global' // a 是全局对象的属性
setTimeout( obj.foo, 100 ); // "oops, global"
其实setTimeout也是一个函数, 类似于
function setTimeout(fn,delay) {
// 等待 delay 毫秒
fn(); // <-- 调用位置!
}
显示绑定
使用call(),apply()显示绑定this
function foo() {
console.log(this.a)
}
var obj = { a: 2 }
var bar = function () {
foo.call(obj)
}
bar() // 2
setTimeout(bar, 100) // 2
// 硬绑定的 bar 不可能再修改它的 this
bar.call(window) // 2
我们创建了函数 bar(),并在它的内部手动调用了 foo.call(obj)
,因此强制把 foo 的 this 绑定到了 obj。无论之后如何调用函数 bar,它 总会手动在 obj 上调用foo。这种绑定是一种显式的强制绑定,因此我们称之为硬绑定。
硬绑定的典型应用场景就是创建一个包裹函数,传入所有的参数并返回接收到的所有值
function foo(something) {
console.log(this.a, something)
return this.a + something
}
var obj = { a: 2 }
var bar = function () {
return foo.apply(obj, arguments)
}
var b = bar(3) // 2 3
console.log(b) // 5
使用bind绑定
function foo(something) {
console.log(this.a, something)
return this.a + something
}
// 简单的辅助绑定函数
function bind(fn, obj) {
return function () {
return fn.apply(obj, arguments)
}
}
var obj = { a: 2 }
var bar = bind(foo, obj)
var b = bar(3) // 2 3
console.log(b) // 5
由于硬绑定是一种非常常用的模式,所以在 ES5 中提供了内置的方法 Function.prototype. bind
,它的用法如下:
function foo(something) {
console.log(this.a, something)
return this.a + something
}
var obj = { a: 2 }
var bar = foo.bind(obj)
var b = bar(3) // 2 3 console.log( b ); // 5
API调用的“上下文”
第三方库的许多函数,以及 JavaScript 语言和宿主环境中许多新的内置函数,都提供了一 个可选的参数,通常被称为“上下文”(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绑定
构造函数
构造函数”是类中的一些特殊方法,使用 new 初始化类时会
调用类中的构造函数。通常的形式是这样的:
something = new MyClass(..);
其实在 JavaScript 中,构造函数只是一些 使用 new 操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上, 它们甚至都不能说是一种特殊的函数类型,它们只是被 new 操作符调用的普通函数而已
实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。
new 绑定
使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
- 创建(或者说构造)一个全新的对象。
- 这个新对象会被执行[[原型]]连接。
- 这个新对象会绑定到函数调用的this。
- 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2
使用 new 来调用 foo(…) 时,我们会构造一个新对象并把它绑定到 foo(…) 调用中的 this 上。new 是最后一种可以影响函数调用时 this 绑定行为的方法,我们称之为 new 绑定。
怎么判断this
现在我们可以根据优先级来判断函数在某个调用位置应用的是哪条规则。可以按照下面的顺序来进行判断:
- 函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象。
var bar = new foo() - 函数是否通过call、apply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是 指定的对象。
var bar = foo.call(obj2)
- 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上 下文对象。
var bar = obj1.foo() - 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到 全局对象。
箭头函数中的this
- 箭头函数没有自己的this, 只能从作用域链的上一层获取this
//不使用箭头函数例子
const obj = {
a: function() { console.log(this) }
}
obj.a() //打出的是obj对象
//使用箭头函数的例子
const obj = {
a: () => {
console.log(this)
}
}
obj.a() //箭头函数中的this, a是属于obj的,但是obj不是一个函数作用域, 没有上下文环境, 所以继续向上找, 即window
- 不能用call方法修改里面的this
const obj = {
a: () => {
console.log(this)
}
}
obj.a.call('123') //打出来的结果依然是window对象
var flag = '996'
function person(fg) {
let o = new Object()
o.flag = fg
// o.getVal = () => {
// console.log(this) //preson {} -> 指向person
// }
o.getVal = function () {
console.log(this) //{ flag: '000', getVal: [Function] } -> pp实例的this
}
return o
}
let pp = new person('000')
pp.getVal()
箭头函数中, 从词法作用域来看,可以看到 getval 的父级是 person 函数返回的对象 o,但是 o 对象不是一个函数作用域,没有 this 上下文,当然 getval 函数也无法指向 o 对象的上下文。继续往上查找 this,于是找到了 person 函数
箭头函数的this是从当前箭头函数逐级向上查找 this,如果找到了,则作为自己的 this 指向,如果没有则继续向上查找。而父级的 this 是可变的,所以箭头函数的 this 也可跟随父级而改变。
为啥会一直跟随父级的 this呢?因为实际上箭头函数的 this 就类似一个普通变量,变量的内容就是父级函数的 this,一个变量赋值罢了。
箭头函数的 this 理解成词法作用域就行了,函数有 this,对象没 this,比如
var name = 'shequ'
var obj = {
name: 'juejin',
fn: () => {
console.log(this.name)
},
method: function () {
console.log(this.name)
},
}
obj.fn() // shequ
//obj内定义的fn, 但是没有函数作用域, 继续向上变成了window
obj.method() // juejin, this指向obj
obj.fn.call(obj) // shequ, 手动绑定没有用
obj.method.call(window) // shequ, 手动绑定有用
简单理解, 就是
- 对象没有词法作用域(上下文), 函数有作用域.
- 普通函数按照最开始提到的优先级进行this的判断
- 箭头函数的this同父级的词法作用域, 如果父级是对象没有作用域, 就继续向上