自定義對象中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/#printer1.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 - ES2015If IsAnonymousFunctionDefinition(AssignmentExpression) and IsIdentifierRef ofLeftHandSideExpression are both true, then
- Let hasNameProperty be HasOwnProperty(rval, "name").
- ReturnIfAbrupt(hasNameProperty).
- If hasNameProperty is false, perform SetFunctionName(rval, GetReferencedName(lref)).
這就跟下面演示的新版本Chrome / V8的行為一致了。注意ES6對對象字面量里也有相似的處理,會通過SetFunctionName()把屬性名設置到函數對象的name屬性上。
- 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 7a100dffc669a1e261831a37dbe67d2a9f8075faAuthor: vogelheim &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());
}
template &
typename ParserBase&
ParserBase&
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.
Expression* ParserTraits::ExpressionFromIdentifier(const AstRawString* name,
int start_position,
int end_position,
Scope* scope,
AstNodeFactory* factory) {
if (parser_-&>fni_ != NULL) parser_-&>fni_-&>PushVariableName(name);
// ...
}
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__)的構造函數名(這裡遞歸了)
推薦閱讀:
※webstorm 如何自定義代碼的補全提示,快捷輸入?
※瀏覽器自身為什麼不集成js,jQuery文件?反正每個網站基本都會用到?
※web前端工程師的迷茫?
※一般用哪些工具做大數據分析?
※你是如何學會正則表達式的?
TAG:JavaScript | 編程 | JavaScript引擎 |