自定義對象中this為什麼代表A.fn.A.init {}?

我補充一下我自己的困惑:

JavaScript設計模式(張容銘 著)。在第二十七章:鏈模式中,作者解釋說:

∵ new A.fn.init()的構造函數可以是A.fn.init()或者A.init();

A.init()的解釋是: new 關鍵字創建 並通過A.prototype原型找到的(加粗的我沒理解,這裡指的new是哪來的?)。

∴ A.fn.init=A.init--&>A.fn.init=A.fn.A.init

以下為本來提的問題。

======================================

自定義對象A,如下:

A = function() {

}
A.prototype = {
init: function() {
console.log(this);
}
}

當調用new A.prototype.init();的時候控制台列印的結果是A.init {}。

為A.prototype取別名,如下:

A = function() {

}
A.as = A.prototype = {
init: function() {
console.log(this);
}
}

調用new A.prototype.init();列印的結果變成了A.as.A.init {}。

為什麼?

====================================================

原問題地址:自定義對象中this為什麼代表A.fn.A.init {}?

PS:第四個答案是我的,請大神幫忙解答一下,謝謝。

---

以下為補充的書籍原文:


很簡單。題主在依賴規範所沒有統一規定的、JavaScript引擎可以一定程度自由發揮的行為。

(一看題主貼出來的結果就知道題主用的不是Chrome / Chromium。我現在手邊沒有Firefox測不了,不知道題主是不是用Firefox來跑的例子?

——題主更新問題描述後說是用較老版本的Chrome來測的。呵呵這個好玩。)

題主關注的是console.log()對一個自定義JavaScript對象顯示的字元串。注意這個並不是Object.prototype.toString(),而是Console介面的實現自己的行為。toString()如果被覆寫了的話可以包含任意副作用,大家總不想在console.log()的時候還引發不明副作用吧。

根據WHATWG對Console API的規定,console.log的行為如下:

Console Standard

=&> https://console.spec.whatwg.org/#logger

=&> https://console.spec.whatwg.org/#printer

1.3. Printer(logLevel, args)

The printer operation is implementation-defined. It accepts a log level indicating severity, and a List of arguments to print (which are either JavaScript objects, of any type, or are implementation-specific representations of printable things, as produced by the %o and %O specifiers). How the implementation prints args is up to the implementation, but implementations should separate the objects by a space or something similar, as that has become a developer expectation.

所以說規範這裡明確指出JavaScript引擎可以自己決定要列印啥字元串出來。

題主做的實驗所看到的,是JavaScript引擎在console.log()需要輸出對象的字元串表現形式時,以「構造函數名字 { 對象屬性(鍵值對)* }」的格式構造出來的輸出。

所以在這種情況下,JavaScript引擎認為函數(構造函數)的名字是什麼,就是影響題主看到的輸出的關鍵。

演示:在Chrome 51.0.2704.103上測試題主給的這段代碼:

A = function() { }
A.prototype = {
init: function() {
console.log(this);
}
}
var xa = new A.prototype.init()
console.log(xa)

得到的console.log(xa)輸出是:

init {}

對變數xa所指向的對象來說,A.prototype.init是它的構造函數(constructor),而(根據ES6的規定)V8認為這個函數的名字是"init",因而得到上述輸出。

而稍微改改,讓它帶有屬性,則變成:

A = function() { }
A.prototype = {
init: function() {
this.xxx = 3;
this.yyy = 42;
console.log(this);
}
}
var xa = new A.prototype.init()
console.log(xa)

得到的console.log(xa)輸出是:

init {xxx: 3, yyy: 42}

============================================

JavaScript引擎的實現中,一個「函數」(function)的名字是什麼,是個非常有趣的話題。

ES5對如何設置function的name屬性沒有規定,各JavaScript引擎的實現可以自由發揮,這就是題主所用的老版本Chrome的情況;

而ES6(ES2015)有一個簡易的規定:

SetFunctionName - ES2015

然後賦值表達式里的特殊規定對SetFunctionName()的使用:

12.14.4 Runtime Semantics: Evaluation - ES2015

If IsAnonymousFunctionDefinition(AssignmentExpression) and IsIdentifierRef ofLeftHandSideExpression are both true, then

  1. Let hasNameProperty be HasOwnProperty(rval, "name").
  2. ReturnIfAbrupt(hasNameProperty).
  3. If hasNameProperty is false, perform SetFunctionName(rval, GetReferencedName(lref)).

這就跟下面演示的新版本Chrome / V8的行為一致了。注意ES6對對象字面量里也有相似的處理,會通過SetFunctionName()把屬性名設置到函數對象的name屬性上。

要關注ES6的功能在瀏覽器上的兼容性,請參考:ECMAScript 6 compatibility table

具體來說,題主關注的問題,可以參考這裡:function "name" property。展開"Show obsolete platforms"可以看到老版本瀏覽器的支持情況。

下面演示用的瀏覽器版本為:

  • Chrome:51.0.2704.103

  • Safari:9.1.1 (9537.86.6.17)

想想看,下面的這個具名函數聲明:

function Foo() { }

名字Foo是函數聲明的一部分,所以當我們問Foo.name是什麼的時候,理所當然應該得到"Foo"。

而下面這個匿名函數聲明(作為聲明) / 匿名函數字面量(作為表達式):

function () { }

在聲明中沒有包含名字,所以當我們問它的.name屬性時,我們應該得到什麼呢?

Chrome / V8 和 Safari / JavaScriptCore 都說應該返回個空字元串:

(function () { }).name //=&> ""

但當我們把這個匿名函數字面量賦值給一個具名變數時,事情就變得有趣了:

var Bar = function () { }
Bar.name //=&> ???

該例子中,Bar是一個具名(有名字的)變數,那麼Bar.name應該返回什麼?

  • Chrome / V8說:"Bar"

  • Safari / JavaScriptCore說:""

不一致了對不對?那麼Bar.toString()呢?

  • Chrome / V8說:"function () { }"

  • Safari / JavaScriptCore說:"function () { }"

兩者一樣 ,大家都同意這個函數的聲明是沒有名字的。

再來一例,

X = Y = function () { }
X.name //=&> ???
Y.name //=&> ???

  • Chrome / V8說:X.name為"Y",Y.name也為"Y"。

  • Safari / JavaScriptCore說:X.name為"",Y.name也為""。

問題是當前版本的V8是怎麼讓Bar.name返回出"Bar"的呢?這是V8開腦洞實現的一個「便民功能」,也是後來在ES6(ES2015)里規範化的行為:通過模式匹配源碼里把函數字面量賦值給具名變數的代碼,讓該函數聲明記住它是被賦值給什麼名字的變數了。

注意這個便民功能的限制很死,只有當一個匿名函數字面量被賦值給別的東西時,第一個賦值是一個簡單名字的變數,例如:

Foo = function () { }
// Foo.name == "Foo"

或者第一個賦值是對象字面量的屬性聲明,例如:

var x = {
foo: function () { } // here
}
// x.foo.name == "foo"

這個簡單名字才會被記錄為函數對象的名字(name屬性)。

如果第一個賦值的目標是個稍微複雜一點的表達式:

a = {}
X = a.xxx = function () { }
// X.name =&> ""
// a.xxx.name =&> ""

則不會記錄任何名字給這個函數對象,而是用空字元串。

(注意這裡嵌套的賦值表達式里對http://a.xxx的賦值是「第一個」,因為賦值運算符是右結合的。等價於:

X = (a.xxx = function () { })

============================================

當前版本V8里的相關實現細節:(參考的源碼版本為

commit 7a100dffc669a1e261831a37dbe67d2a9f8075fa

Author: vogelheim &

Date: Thu Aug 11 11:17:10 2016 -0700)

V8里,一個函數的靜態部分的信息由SharedFunctionInfo對象記錄:

#define DECL_ACCESSORS(name, type)
inline type* name() const;
inline void set_##name(type* value,
WriteBarrierMode mode = UPDATE_WRITE_BARRIER);

// SharedFunctionInfo describes the JSFunction information that can be
// shared by multiple instances of the function.
class SharedFunctionInfo: public HeapObject {
public:
// [name]: Function name.
DECL_ACCESSORS(name, Object)

// ...
};

runtime/http://runtime-function.cc

RUNTIME_FUNCTION(Runtime_FunctionSetName) {
HandleScope scope(isolate);
DCHECK(args.length() == 2);

CONVERT_ARG_HANDLE_CHECKED(JSFunction, f, 0);
CONVERT_ARG_HANDLE_CHECKED(String, name, 1);

name = String::Flatten(name);
f-&>shared()-&>set_name(*name);
return isolate-&>heap()-&>undefined_value();
}

js/prologue.js

function SetFunctionName(f, name, prefix) {
if (IS_SYMBOL(name)) {
name = "[" + %SymbolDescription(name) + "]";
}
if (IS_UNDEFINED(prefix)) {
%FunctionSetName(f, name);
} else {
%FunctionSetName(f, prefix + " " + name);
}
}

parsing/parser.cc

void ParserTraits::SetFunctionNameFromIdentifierRef(Expression* value,
Expression* identifier) {
if (!identifier-&>IsVariableProxy()) return;
SetFunctionName(value, identifier-&>AsVariableProxy()-&>raw_name());
}

然後parsing/parser-base.h里的幾個調用SetFunctionNameFromIdentifierRef的地方,例如:

template &
typename ParserBase&::ExpressionT
ParserBase&::ParseAssignmentExpression(bool accept_IN,
ExpressionClassifier* classifier,
bool* ok) {
// ...

if (op == Token::ASSIGN) {
Traits::SetFunctionNameFromIdentifierRef(right, expression);
}

// ...
}

這樣就實現了前面引用的ES6對函數對象的"name"屬性的值的規定。

============================================

較老版本的V8里有一套相對複雜的「推導匿名函數的函數名」的實現。

例如說,參考這個版本:GitHub - v8/v8 at chromium/2370

其中有一個FuncNameInferrer類,

// FuncNameInferrer is a stateful class that is used to perform name
// inference for anonymous functions during static analysis of source code.
// Inference is performed in cases when an anonymous function is assigned
// to a variable or a property (see test-func-name-inference.cc for examples.)
//
// The basic idea is that during parsing of LHSs of certain expressions
// (assignments, declarations, object literals) we collect name strings,
// and during parsing of the RHS, a function literal can be collected. After
// parsing the RHS we can infer a name for function literals that do not have
// a name.

在Parser里的fni_欄位指向它的一個實例。它會在parse過程中收集信息,例如:

Expression* ParserTraits::ExpressionFromIdentifier(const AstRawString* name,
int start_position,
int end_position,
Scope* scope,
AstNodeFactory* factory) {
if (parser_-&>fni_ != NULL) parser_-&>fni_-&>PushVariableName(name);
// ...
}

這樣,一邊parse,一邊可以在看到賦值表達式或對象字面量聲明等語法結構時,parse左手邊收集名字記錄到FuncNameInferrer里,parse右手邊得到匿名函數字面量聲明,最後讓FuncNameInferrer把收集到的名字記錄到匿名函數聲明作為它的name屬性。

這個做法相當複雜,在V8更完善地支持ES6之後已經不那麼依賴它來生成匿名函數對象的name屬性的值了——雖然這塊代碼還存在。

在console.log()的實現中,它會問V8要某個對象的構造函數的名字:

String* Map::constructor_name() {
Object* maybe_constructor = GetConstructor();
if (maybe_constructor-&>IsJSFunction()) {
JSFunction* constructor = JSFunction::cast(maybe_constructor);
String* name = String::cast(constructor-&>shared()-&>name());
if (name-&>length() &> 0) return name;
String* inferred_name = constructor-&>shared()-&>inferred_name();
if (inferred_name-&>length() &> 0) return inferred_name;
Object* proto = prototype();
if (proto-&>IsJSObject()) return JSObject::cast(proto)-&>constructor_name();
}
// TODO(rossberg): what about proxies?
// If the constructor is not present, return "Object".
return GetHeap()-&>Object_string();
}

這個邏輯是:

  • 函數自身有名字("name")的話,返回那個名字
  • 函數沒有名字的話,返回通過FuncNameInferrer推導出來的名字("inferred_name")
  • 前兩者都沒成功的話,返回原型([[Prototype]] / __proto__)的構造函數名(這裡遞歸了)

============================================

題主的問題來自《JavaScript設計模式》,我沒有讀過這本書,不了解它整個上下文是如何描述的,所以無法確認書中這塊到底說得對不對。


推薦閱讀:

webstorm 如何自定義代碼的補全提示,快捷輸入?
瀏覽器自身為什麼不集成js,jQuery文件?反正每個網站基本都會用到?
web前端工程師的迷茫?
一般用哪些工具做大數據分析?
你是如何學會正則表達式的?

TAG:JavaScript | 編程 | JavaScript引擎 |