https://cythilya.github.io/2018/10/10/intro-2/ 看完後在緊急補一下ES6
原型(Prototype)( https://blog.techbridge.cc/2017/04/22/javascript-prototype/ )
原型可說是物件的一種 fallback 機制,當在此物件找不到指定屬性時,就會透過原型鏈結(prototype link / prototype reference)追溯到其父物件上。範例如下,若想存取 bar.a
但由於 bar 並無 a 屬性,因此就會透過原型鏈結找到 foo,並得到 100 這個值。
var foo = { a: 100 };
var bar = Object.create(foo); // 建立 bar 物件,並連結到 foo
bar.b = 'hi';
bar.a // 100,委派給 foo
bar.b // 'hi'
另外,原型最常應用於「行為委派」(behavior delegation),如上例所示,將物件 bar 的行為委派給 foo,這也是常聽到很類似於其他語言的類別的繼承功能,但其實完全不同。
bar.a
但由於 bar 並無 a 屬性,因此就會透過原型鏈結找到 foo,並得到 100 這個值。var foo = { a: 100 };
var bar = Object.create(foo); // 建立 bar 物件,並連結到 foo
bar.b = 'hi';
bar.a // 100,委派給 foo
bar.b // 'hi'
https://blog.techbridge.cc/2017/04/22/javascript-prototype/ 該來理解 JavaScript 的原型鍊了
new运算符的缺点
用构造函数生成实例对象,有一个缺点,那就是无法共享属性和方法。
比如,在DOG对象的构造函数中,设置一个实例对象的共有属性species。
function DOG(name){
this.name = name;
this.species = '犬科';
}
然后,生成两个实例对象:
var dogA = new DOG('大毛');
var dogB = new DOG('二毛');
这两个对象的species属性是独立的,修改其中一个,不会影响到另一个。
dogA.species = '猫科';
alert(dogB.species); // 显示"犬科",不受dogA的影响
每一个实例对象,都有自己的属性和方法的副本。这不仅无法做到数据共享,也是极大的资源浪费(站了兩個空間)。
prototype属性的引入
考虑到这一点,Brendan Eich决定为构造函数设置一个prototype属性。
这个属性包含一个对象(以下简称"prototype对象"),所有实例对象需要共享的属性和方法,都放在这个对象里面;那些不需要共享的属性和方法,就放在构造函数里面。
实例对象一旦创建,将自动引用prototype对象的属性和方法。也就是说,实例对象的属性和方法,分成两种,一种是本地的,另一种是引用的。
还是以DOG构造函数为例,现在用prototype属性进行改写:
function DOG(name){
this.name = name;
}
DOG.prototype = { species : '犬科' };
var dogA = new DOG('大毛');
var dogB = new DOG('二毛');
alert(dogA.species); // 犬科
alert(dogB.species); // 犬科
现在,species属性放在prototype对象里,是两个实例对象共享的。只要修改了prototype对象,就会同时影响到两个实例对象。
DOG.prototype.species = '猫科';
alert(dogA.species); // 猫科
alert(dogB.species); // 猫科
console.log(dogA.species === dogB.species) // false
P.S. 有些人會直接在
Array.prototype
上面加一些函式,讓自己可以更方便地做一些操作,原理也是這樣。可是一般來說,不推薦直接去修改不屬於你的 Object。1 2 3 4 5 | Array.prototype.last = function () { return this[this.length - 1]; }; console.log([1,2,3].last()) // 3 |
__proto__ (這個是要深入研究運行原理的,較複雜,而且不實用,可視情況跳過)
JavaScript 怎麼知道要到prototype去找?
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.log = function () {
console.log(this.name + ', age:' + this.age);
}
var nick = new Person('nick', 18);
console.log(nick.__proto__ === Person.prototype === Function.prototype) // true
//子.__proto__ === 父.prototype (不能越級!!!)
// 那 Person.prototype.__proto__ 會指向誰呢?會指向 Object.prototype
console.log(Person.prototype.__proto__ === Object.prototype) // true
// 那 Object.prototype.__proto__ 又會指向誰呢?會指向 null,這就是原型鍊的頂端了
console.log(Object.prototype.__proto__) // null
nick 的__proto__
會指向Person.prototype
,所以在發現 nick 沒有 log 這個 method 的時候,JavaScript 就會試著透過__proto__
找到Person.prototype
,去看Person.prototype
裡面有沒有 log 這個 method。
那假如Person.prototype
還是沒有呢?那就繼續依照這個規則,去看Person.prototype.__proto__
裡面有沒有 log 這個 method,就這樣一直不斷找下去。找到時候時候為止?找到某個東西的__proto__
是 null 為止。意思就是這邊是最上層了。
而上面這一條透過__proto__
不斷串起來的鍊,就叫做原型鍊。透過這一條原型鍊,就可以達成類似繼承的功能,可以呼叫自己 parent 的 method。
如果想知道一個屬性是存在 instance 身上,還是存在於它屬於的原型鍊當中,可以用hasOwnProperty
這個方法:
1
2
3
4
5
6
7
8
9
10
11
12
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.log = function () {
console.log(this.name + ', age:' + this.age);
}
var nick = new Person('nick', 18);
console.log(nick.hasOwnProperty('log')); // false
console.log(nick.__proto__.hasOwnProperty('log')); // true
有了hasOwnProperty
之後,我們就可以自己來模擬這段往上找的過程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.log = function () {
console.log(this.name + ', age:' + this.age);
}
var nick = new Person('nick', 18);
function call(obj, methodName) {
var realMethodOwner = obj;
// 不斷往上找,直到 null 或者是找到真的擁有這個 method 的人為止
while(realMethodOwner && !realMethodOwner.hasOwnProperty(methodName)) {
realMethodOwner = realMethodOwner.__proto__;
}
// 找不到就丟一個 error,否則執行這個 method
if (!realMethodOwner) {
throw 'method not found.';
} else {
realMethodOwner[methodName].apply(obj);
}
}
call(nick, 'log'); // nick, age:18
call(nick, 'not_exist'); // Uncaught method not found.
__proto__
會指向Person.prototype
,所以在發現 nick 沒有 log 這個 method 的時候,JavaScript 就會試著透過__proto__
找到Person.prototype
,去看Person.prototype
裡面有沒有 log 這個 method。Person.prototype
還是沒有呢?那就繼續依照這個規則,去看Person.prototype.__proto__
裡面有沒有 log 這個 method,就這樣一直不斷找下去。找到時候時候為止?找到某個東西的__proto__
是 null 為止。意思就是這邊是最上層了。__proto__
不斷串起來的鍊,就叫做原型鍊。透過這一條原型鍊,就可以達成類似繼承的功能,可以呼叫自己 parent 的 method。hasOwnProperty
這個方法:1 2 3 4 5 6 7 8 9 10 11 12 | function Person(name, age) { this.name = name; this.age = age; } Person.prototype.log = function () { console.log(this.name + ', age:' + this.age); } var nick = new Person('nick', 18); console.log(nick.hasOwnProperty('log')); // false console.log(nick.__proto__.hasOwnProperty('log')); // true |
hasOwnProperty
之後,我們就可以自己來模擬這段往上找的過程:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | function Person(name, age) { this.name = name; this.age = age; } Person.prototype.log = function () { console.log(this.name + ', age:' + this.age); } var nick = new Person('nick', 18); function call(obj, methodName) { var realMethodOwner = obj; // 不斷往上找,直到 null 或者是找到真的擁有這個 method 的人為止 while(realMethodOwner && !realMethodOwner.hasOwnProperty(methodName)) { realMethodOwner = realMethodOwner.__proto__; } // 找不到就丟一個 error,否則執行這個 method if (!realMethodOwner) { throw 'method not found.'; } else { realMethodOwner[methodName].apply(obj); } } call(nick, 'log'); // nick, age:18 call(nick, 'not_exist'); // Uncaught method not found. |
閉包(Closure)
閉包是指變數的生命週期只存在於該函式內,一旦離開了函式,該變數就會被回收而不可再利用,且必須在函式內事先宣告。
範例如下,在函式 closure 內可以存取 a 的值,但離開了函式 closure 走到全域範疇之下,就取不到 a 的值了,因此會被報錯「Uncaught ReferenceError: a is not defined」。
function closure() {
var a = 1;
console.log(a); // 1
}
closure();
a // Uncaught ReferenceError: a is not defined
function closure() {
var a = 1;
console.log(a); // 1
}
closure();
a // Uncaught ReferenceError: a is not defined
模組(Module)
模組模式(Module Pattern)又稱為揭露模組(Revealing Module),經由建立一個模組實體(Module Instance,如下範例的 foo),來調用內層函式。而內層函式由於具有閉包的特性,因此可存取外層包含函式(Outer Enclosing Function)之內的變數和函式。透過模組模式,可隱藏私密的資訊,並對外公開 API。
範例如下,CoolModule 對外公開 API doSomething 和 doAnother,CoolModule 之外是無法取得其私有的 something 和 another 兩個變數的值。
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
什麼是Module Pattern?解決什麼問題?
Module Pattern 利用函數的「閉包(closure)」特性來避免汙染全域的問題 - 使用閉包(closure)來提供封裝的功能,將方法和變數限制在一個範圍內存取與使用。這樣的好處除了避免汙染全域外,也將實作隱藏起來,只提供公開的介面(public API)供其他地方使用,簡單易懂。
物件實字(object literals)
第一個先來看物件實字(object literals)的觀念。
在object literal notation中,物件中的屬性和方法用這樣的方式被描述 - name/value pairs,即key/value的方式,並用分號(:)區隔 - 每個name/value pairs用逗號(,)區隔,而最後一個name/value pair後不需使用逗號結尾 - 使用大括號({})包裝起來
範例如下。
var myObjectLiteral = {
variableKey: variableValue,
functionKey: function(){
// ...
}
};
使用object literal的好處是將程式碼封裝和組織起來。
如果用new來建構也是可以的,但會發生一些非預期的結果,並不建議使用。
[Note] JavaScript並沒有如同我們熟知的其他語言(例如:Java、C#等)有private、protected和public這些語法可用,而要靠函數的作用域,即「閉包(closure)」來實作。
閉包(closure)
再來看閉包(closure)的觀念。
Closure是指變數的生命週期只存在於該function中,一旦離開了function,該變數就會被回收而不可再利用,且必須在function內事先宣告。
function closure() {
var a = 1;
console.log(a); //1
}
closure();
console.log(a); //Uncaught ReferenceError: a is not defined
對於closure進一步的探討可參考這篇文章 Closures。
Module Pattern的範例
來看一個Module Pattern的實際範例。
var testModule = (function(){
var counter = 0;
return {
incrementCounter: function(){
return counter++;
},
resetCounter: function(){
console.log('counter value prior to reset: ' + counter);
counter = 0;
}
};
}());
//test
testModule.incrementCounter();
testModule.resetCounter(); //counter value prior to reset: 1
在這裡,變數counter是個private變數,無法被function外的其他地方任意存取,只能經由公開方法incrementCounter 和 resetCounter取用。 function最後會return一個物件,這個物件即是公開出去的API,讓程式的其他區域可以與之互動。 而這就是利用函數的閉包特性來達成的。
我們再看一個更複雜的例子...
var basketModel = (function(){
//private
var basket = [];
function doSomethingPrivate(){
//...
}
function doSomethingElsePrivate(){
//...
}
//public
return {
addItem: function(value){
basket.push(value);
},
getItemCount: function(){
return basket.length;
},
doSomething: doSomethingPrivate(),
getTotal: function(){
var q = this.getItemCount(),
p = 0;
while(q--){
p = p + basket[q].price;
}
return p;
}
}
}());
basketModel.addItem({
item: 'bread',
price: 0.5
});
basketModel.addItem({
item: 'butter',
price: 0.3
});
console.log(basketModel.getItemCount()); //2
console.log(basketModel.getTotal()); //0.8
我們這樣的存取是不行的...
console.log(basketModule.basket); //"basket"是private variable,不提供函數外部存取
console.log(basket); //"basket"在函數內部的時候才可以這樣呼叫
所以我們就可以為Module Pattern做一個範本,如下。
var myNamespace = (function(){
//private members
var myPrivateVariable = 0;
var myPrivateMethod = function(someText){
console.log(someText);
};
//public members
return {
myPublicVariable: 'foo',
myPublicFunction: function(bar){
myPrivateVariable++;
myPrivateMethod(bar);
}
};
}());
console.log(myNamespace.myPublicVariable); //foo
myNamespace.myPublicFunction('hi'); //hi
優缺點?
優點是清楚的物件導向、封裝概念。
缺點是
- 如果要變數或方法的public/private狀態,我們必須要去用到的每一個地方手動修改使用方式
- 在debug時,對於private members較難偵測
- private members難以擴充,彈性不高
嚴格模式(Strict Mode)
嚴格模式簡單說就是為了預防開發者的一些不小心或錯誤的行為,JavaScript 引擎協助做了一些檢測的工作,當開發者誤用時就把錯誤丟出來。可參考-MDN。
範例如下,在未宣告變數而賦值的狀況下,會無預警的產生一個全域變數,但若使用嚴格模式('use strict'
)則會禁止這行為外,還會報錯,告知開發者變數尚未被定義。
'use strict';
a = 1; // Uncaught ReferenceError: a is not defined
'use strict'
)則會禁止這行為外,還會報錯,告知開發者變數尚未被定義。'use strict';
a = 1; // Uncaught ReferenceError: a is not defined
巢狀範疇(Nested Scope)
若在目前執行的範疇找不到這個變數的時候,就會往外層的範疇搜尋,持續搜尋直到找到為止,或直到最外層的全域範疇(global scope,在瀏覽器底下就是指 window)。
如下,console.log(a + b)
中,b 無法在 foo 中找到,但可從全域範疇中追出來。
const foo = (a) => {
console.log(a + b);
}
const b = 2;
foo(2); // 4
console.log(a + b)
中,b 無法在 foo 中找到,但可從全域範疇中追出來。const foo = (a) => {
console.log(a + b);
}
const b = 2;
foo(2); // 4
拉升(Hoisting)
在程式執行前,編譯器(compiler)會先由上到下逐行將程式碼轉為電腦可懂的命令,然後再執行編譯後的指令。在這個編譯的階段,編譯器找出所有的變數並繫結所屬範疇,但不賦值,所以此刻變數所帶的值是 undefined;而在執行階段,JavaScript 引擎才會處理給值的事情。
我們可以把這個過程想像成是將這些變數「提升」到程式碼的最頂端,如下範例所示,因此當印出 a 的值的時候,會是已宣告但還沒賦值的狀態,也就是有這個變數,但其值是 undefined,一直到程式執行了,才給值。因此,我們可以在程式碼任何地方呼叫運用這個變數,但只有在正式宣告之後才能有正確的值可用,在宣告之前使用都會得到 undefined。
var a; // 編譯時期的工作
console.log(a); // undefined
a = 2; // 執行時期的工作
var a; // 編譯時期的工作
console.log(a); // undefined
a = 2; // 執行時期的工作
不等性(Inequality)
關於不等性的比較運算子有
>
、<
、>=
、<=
共四種,在這裡有幾種狀況需要注意- 若比較的值都是字串,則以字典的字母順序為主。
'ab' < 'cd' // true
- 若比較的值型別不同,由於值的不等性比較沒有嚴格不相等這種情況,因此,無論什麼樣的比較都會被強制轉型為數字,無法轉為數字的就會變成 NaN。
'99' > 98 // true,字串 '99' 被強制轉型為數字 99
'Hello World' > 1 // false,字串 'Hello World' 無法轉為數字,變成 NaN
'Hello World' < 1 // false
'Hello World' = 1 // false
- NaN 不大於、不小於、不等於任何值,當然也不等於自己。
NaN > NaN // false
NaN < NaN // false
NaN === NaN // false
NaN == NaN // false
typeof
typeof 可用於檢測值的型別是什麼。
typeof 'Hello World!' // 'string'
typeof true // 'boolean'
typeof 1234567 // 'number'
typeof null // 'object'
typeof undefined // 'undefined'
typeof { name: 'Jack' } // 'object'
typeof Symbol() // 'symbol'
typeof function() {} // 'function'
typeof [1, 2, 3] // 'object'
typeof NaN // 'number'
不懂得都趕緊看一看
https://cythilya.github.io/archieve/
沒有留言:
張貼留言