You Don't Know JS: this & Object Prototypes( 第2章 this)
this
is a binding made for each function invocation, based entirely on its call-site (how the function is called).
this是为函数被引用而创建的绑定!完全地基于函数如何被调用/函数的call-site!
Call-site
:the location in code where a function is called
call-stack: 调用函数的堆栈。(函数的调用链条,函数的call-site的移动形成了stack。)
The call-site is in the invocation before the currently executing function.
function baz() { // call-stack is: `baz` // so, our call-site is in the global scope console.log( "baz" ); bar(); // <-- 下一个调用位置是'bar' } function bar() { // call-stack is: `baz` -> `bar` // so, 我们的call-site是在'baz'内。 console.log( "bar" ); foo(); // <-- call-site for `foo` } function foo() { // call-stack is: `baz` -> `bar` -> `foo` // so, our call-site is in `bar` console.log( "foo" ); } baz(); // <-- call-site for `baz`
在浏览器inspector中,可以使用debugger工具观察call-site。
function baz() { console.log(this); console.log( "baz" ); bar(); // <-- call-site for `bar` } function bar() { console.log(this) console.log( "bar" ); foo(); // <-- call-site for `foo` } function foo() { console.log(this); console.log( "foo" ); } baz();
最后的结果是3个this都指向window对象。
Nothing But Rules
在函数执行期间,call-site如何决定this指向哪里?
有:4条法则!及这4条法则的优先级。
default Binding
第一条法则来自最常用的函数调用: 独立的函数引用。(没有其他法则影响的函数)
function foo() { console.log( this.a ); } var a = 2; foo(); // 2
this指向全局对象,变量a是全局对象的属性。
如果是严格模式,this指向undefined!
第一条:this绑定完全基于call-site,非严格模式下,全局对象是默认的绑定对象。
注意:不要混用严格模式和非严格模式!
Implicit Binding
第2条:如果call-site有一个context对象,也涉及一个拥有或包含的对象。
function foo() { console.log( this.a ); } var obj = { a: 2, foo: foo }; obj.foo(); // 2 因为obj就foo()调用的this, 所以this.a等同于obj.a
当有一个content object 拥有一个函数引用时,
Implicit Binding Rule 就是那个对象应当拥有这个函数调用的this绑定
⚠️:Only the top/last level of an object property reference chain matters to the call-site.
也就是说假如有: obj1.obj2.obj3.foo(), 调用foo()函数的this绑定在obj3上。
Implicitly Lost
当一个Implicit bound 函数丢弃了context对象的binding, 就会使用默认binding。或用全局对象,或undefined(use strict)。
function foo() { console.log( this.a ); } var obj = { a: 2, foo: foo }; var bar = obj.foo; // function reference/alias! var a = "oops, global"; // `a` also property on global object bar(); // "oops, global" obj.foo
结果是: ƒ foo() { console.log( this.a ); }
bar //变量bar的值就是函数foo, 因为bar被分配了obj.foo
结果是 ƒ foo() { console.log( this.a ); }
所以: this指向全局对象window, this.a就是 window.a
另一个例子:参数传递
其实就是把obj.foo的值,分配给fn参数。等同于在doFoo函数内,声明了变量:var fn = obj.foo;
function foo() { console.log( this.a ); } function doFoo(fn) { // `fn` is just another reference to `foo` fn(); // <-- call-site! } var obj = { a: 2, foo: foo }; var a = "oops, global"; // `a` also property on global object doFoo( obj.foo ); // "oops, global"
看到了吧: 结果this绑定的是全局对象。this.a就是window.a
JavaScript内建函数也同样:
function foo() { console.log( this.a ); } var obj = { a: 2, foo: foo }; var a = "oops, global"; // `a` also property on global object setTimeout( obj.foo, 100 ); // "oops, global" setTimeout()函数相当于: function MySetTimeout(fn) { //0.1秒后; fn(); } MySetTimeout( obj.foo )
函数回调丢失它们的this绑定是很常见的。
还有一类方式,是主动改变this:
Event handlers就会强制你的回调有一个this指向DOM中被激活的元素。
如果有一种方法可以固定住this,就好了!当然有了,见⬇️,明确的绑定!
Explicit Binding
这是第3个rule:使用call()方法,apply()方法。第一个参数是一个对象的话,这个对象就绑定了this。
function foo() { console.log( this.a ); } var obj = { a: 2 }; foo.call( obj ); // 2
⚠️,call(..),apply(..)可接受多个参数,它们的用途这里不谈!
不幸的是,明确绑定,不能解决一个函数丢失它想要的this绑定的问题。
Hard Binding(explict and strong)
我的理解就是再加一层函数表达式给一个声明的变量。这个变量的值就是函数。
apply()和call()的功能一样,区别就是apply可以接受array作为参数。
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
API Call "Contexts"
许多库的函数,和许多新的内建函数,提供了可选参数,叫做'context'。
context即一个上下文环境。一般传入一个对象参数。
用途就是确保你的回调函数使用一个指定的this关键字 。
function foo(el) { console.log( el, this.id ); } var obj = { id: "awesome" }; // use `obj` as `this` for `foo(..)` calls [1, 2, 3].forEach( foo, obj );
// 1 awesome 2 awesome 3 awesome [1, 2, 3].forEach( foo );
//返回的1 undefined, 2 undefined, 3 undefined //因为此时this是window对象。
new
Binding
第四条法则:
先看一个误区:JS的new操作符号,和这个代码: sth = new MyClass(),的使用类似传统的类语言。
但是:实际上在JS中,并没有class-oriented功能暗示和new相关。
constructor: 它就是一个函数,和new操作符一起在被调用时运行。和类无关也不实例化类!就是一个标准的函数。
如 new Number(..) ,
没有constructor function,只有construction calls of functions
当一个函数前面有一个new, 会自动做以下事情:
- a brand new object is created
- the newly constructed object is
[[Prototype]]
-linked, - the newly constructed object is set as the
this
binding for that function call - unless the function returns its own alternate object, the
new
-invoked function call will automatically return the newly constructed object.
例子:
function foo(a) {
this.a = a;
}
var bar = new foo( 2 );
console.log( bar.a ); // 2
bar
foo {a: 2}
a: 2
__proto__: Object
当调用foo()函数,前面加一个new的时候:
- 1. 新建一个对象foo{ .. } 。
- 2.(这里不讨论prototype)
- 3. 新建的对象绑定了this关键字, foo.a = 2
- 4. 默认自动返回新建的对象,给变量var。
总结: 4条rule:
- default binding
- implicit binding
- explicit binding: 使用call(), apply()
- new binding
使用方法:先找到call-site,然后检查符合哪条rule。
Everything In Order
是否在一个call-site,有多个rule生效?
答案:rule也是有优先级顺序的!!
- 很清楚,第一条default binding rule是最低优先级priority
- Explicit binding rule 高于 Implicit binding rule。
- new rule 高于 Implicit binding rule
function foo(something) { this.a = something; } var obj1 = { foo: foo }; var obj2 = {}; obj1.foo( 2 ); console.log( obj1.a ); // 2 obj1.foo.call( obj2, 3 ); console.log( obj2.a ); //
明确使用call方法,即Explicit rule 优先于 Implicit rule var bar = new obj1.foo( 4 ); console.log( obj1.a ); // 2 console.log( bar.a ); // 4
new rule 优先于 Implicit rule
(一大坨没有读!)
Determining this
- 如果函数调用前面加上了new, 则this是新的constructed object。
- 使用了call/apply方法的函数, this就是明确指定的对象。
- var bar = foo.call( obj2 )
- 如果函数被一个context调用。 this就是context object。
- var bar = obj1.foo()
- 其他情况,默认this,是default binding。如果是严格模式,pick undefined,否则用全局对象。
但还没完。。。
Binding Exceptions
总有一些例外。
Ignored this
如果你传递null, undefined作为this绑定参数来call, apply, 或者bind, 那些值会被忽略,使用default binding rule。
function foo(a,b) { console.log( "a:" + a + ", b:" + b ); } // spreading out array as parameters foo.apply( null, [2, 3] ); // a:2, b:3 // currying with `bind(..)` var bar = foo.bind( null, 2 ); bar( 3 ); // a:2, b:3
Safer this
我的简单理解,创建一个Object.create(null), 替代null。
Object.create(null)没有委托到Object.prototype,所以更干净。
Indirection
you can (intentionally or not!) create "indirect references" to functions, and in those cases, when that function reference is invoked, the default binding rule also applies.
非直接的引用,使用default binding rule!!
除了以下这个案例使用“立即函数执行”,我不知道其他的用法!
function foo() { console.log( this.a ); } var a = 2; var o = { a: 3, foo: foo }; var p = { a: 4 }; o.foo(); // 3 (p.foo = o.foo)(); // 2
call-site是foo(),不是p.foo(),也不是o.foo(), 所以this就是全局对象。
Softening Binding(没看!)
Lexical this
箭头函数fat arrow, 不使用标准的4条rules。
箭头函数的this binding来自enclosing scope!
function foo() { // return an arrow function return (a) => { // `this` here is lexically adopted from `foo()` console.log( this.a ); }; } var obj1 = { a: 2 }; var obj2 = { a: 3 }; var bar = foo.call( obj1 ); bar.call( obj2 ); // 2, not 3!
当foo()函数被调用时, 箭头函数在foo()内被创建,箭头函数会捕捉到foo()的this. 这个this就是箭头函数的Lexical scope。
因为foo()的this绑定到了obj1 , 所以 bar的this也绑定到了obj1。
箭头函数的Lexical binding不会被重写!!
箭头函数常用的案例是event handlers或者timers.
function foo() { setTimeout(() => { // `this` here is lexically adopted from `foo()` console.log( this.a ); },100); } var obj = { a: 2 }; foo.call( obj ); // 2
在没有箭头函数的时候,会使用var self = this来得到
function foo() { var self = this; // lexical capture of `this` setTimeout( function(){ console.log( self.a ); }, 100 ); } var obj = { a: 2 }; foo.call( obj ); // 2
Review (TL;DR)
4条rule
-
Called with
new
? Use the newly constructed object. -
Called with
call
orapply
(orbind
)? Use the specified object. -
Called with a context object owning the call? Use that context object.
-
Default:
undefined
instrict mode
, global object otherwise.
有例外情况:
-
Ignored
this, 使用明确绑定(call,apply,bind)时,如果传入的参数值时undefined,null则使用default rule。
- 非直接使用。看上面的案例
-
API Call "Contexts"。 许多方法可以传入一个context Object,这个对象就是this.
- 软binding(未看)
许多库的函数,和许多新的内建函数,提供了可选参数,叫做'conte
箭头函数,的this可以得到箭头函数被定义/创建时的Lexical scope。