[JS] You Don't Know JavaScript [this & Object Prototypes] - this All Makes Sense Now! [上]

前言

this Or That?中提到了许多对於this的误解,并且也对於这些误解做了一些解释,我们了解到this是对每个函式调用的绑定,是基於被调用的位置而不是宣告的位置。

Call-site

为了了解this,我们必须要先了解一个重要的观念call-site,它代表着函式在程序中被调用的位置,通常要找到call-site是要定位从何处调用此函式,但通常这个行为并不是这麽容易,因为某些模式下会掩盖掉真正的call-site,这个状态下我们需要考虑的是call-stack,call-stack代表着呼叫function的堆叠,比如说

function c(){
    console.log('c');
}

function b(){
    c();
    console.log('b');
}

function a(){
    b();
    console.log('a');
}

a();

上面的程序中呼叫a(...),而a(...)中又呼叫b(...),最後b(...)中又呼叫c(...),而call-stack -> a() -> b() -> c()。
https://ithelp.ithome.com.tw/upload/images/20201027/20124767MJrJJDrECS.png
而call-site来说,他是在他call-stack父层中呼叫自己的位置,看其来很绕舌不过我们一样拿上面的程序码来做举例,对於c(...)而言,他的call-site就是在call-stack父层(b(...))所呼叫的位置,以此类推。

function c(){
    console.log('c');
}

function b(){
    c(); // function c(...) => call-site
    console.log('b');
}

function a(){
    b(); // function b(...) => call-site
    console.log('a');
}

a();

Nothing But Rules

介绍完call-site与call-stack,接下来我们将重点移到call-site是如何确定函数执行期间this的指向,对於这个指向我们有4条规则,我们先一一介绍是哪些规则。

Default Binding

第一种规则是来自函数最常见的情况函数独立调用(换句话说就是只呼叫自己而没有在内部嵌入其他函数),若没有其他规则适用,可以将这个规则是为万用规则。

function foo() {
	console.log( this.a );
}

var a = 2;
foo(); // 2

以上面的程序中,default binding代表着this是绑定全域物件,由於我们的var a = 2是宣告在全域,所以这里的this才会指向到全域的a,我们要如何知道default binding规则适用於这个例子?

我们通过call-site来观察foo(...)是在哪里被调用的,在我们的程序中foo(...)是一个直白且无修饰的函数呼叫,意味着他没有在内部嵌入其他函数,所以default binding规则在这里适用。

但是default binding对於使用严格模式来说就不适用

function  foo ( )  { 
	"use strict" ;
	console . log (  this . a  ) ; 
}

var  a  =  2 ;
foo ( ) ;  // TypeError: `this` is `undefined`

但是有一个特别的点,对於严格模式来说只要foo(...)的作用域内不是严格模式,那麽this一样也可以绑定到全域物件。

function  foo ( )  { 
	console . log (  this . a  ) ; 
}

var  a  =  2 ;

( function ( ) { 
	"use strict" ;

	foo ( ) ;  // 2 
} ) ( ) ;

Implicit Binding

第二个规则是需要考虑call-site是否有一个环境物件(context object)。

function foo() {
	console.log( this.a );
}

var obj = {
	a: 2,
	foo: foo
};
obj.foo(); // 2

我们宣告了一个function foo(...)之後将他加入到obj物件中成为他的property,无论foo()是否一开始就在obj上被宣告或是後来才加入到obj中(上面的例子),这个函数都不被obj所真正的拥有或包含,但是由於对於call-site来说obj环境来Referencefoo(...),所以可以说obj在函数被调用的时间点拥有或包含这个funciton reference

当一个context object中有一个function reference则implicit binding规则会将这个fucntion中的this绑定这个object,所以以上面的例子来说foo(...)中的this所指向的就是obj

对於嵌套的物件来说,只有最後一层/最上层物件才会对call-site起作用

function foo() {
	console.log( this.a );
}

var obj2 = {
	a: 42,
	foo: foo // call-site
};

var obj1 = {
	a: 2,
	obj2: obj2
};

obj1.obj2.foo(); // 42

Implicitly Lost

当一个implicitly bound的函数丢失了绑定,则会退回default binding,至於指向的是全域物件还是undefined则取决於是否使用严格模式。

function foo() {
	console.log( this.a );
}

var obj = {
	a: 2,
	foo: foo
};

var bar = obj.foo; // loses that binding!

var a = "oops, global"; // `a` is property on global object

bar(); // "oops, global"

虽然bar似乎是obj.foo的reference,但是实际上他只是对foo(...)本体的另一个reference,换句话说虽然bar与obj.foo都是对foo(...)本体的reference,但是实际上是两个不一样的地方,而且对於call-site而言呼叫bar(...)是一个直白且无修饰的函数呼叫,所以他适用於default binding

还有一个更加微妙更常见更出乎意料的方式,当我们传递一个callback function时

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"

对於参数的传递来说他是一个隐性赋值,而且如果要传递的参数是函数的话则是一个隐性的reference赋值,所以结果会与上一个程序码相同。

function doFoo(var fn = obj.foo){
  // 隐性function reference 赋值代表fn与obj.foo的reference是不同的。
}

对於传递callback function作为参数会丢失binding这件事,除了自己定义的function之外对於原生的funciton也是一样的情况。

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"

可以将他看为

function setTimeout(var fn = obj.foo, delay){
    //隐性function reference 赋值
}

Explicit Binding

我们介绍了Implicit Binding如果需要间接地将函数中的this绑定到这个物件上,会需要对这个物件做一些改变(将function reference引入到物件属性中),但是有没有方法是可以不更改物件的型态却又可以使function的this绑定着这个物件的呢?

我们可以使用JS所提供function的prototype(後面会介绍)call(...)apply(...)method,他们的第一个参数都是一个物件,他代表着我这个fucntion的this所指向的目标,因为明确的指出this要指向什麽所以我们称这种方式为Explicit Binding。

function foo() {
	console.log( this.a );
}
var obj = {
	a: 2
};

var a = 5; // declaration in global 
foo.call( obj ); // 2

通过foo.call(...)的方式将this明确的指向obj,注意的是如果对於call(...)或apply(...)的第一个参数传递的不是一个物件(string,boolean,number...)那麽传递的这个参数的类性会被包装在物件(new String(...), new Boolean(...), new Number(...))这种行为称为boxing

Hard Binding

虽然可以对单独的function进行显性绑定,但是依然无法解决上面提到的赋值导致绑定丢失的问题,但是可以有一种明确绑定的变种可以解决这个问题。

function foo() {
	console.log( this.a );
}

var obj = {
	a: 2
};
var a = 4; // declaration in global

var bar = function() {
	foo.call( obj );
};
bar(); // 2
setTimeout( bar, 100 ); // 2

// `bar` hard binds `foo`'s `this` to `obj`
// so that it cannot be overriden
bar.call( window ); // 2

我们在bar内部强制绑定了foo(...)的this指向obj,所以无论之後怎麽调用bar(...)在他的内部都会自动的强制绑定obj,这种行为我们称为hard binding

对於hard binding来说,ES5中提供了funciton.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 Call "Contexts

在许多现在JS的内建函数中都有提供一个可选的参数通常称为context,这种设计可以让你直接填入你需要绑定的object而不必一定要使用bind(...)

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
arr.forEach(function callback(currentValue[, index[, array]]) {
    //your iterator
}[, thisArg]);
/* 
    callback : 把 Array 中的每一个元素作为参数,带进本 callback function中
         currentValue : 当前被处理的Array元素
         index(可选):当前被处理的Array元素的index
         array(可选):forEach()本身的Array -> arr
    thisArg(context)(可选):callback function的this (需要绑定的物件)
*/

New Binding

在传统拥有class的语言中,constructor是一个特殊的method,当一个class被new实体化後这个constructor就会被调用以用来初始化这个class。

something = new MyClass(...);

虽然JavaScript中也有new但是他与其他语言的new是没有关系的,对於JavaScript来说constructor就只是个函数他们偶然的与new一起被调用,但他却不依附於也不会初始化一个class。

当一个函数前面加上new调用,也就是constructor调用时,会自动完成以下的事情:

  1. 凭空创造一个全新的物件。
  2. 被创建的物件会接入原形链([[prototype]]-Link)。
  3. 被调用funciton中的this被设定为指向新的物件。
  4. 除非function return属於自身的物件,否则这个new调用的function会自动return这个新创建的物件。
function foo(a) {
	this.a = a;
}

var bar = new foo( 2 );
console.log( bar.a ); // 2

使用new来调用foo(...)等於我们建立了一个新的物件并将function中的this指向这个新创出来的物件,这种绑定新建出物件的方法称为new binding。

参考文献:You Don't Know JavaScript


<<:  格线系统(2) DAY44

>>:  如何在Windows 10中隐藏修复分区

【资料结构】串链的表示法

串链的表示法 基本介绍 1.矩阵表示法: 若G(V,E)是含n个顶点的图,表示图G的矩阵为mat[n...

[Day13] 建立订单交易API_6

本节将进行继续完成虚拟订单的功能 首先每组订单需要一组订单编号,为了方便,笔者这边采UUID的方式,...

【PHP Telegram Bot】Day20 - sendMessage:发送和转换 Markdown 讯息

今天先来点轻松的,先来看看各种 sendMessage 的功能,最後来转换使用者发送的 Markd...

架构总览与闲聊

终於到了最後一天了,不知道把这三十天看完的人有多少呢?希望看到最後一天的人,有感受到我对於这系列文章...

Day 20 - React.memo

如果有错误,欢迎留言指教~ Q_Q 没写完啦 子元件通常会接收父元件的 state 或 event...