【讀書筆記】你不知道的JavaScript(上)
第一部分 作用域和閉包
第一章 作用域
當前程式碼所執行的上下文,即當前可訪問的所有變數的集合;
編譯原理
編譯的三個步驟:
- 分詞/詞法分析(Tokenizing/Lexing) :將字串分解成有意義的程式碼塊,這些程式碼塊被稱為詞法單元(token)。如將var a = 2;分解為var、a、=、2、;。空格是否被當做詞法單元,取決於空格在這門語言中是否有意義;(分詞和詞法的區別在於拆分的詞法單元是否有狀態,有狀態是詞法,無狀態是分詞);
- 解析/語法分析(Parsing) : 將詞法單元流(陣列)轉換成一個由元素逐級巢狀所組成的代表了程式語法結構的樹,即AST抽象語法樹;
- 程式碼生成 : 將AST轉換為可執行程式碼(機器指令)的過程;
JS引擎除了要做上面那三步,還需要在語法分析和程式碼生成階段有特定的步驟來對執行效能進行優化,如對冗餘元素進行優化;JS的編譯過程也不是發生在構建之前,而是發生在程式碼執行前的幾微秒(甚至更短)的時間內。
作用域的背後是用各種如JIT、延遲編譯、重編譯等辦法來保證效能的最佳;
編譯完成上面三個步驟之後,則需要把生成的執行時所需的程式碼給引擎做處理;
如var a = 2;這段賦值操作,編譯器會在當前作用域中宣告一個變數(如果作用域已經有了,會直接忽略),然後在執行時引擎會在作用域中查詢該變數,如果有,才會對它進行賦值;
引擎:從頭到尾負責整個javascript程式的編譯及執行;
編譯器:負責語法分析及程式碼生成等髒活累活;
作用域:負責收集並維護由所有宣告的識別符號(變數)組成的一系列查詢,並實施一套非常嚴格的規則,確定當前執行的程式碼對這些識別符號的訪問許可權;
RHS(retrieve his source value)查詢: 查詢某個變數的值;
LHS(list his source value)查詢: 找到變數的容器,從而對其賦值;
例子:
function foo(a) {
console.log(a); // 2
}
foo(2);
- 對foo進行RHS查詢;
- 對a進行LHS查詢,把2賦值給它;
- 對console進行RHS查詢;
- 對a進行RHS查詢;
PS:編譯器可以在程式碼生成的同時處理宣告和值的定義,所以在引擎執行程式碼時,不會有專門的執行緒將函式分配給foo,所以,這裡沒有LHS查詢;
作用域巢狀
當一個塊或函式巢狀在另一個塊或函式中時,則發生了作用域巢狀;當在當前作用域中無法找到某個變數時,引擎就會在外層巢狀的作用域中繼續查詢;如果繼續找不到,則繼續向上一層級繼續查詢,直到抵達全域性作用域時,還沒找到,則停止;
異常
在作用域鏈中找不到對應的變數時,RHS查詢會丟擲ReferenceError異常;LHS查詢在嚴格模式下,也會丟擲ReferenceError,寬鬆/懶惰模式則會自動在全域性作用域下建立宣告該變數;
對變數的值進行不合理操作會丟擲TypeError異常;
ReferenceError同作用域判別失敗相關,而TypeError代表作用域判斷成功了,但是對結果的操作不合法;
第二章 詞法作用域
詞法作用域意味著作用域是由書寫程式碼時變數宣告的位置來決定的,編譯的詞法分析階段基本能夠知道全部識別符號在哪裡以及如何宣告的,從而能預測在執行過程中如何對他們進行查詢;
全域性變數會自動成為全域性物件的屬性;
遮蔽效應:在多層巢狀的作用域中,在作用域鏈中查詢變數,當找到變數就會停止,所以會“遮蔽”了外部的同名變數;
欺騙詞法
詞法作用域一般完全由寫程式碼期間函式所宣告的位置定義,欺騙此法即是修改詞法作用域;
欺騙詞法作用域會導致效能下降;
- 用eval修改詞法作用域(修改到全域性);嚴格模式下,eval執行在自己的詞法作用域中,所以無法修改所在的作用域;
- setTimeout、setInterval、new Function同上;
- with;with可以將一個物件處理為一個完全隔離的詞法作用域,因此這個物件的屬性也會被處理為定義在這個作用域中的詞法識別符號;eval是修改其所處的詞法作用域,with是根據傳遞的物件憑空建立一個全新的詞法作用域;
效能
使用eval和with會使得引擎無法在編譯時對作用域查詢進行優化,過多使用會導致執行變慢;
第三章 函式作用域和塊作用域
函式作用域: 屬於這個函式的全部變數都可以在整個函式的範圍內使用及複用;
隱藏內部實現: 通過作用域,將一些變數、方法封裝起來,避免汙染全域性;
規避衝突: 使用命令空間、模組管理;
自執行函/函式表示式: 隱藏識別符號且不汙染所在作用域的方法;
函式宣告和函式表示式: 他們的名稱識別符號是否會繫結到某個作用域中,表示式不會,宣告會;
匿名函式表示式: 省略函式名;缺點如下:
- 在棧追蹤中不會顯示出有意義的函式名,影響除錯;
- 遞迴只能使用過期的arguments.callee;
- 可讀性、可理解性不好;
行內表示式在封裝上非常有用,所以推薦具名行內表示式;
立即執行函式表示式(Immediately Invoked Function Expression) :(function() {})();
進階用法: 把引數傳遞進去;
var a = 2;
(function IIFE(global) {
var a = 3;
console.log(a);// 3
console.log(global.a);// 2;
})(window);
console.log(a);// 2
塊作用域: 其他型別的作用域單元;表面上看 JavaScript 並沒有塊作用域的相關功能;for、if這種都是繫結到上層;
with: with從物件中創建出的作用域僅在with宣告中而非外部作用域有效;
try/catch: catch會建立一個塊作用域,宣告的變數僅在catch內部有效;
let: 存在塊級作用域;不存在變數提升;
塊作用域重要作用: 幫助將一些沒有用的變數被回收;
const: 常量,值是不能改的;
第四章 提升
變數提升: 將變數的宣告提升到當前作用域的最上方;
編譯器在編譯時,會將所有的宣告找到並綁定當前作用域,而其他操作會留在原地待命;
函式宣告會被提升,但函式表示式不會被提升;
函式優先: 變數和函式提升的時候,先把所有的函式提升到最上面,然後依次是變數;
第五章 作用域閉包
閉包:能夠訪問自由變數的方法所在的作用域形成的閉包;
模組
模組模式需具備的兩個必要條件:
- 必須有外部的封閉函式,該函式必須至少被呼叫一次;
- 封閉函式返回至少一個內部函式,這樣內部函式才能在私有作用域中形成閉包,並且可以訪問或修改私有的狀態;
如下:
function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log( something );
}
function doAnother() {
console.log( another.join( " ! " ) );
}
return {
doSomething: doSomething,
doAnother: doAnother
};
}
var foo = CoolModule();
foo.doSomething(); // cool foo.doAnother(); // 1 ! 2 ! 3
單例模式:
var foo = (function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log( something );
}
function doAnother() {
console.log( another.join( " ! " ) );
}
return {
doSomething: doSomething,
doAnother: doAnother
};
})();
foo.doSomething(); // cool foo.doAnother(); // 1 ! 2 ! 3
未來的模組機制:es6的匯入匯出import和export;
第二部分 this和物件原型
第一章 關於this
this的指向:最後呼叫它的那個物件或上下文;
第二章 this全面解析
函式呼叫棧:執行當前操作時,整個執行函式的呼叫層級列表;
隱式繫結:預設繫結最後呼叫他的物件或上下文;
顯式繫結:bind、call、apply等操作;
硬繫結:
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
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
new:繫結this;
步驟:
- 建立(或者說構造)一個全新的物件。
- 這個新物件會被執行 [[ 原型 ]] 連線。
- 這個新物件會繫結到函式呼叫的 this。
- 如果函式沒有返回其他物件,那麼 new 表示式中的函式呼叫會自動返回這個新物件。
new和顯示繫結優先順序高於隱式繫結;
-
函式是否在 new 中呼叫(new 繫結)?如果是的話 this 繫結的是新建立的物件。 var bar = new foo()
-
函式是否通過 call、apply(顯式繫結)或者硬繫結呼叫?如果是的話,this 繫結的是 指定的物件。 var bar = foo.call(obj2)
-
函式是否在某個上下文物件中呼叫(隱式繫結)?如果是的話,this 繫結的是那個上 下文物件。 var bar = obj1.foo()
-
如果都不是的話,使用預設繫結。如果在嚴格模式下,就繫結到 undefined,否則繫結到 全域性物件。 var bar = foo()
apply的其他用處:展開字串
function foo(a, b) {
console.log('a:' + a + ';b:' + b);
}
foo.apply(null, [1, 2]); // a:1;b:2
bind其他用法:柯里化
function foo(a, b) {
console.log('a:' + a + ';b:' + b);
}
let bar = foo.bind(null, 2);
bar(3);// a:2;b:3
Object.create(null) 和{}很像,但是並不會建立 Object. prototype 這個委託,所以它比 {}“更空”:
軟繫結:會對指定的函 數進行封裝,首先檢查呼叫時的 this,如果 this 繫結到全域性物件或者 undefined,那就把 指定的預設物件 obj 繫結到 this,否則不會修改 this。
if (!Function.prototype.softBind) {
Function.prototype.softBind = function (obj) {
var fn = this; // 捕獲所有 curried 引數
var curried = [].slice.call(arguments, 1);
var bound = function () {
return fn.apply((!this || this === (window || global)) ? obj : this, curried.concat.apply(curried, arguments));
};
bound.prototype = Object.create(fn.prototype);
return bound;
};
}
function foo() {
console.log('name: ' + this.name);
}
var obj = {name: 'obj'},
obj2 = {name: 'obj2'},
obj3 = {name: 'obj3'};
var fooOBJ = foo.softBind(obj);
fooOBJ(); // name: obj
obj2.foo = foo.softBind(obj);
obj2.foo(); // name: obj2 <---- 看!!!
fooOBJ.call(obj3); // name: obj3 <---- 看!
setTimeout(obj2.foo, 10); // name: obj <---- 應用了軟繫結
箭頭函式:繫結this,而且繫結後無法被修改,new也不行;
function foo() { // 返回一個箭頭函式
return (a) => { //this 繼承自 foo()
console.log(this.a);
};
}
var obj1 = {a: 2};
var obj2 = {
a: 3
};
var bar = foo.call(obj1);
bar.call(obj2); // 2, 不是 3 !
第三章 物件
字面量形式:
var myObj = {
}
構造形式:
var myObj = new Object();
muObj.key = value;
js基礎型別:string、number、boolean、null、undefined、object;
簡單基礎型別並不是物件。
JavaScript 中有許多特殊的物件子型別,我們可以稱之為複雜基本型別。如函式、陣列;
內建物件:String • Number • Boolean • Object • Function • Array • Date • RegExp • Error
內建函式可以當作建構函式 (由 new 產生的函式呼叫——參見第 2 章)來使用,從而可以構造一個對應子型別的新對 象。
var strPrimitive = "I am a string";
typeof strPrimitive; // "string" strPrimitive instanceof String; // false
var strObject = new String( "I am a string" );
typeof strObject; // "object"
strObject instanceof String; // true
// 檢查 sub-type 物件
Object.prototype.toString.call( strObject ); // [object String]
會自動把字串字面量轉換成一個 String 物件,可以訪問屬性和方法
屬性訪問方法:.方式,稱之為屬性;[]方式,稱之為鍵;
var myObject = {};
myObject[true] = 'foo';
myObject[3] = 'bar';
myObject[myObject] = 'baz';
myObject['true']; // "foo"
myObject["3"]; // "bar"
myObject["[object Object]"]; // "baz"
可計算屬性名:最常用的場景可能是es6的符號(Symbol)
var prefix = 'foo';
var myObject = {
[prefix + 'bar']: 'hello',
[prefix + 'baz']: 'world'
};
myObject['foobar']; // hello
myObject["foobaz"]; // world
方法:屬於物件(也被稱為“類”)的函式通常被稱為方法;
這裡作者的意思是,所有的函式都可以被繫結this,而js中從本質上講,並不是把函式歸屬到某個物件中,而this是在執行時根據呼叫位置動態繫結的,所以他們最多算間接關係,不是歸屬關係;所以js的函式訪問為屬性訪問,不是方法訪問;
這裡我並不完全贊同作者的看法,舉個例子
var test = function () {
console.log(1, this.a)
}
class Test {
constructor(a) {
this.a = a;
}
}
var a = 'window a';
var obj = new Test('test a');
obj.test = test;
console.log(obj.test()) // test a
test = function () {
console.log(2, this.a)
}
console.log(test()) // 2 window a
這裡把函式test賦值給obj.test之後,更改了test,test變了,但是obj的test並沒有變,也就是說,當你操作obj的test的時候,只能通過obj才能去取得或操作它,所以它是屬於obj的,而方法的定義是屬於物件的函式,因此把物件的函式稱為方法,我覺得沒毛病;作者說的函式中的this可以繫結到其他地方去,所以不屬於物件,但是 如果去重新繫結this是不是還是得去用obj去重新綁?
陣列: 比普通物件多了下標和根據陣列行為和用途進行了優化;
複製物件
淺複製:複製的屬性如果是物件,那麼只是複製引用;
深複製:複製的屬性如果是物件,那麼是新建立一個物件;
由於 Object.assign(..) 就是使用 = 操作符來賦值,所以源物件屬性的一些特性(比如 writable)不會被複制到目標物件。
屬性描述符
// writable
var myObject = {};
Object.defineProperty(myObject, 'a', {
value: 2,
writable: false, // 不可寫
configurable: true,
enumerable: true
});
myObject.a = 3; // 嚴格模式下會報錯TypeError setter也會報這個錯
myObject.a; // 2
// configurable
var myObject = {a: 2};
myObject.a = 3;
myObject.a; // 3
Object.defineProperty(myObject, 'a', {
value: 4,
writable: true,
configurable: false, // 不可配置!
enumerable: true
});
myObject.a;// 4
myObject.a = 5;
myObject.a; // 5
Object.defineProperty(myObject, 'a', {
value: 6,
writable: true,
configurable: true,
enumerable: true
}); // TypeError 不可逆過程 但是這裡可以把 writable 的狀態由 true 改為 false,但是無法由 false 改為 true。還會禁止刪除這個屬性;如delete myObject.a;會不生效
enumerable
配置為false,就不可以對該屬性進行for in迴圈
不變性:在es5中如何實現屬性或物件不可改變,操作有如下;
- 物件常量:可以把writable和configurable設定為false;
- 禁止擴充套件:禁止對物件新增屬性並且保留已有屬性,使用Object.preventExtensions(myObj);在非嚴格模式下,建立屬性 b 會靜默失敗。在嚴格模式下,將會丟擲 TypeError 錯誤。
- 密封:Object.seal(..),會在一個現有物件上呼叫 Object.preventExtensions(..) 並把所有現有屬性標記為 configurable:false。不僅不能新增新屬性,也不能重新配置或者刪除任何現有屬性(雖然可以 修改屬性的值)。
- 凍結:Object.freeze(..),會在一個現有物件上呼叫 Object.seal(..) 並把所有“資料訪問”屬性標記為 writable:false,這樣就無法修改它們 的值。
[[Get]]:獲取值的演算法;
[[Put]]:修改值的演算法;
[[Put]] 演算法:
-
屬性是否是訪問描述符?如果是並且存在 setter 就呼叫 setter。
-
屬性的資料描述符中 writable 是否是 false ?如果是,在非嚴格模式下靜默失敗,在 嚴格模式下丟擲 TypeError 異常。
-
如果都不是,將該值設定為屬性的值。
Getter和Setter
var myObject = { // 給 a 定義一個 getter
get a() {
return 2;
}
};
Object.defineProperty(myObject, // 目標物件
'b', // 屬性名 物件 | 117
{ // 描述符 // 給 b 設定一個 getter
get: function () {
return this.a * 2;
}, // 確保 b 會出現在物件的屬性列表中
set: function (val) {
console.log(val);
},
enumerable: true
});
myObject.a; // 2 myObject.b; // 4
判斷物件中是否存在某個屬性:
- 'a' in myObj; 這種會遍歷原型鏈,只是判斷屬性,而不是值,所以4 in [1,3,4]是false;
- myObj.hasOwnProperty('a');不會遍歷原型鏈;
列舉:通過enumerable設定是否可列舉,不管是不是可以列舉,都可以通過hasOwnProperty判斷是否存在,但是不能通過for in去遍歷到;是否可列舉可用myObj.propertyIsEnumerable('a')去判斷(不遍歷原型鏈);
- Object.keys返回的陣列只包括可列舉的;
- Object.getOwnPropertyNames返回的陣列包含所有屬性;
- 上面兩種形式 都不遍歷原型鏈;
遍歷:
- forEach遍歷且忽略回撥函式的返回值;
- every遍歷直到回撥函式返回false停止;
- somoe遍歷直到回撥函式返回true停止;
- for of 遍歷值;
寫一個迭代器
var myObject = {a: 2, b: 3};
Object.defineProperty(myObject, Symbol.iterator, {
enumerable: false, writable: false, configurable: true, value: function () {
var o = this;
var idx = 0;
var ks = Object.keys(o);
return {
next: function () {
return {value: o[ks[idx++]], done: (idx > ks.length)};
}
};
}
}); // 手動遍歷 myObject
var it = myObject[Symbol.iterator]();
it.next(); // { value:2, done:false }
it.next(); // { value:3, done:false }
it.next(); // { value:undefined, done:true }
// 用 for..of 遍歷 myObject
for (var v of myObject) {
console.log(v);
}// 2 // 3
無限迭代器:
var randoms = {
[Symbol.iterator]: function () {
return {
next: function () {
return {value: Math.random()};
}
};
}
};
var randoms_pool = [];
for (var n of randoms) {
randoms_pool.push(n); // 防止無限執行!
if (randoms_pool.length === 100) break;
}
第四章 混合物件“類”
封裝、繼承、多型;
js中,類是一種設計模式;
類意味著複製,例項化時複製到例項中,繼承複製到子類;
混入模式可以用來模擬類的複製行為,但是通常會產生醜陋且脆弱的語法,讓程式碼更加難懂和難以維護;
第五章 原型
屬性設定和遮蔽
myObj.foo = 'bar';
如果foo不存在myObj,則會遍歷原型鏈,如果原型鏈上沒有,foo會被直接新增到myObj上,如果foo位於原型鏈上層,則是以下三種情況;
-
如果在 [[Prototype]] 鏈上層存在名為 foo 的普通資料訪問屬性(參見第 3 章)並且沒有被標記為只讀(writable:false),那就會直接在 myObject 中新增一個名為 foo 的新 屬性,它是遮蔽屬性。
-
如果在 [[Prototype]] 鏈上層存在 foo,但是它被標記為只讀(writable:false),那麼無法修改已有屬性或者在 myObject 上建立遮蔽屬性(這個限制只存在於 = 賦值中,使用 Object. defineProperty(..) 並不會受到影響)。如果執行在嚴格模式下,程式碼會丟擲一個錯誤。否則,這條賦值語句會被忽略。總之,不會發生遮蔽。
-
如果在 [[Prototype]] 鏈上層存在 foo 並且它是一個 setter(參見第 3 章),那就一定會呼叫這個 setter。foo 不會被新增到(或者說遮蔽於)myObject,也不會重新定義 foo 這 個 setter。
// 這種也會產生隱式遮蔽
var anotherObject = {a: 2};
var myObject = Object.create(anotherObject);
anotherObject.a; // 2 myObject.a; // 2 原型 | 145
anotherObject.hasOwnProperty('a'); // true
myObject.hasOwnProperty('a'); // false
myObject.a++; // 隱式遮蔽! 同 myObject.a = myObject.a + 1
anotherObject.a; // 2
myObject.a; // 3
myObject.hasOwnProperty('a'); // true
類
javaScript 才是真正應該被稱為“面向物件”的語言,因為它是少有的可以不通過類,直接建立物件的語言。 在 JavaScript 中,類無法描述物件的行,(因為根本就不存在類!)物件直接定義自己的行為。
JavaScript 中只有物件。
原型繼承:繼承意味著複製操作,js並不會複製,而是一個物件可以通過委託訪問另一個物件的屬性和函式。
new 會劫持所有普通函式並用構造物件的形式來呼叫它
建構函式:
function Foo(name) {
this.name = name;
}
var a = new Foo('a');
a.constructor
// ƒ Foo(name) {
// this.name = name;
// }
// 這裡看起來像Foo是a的建構函式,但是實際上只是a委託了Foo.prototype的所有物件而已,新物件並不會自動獲取.construstor
Foo.prototype.constructor // 列印一下,
// ƒ Foo(name) {
// this.name = name;
// }
function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // 建立一個新原型物件
var a1 = new Foo();
a1.constructor === Foo; // false!
a1.constructor === Object; // true!
給 Foo.prototype 新增一個 .constructor 屬性,不過這需要手動新增一個符合正常行為的不可列舉(參見第 3 章)屬性。
function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // 建立一個新原型物件
// 需要在 Foo.prototype 上“修復”丟失的 .constructor 屬性
// 新物件屬性起到 Foo.prototype 的作用
// 關於 defineProperty(..),參見第 3 章
Object.defineProperty( Foo.prototype, "constructor" , {
enumerable: false,
writable: true,
configurable: true,
value: Foo // 讓 .constructor 指向 Foo
} );
.constructor 並不是一個不可變屬性。它是不可列舉(參見上面的程式碼)的,但是它的值是可寫的(可以被修改)。
比較好的委託的寫法:
function Foo(name) {
this.name = name;
}
Foo.prototype.myName = function() {
return this.name;
};
function Bar(name,label) {
Foo.call( this, name );
this.label = label;
}
// 我們建立了一個新的 Bar.prototype 物件並關聯到 Foo.prototype
Bar.prototype = Object.create( Foo.prototype );
// 注意!現在沒有 Bar.prototype.constructor 了
// 如果你需要這個屬性的話可能需要手動修復一下它
Bar.prototype.myLabel = function() {
return this.label;
};
var a = new Bar( "a", "obj a" );
a.myName(); // "a"
a.myLabel(); // "obj a"
上面為什麼用了Object.create這種方式,其實還有兩種方式;
- Bar.prototype=Foo.prototype;這種就是直接引用到Foo.prototype上,會導致修改Bar的原型方法或屬性會修改到Foo上去,這樣子的話直接修改Foo就好了,Bar的意義就不存在了;
- Bar.prototype=new Foo();這種會產生副作用,之前說過,new的時候會呼叫對應的函式,如果函式中有多餘的操作,會影響到Bar;
但是Object.create有個缺點:需要建立一個新物件然後把舊物件拋棄掉,所以不能直接修改已有的預設物件。
ES6 添加了輔助函式 Object.setPrototypeOf(..) ,可以用標準並且可靠的方法來修改關聯。
// ES6 之前需要拋棄預設的
Bar.ptototype = Object.create( Foo.prototype );
// ES6 開始可以直接修改現有的
Object.setPrototypeOf( Bar.prototype, Foo.prototype );
如果忽略掉 Object.create(..) 方法帶來的輕微效能損失(拋棄的物件需要進行垃圾回收),它實際上比 ES6 及其之後的方法更短而且可讀性更高。不過無論如何,這是兩種完全不同的語法。
內省(或者反射): 檢查一個例項(JavaScript 中的物件)的繼承祖先(JavaScript 中的委託關聯)。
- a instanceof b; 表示b的原型是否出現在a的原型鏈上,但是這種方法b不可以是物件例項;
- b.isPrototypeOf(a);
- Object.getPrototypeOf(a) === b或Object.getPrototypeOf( a ) === Foo.prototype;
- a.proto === b
proto 的實現大致上是這樣的
Object.defineProperty( Object.prototype, "__proto__", {
get: function() {
return Object.getPrototypeOf(this);
},
set: function(o) {
// ES6 中的 setPrototypeOf(..)
Object.setPrototypeOf(this, o);
return o;
}
});
Object.create模擬
if(!Object.create) {
Object.create = function (o) {
function F () {};
F.prototype = o;
return new F();
}
}
Object.create(..) 的第二個引數指定了需要新增到新物件中的屬性名以及這些屬性的屬性,但是這種用的不多,所以沒有模擬出來
第六章 行為委託
JavaScript 中這個機制的本質就是物件之間的關聯關係。
委託行為意味著某些物件(XYZ)在找不到屬性或者方法引用時會把這個請求委託給另一 個物件(Task)。
- 互相委託(禁止)
- 除錯
function Foo() {
}
var a1 = new Foo();
a1.constructor;
// 谷歌下為 Foo {} 火狐下為 Object { }
//chrome 實際上想說的是“{} 是一個空物件,由名為 Foo 的函式構造”。
//Firefox 想說的是“{} 是一個空物件,由 Object 構造”。之所以有這種細微的差別,
//是因為 Chrome 會動態跟蹤並把 實際執行構造過程的函式名當作一個內建屬性,
// 但是其他瀏覽器並不會跟蹤這些額外的資訊。
a1.constructor.name; // 都是Foo
function Foo() {
}
var a1 = new Foo();
Foo.prototype.constructor = function Gotcha() {
};
a1.constructor; // Gotcha(){}
a1.constructor.name; // "Gotcha"
a1; // Foo {}
// 即使我們把 a1.constructor.name 修改為另一個合理的值(Gotcha),Chrome 控制檯仍然會 輸出 Foo。
var Foo = {};
var a1 = Object.create(Foo);
a1; // Object {}
Object.defineProperty(Foo, "constructor", {
enumerable: false, value: function Gotcha() {
}
});
a1; // Gotcha {}
比較思維模型:通過物件關聯比原型鏈掛載方式更加簡潔(Foo.prototype.speak =...);
我們用一種簡單的設計實現了同樣的功能,這就是物件關聯風格程式碼和行為委託設計模式的力量。
反詞法:簡潔方法有一個非常小但是非常重要的缺點,就是去掉語法糖之後會變成匿名函式;就會帶來匿名函式的缺點;
鴨子型別:如果看起來像鴨子,叫起來像鴨子, 那就一定是鴨子。
我們認為 JavaScript 中物件關聯比類風格的程式碼更加簡潔(而且功能相同)。
行為委託認為物件之間是兄弟關係,互相委託,而不是父類和子類的關係
當你只用物件來設計程式碼時,不僅可以讓語法更加簡潔,而且可以讓程式碼結構更加清晰。
為 ES6 的 class 語法是向 JavaScript 中引入了一種新的“類”機制,其實不是這樣。class 基本上只是現有 [[Prototype]](委託!)機制的一種語法糖。