2016年6月29日

[筆記] 談談JavaScript中最單純的原型繼承(prototypal inheritance)─ Object.create

img
在先前的筆記中,我們談到可以使用 function constructor 來建立物件,同時我們也提到 function constructor 這種概念是用來模仿 Java 語言而產生的(參考:[筆記] 談談 JavaScrip t中的 function constructor 和關鍵字 new),在其它的程式語言中,會用 class 這個關鍵字來設定該物件要長什麼樣子,然後透過關鍵字 new 來建立物件。
然而,和其他程式語言不同的地方在於,JavaScript 實際上使用的是 prototypal inheritance 而不是 classic inheritance,所以為了讓 JavaScript 回歸單純的 prototypal inheritance,現在的瀏覽器大部分都支援Object.create() 這種單純的方式來建立物件。
讓我們透過下面的例子,更清楚 pure prototypal inheritance 和 Object.create() 的操作。
首先,我們先使用下面這段程式建立一個原型的物件:
var Person = {
  firstName: 'Default',
  lastName: 'Default',
  getFullName: function () {
    return this.firstName + " " + this.lastName;
  }
}
注意一下 return 的地方,這裡我們用的是 this.firstName 而不是直接用 firstName。當我使用 this 這個關鍵字的時候,它會指稱到的是 Person 這個物件,但是如果沒有使用 this 的話,會變成在 getFullName 這個 execution context 中找 firstNamelastName 這兩個變數,如果找不到的話就會往最外層的全域環境(global environment)去找,最後如果還是找不到,就會出現 "Uncaught ReferenceError: firstName is not defined" 。

Object.create() 的使用

接下來我們說明如何使用 Object.create()
var john = Object.create(Person);
console.log(john);
輸出的結果如下,它是一個空物件,但是它繼承了 Person 這個物件當中的屬性和方法:
透過 Object.create() 可以建立一個空物件,同時可以將你帶入 Object.create() 的參數內容變成該物件的原型。
由於原型鍊(prototype chain),可以在這個物件當中建立相同屬性的內容,在執行的時候它會先找物件中最上層的屬性,於是就不會得到 default 的結果,像是這樣:
var Person = {
  firstName: 'Default',
  lastName: 'Default',
  getFullName: function () {
    return this.firstName + " " + this.lastName;
  }
}

var john = Object.create(Person);

john.firstName = 'John';
john.lastName = 'Doe';
console.log(john.getFullName());
如此,對於 firstNamelastName 來說,在該物件就已經有這兩個屬性,因此它不會在往該物件的原型去尋找,而對 getFullName 來說,因為在 john 這個物件裡沒有這個方法,於是就會到 prototype 裡面去找,最後會回傳 "John Doe"。
透過 Object.create() 這種方法,是最單純使用 prototypal inheritance 的方式。如果你想要定一個物件的原型,就先建立一個 A 物件當做其他物件的基礎,然後再建立另一個空物件 B,指稱 A 物件當做它的原型,在透過為 B 物件賦予屬性或方法。
使用 Object.create() 的方式運用繼承和原型的概念能夠非常簡便的建立物件。

如果瀏覽器不支援 Object.create()

雖然大部分的新瀏覽器都支援這樣的做法,但如果某些瀏覽器的版本真的舊到無法使用 Object.create() 的話,可以怎麼辦呢?
這時候我們會寫一些程式來填補某些瀏覽器不支援的情況,我們把這些程式稱做 polyfill
如果我們不確定瀏覽器是不是有支援 Object.create() 的話,我們可以寫如下的 polyfill
// polyfill for Object.create()

if (!Object.create) {
  Object.create = function (o) {
    if (arguments.length > 1) {
      throw new Error('Object.create implementation only accepts the first parameter');
    }

    function F() {};
    F.prototype = o;
    return new F();
  };
}
讓我們來了解一下這段程式碼。
首先,if(!Object.create) 是用來判斷瀏覽器中是否有內建 Object.create() 的函式,如果沒有的話就會回傳 undefined ,在前面加一個 ! 這個邏輯運算子,則會把 undefined 轉換成布林值,所以這段程式碼轉成中文的話,意思就是「如果 Object.create 不存在的話,則執行... 」。
Object.create 不存在的時候,接著會建立 Object.create 這個 function。 if(arguments.length > 1) 是說明如果所代入的參數超過一個的話,會在 console.log 中回傳錯誤訊息。
最後會去執行 Object.create 這個函式原本會執行的內容,也就是先建立一個空的函式 F(){ },然後把原本基礎物件的內容放入 F.prototype 中,最後再用函式建構式(function constructors)的方式,回傳 new F( ) ,如此,就能夠達到 Object.create() 原本的效果。

程式範例

//  polyfill
if (!Object.create) {
  Object.create = function (o) {
    if (arguments.length > 1) {
      throw new Error('Object.create implementation only accepts the first parameter');
    }

    function F() {};
    F.prototype = o;
    return new F();
  };
}

var Person = {
  firstName: 'Default',
  lastName: 'Default',
  getFullName: function () {
    return this.firstName + " " + this.lastName;
  }
}

var john = Object.create(Person);

john.firstName = 'John';
john.lastName = 'Doe';
console.log(john.getFullName());

資料來源

[筆記] 談談 JavaScript 中 for ... in 這個 function

img
在 JavaScript 中有一個非常常用到的函式 for ... in 迴圈,for...in 的用法和 Array.prototype.forEach 很像,但它可以針對**物件(Object)陣列(Array)**來使用。

for in 針對物件的基本使用

讓我們先來看一下下面這個例子:
var john = {
  firstName: 'John',
  lastName: 'Doe'
};

for (var prop in john) {
  console.log(prop + ':' + john[prop]);
}
我們建立一個物件名稱為 john,而 prop 是自訂的變數,會把該物件的屬性存在這個變數中,接著讀取下一個屬性,重覆直到沒有屬性為止。透過 for...in,可以把該物件中的所有屬性名稱和屬性值都呼叫出來。
延伸閱讀:for...in @ MDN

可能問題一:包含繼承屬性的物件 - hasOwnProperty

然而,如果我們是透過函式建構式(function constructor)來建立物件時,這個物件可能會繼承該函式建構式的一些屬性或方法,這時候當我們直接使用 for...in 時,這些繼承而來的屬性和方法也會被一併輸出,如下:
//  function constructor
var Person = function (firstName, lastName) {
  this.firstName = firstName
  this.lastName = lastName
}

//  function constructor 的 prototype
Person.prototype.getFullName = function () {
  return this.firstName + ' ' + this.lastName
}

//  根據 function constructor 所建立的物件 Customer1
var Customer1 = new Person('John', 'Doe')

//  透過for...in輸出
for (var prop in Customer1) {
  console.log(prop + ': ' + Customer1[prop])
}
透過這種方法,我們會發現連同prototype中繼承的方法也被輸出了:
img
如果想要解決這個問題,我們會需要使用到 Object.prototype.hasOwnProperty 這個內建的函式,透過 obj.hasOwnProperty(prop),它會回傳 Boolean,以此區分這個屬性是直接的或是繼承而來的,也就是說,透過這個方法,它不會往該物件的原型鏈(prototype chain)去檢查。寫法如下:
for (var prop in Customer1) {
  // 不是繼承而來的屬性,才輸出...
  if (Customer1.hasOwnProperty(prop)) {
    console.log(prop + ': ' + Customer1[prop]);
  }
}
如此,我們輸出的結果就只會有 firstName 和 lastName 這兩個直接的屬性。

可能問題二:包含繼承屬性的陣列,儘可能不要使用 for...in

在 JavaScript 中,陣列(Array)也是一種物件,因此,我們也可以對陣列使用 for...in 的方法來輸出陣列的內容,如下:
var arr = ['John', 'Jane', 'Jim']
for (var prop in arr) {
  console.log(prop + ': ' + arr[prop])
}
如此,我們會得到如下的結果:
img
然而,當我們使用方括號 [ ] 來建立陣列的時候,其實就和使用 new Array ( ) 是一樣的意思。因此,如果原本的 Array.prototype 有被添加過一些屬性或方法時,使用 for...in 的結果一樣會把這些繼承的屬性和方法給輸出:
Array.prototype.website = 'pjchender'

var arr = ['John', 'Jane', 'Jim']

for (var prop in arr) {
  console.log(prop + ': ' + arr[prop])
}
輸出的結果會把website這個繼承而來的屬性給一併輸出:
img
為了避免這樣的問題,如果是針對陣列在處理的話,會建議可以使用一般的 for 迴圈來輸出陣列就可以了:
for (var i = 0; i < arr.length; i++) {
  console.log(i + ': ' + arr[i])
}
因此,當我們在處理陣列資料時,為了避免呼叫出不必要的屬性,應該儘可能不要使用 for...in 的用法來處理迴圈
var Person = function (firstName, lastName) {
  this.firstName = firstName
  this.lastName = lastName
}

Person.prototype.getFullName = function () {
  return this.firstName + ' ' + this.lastName
}

var Customer1 = new Person('John', 'Doe')

for (var prop in Customer1) {
  if (Customer1.hasOwnProperty(prop)) {
    console.log(prop + ': ' + Customer1[prop])
  }
}

/* ------------------------------------- */
Array.prototype.website = 'pjchender'

var arr = ['John', 'Jane', 'Jim']

for (var prop in arr) {
  console.log(prop + ': ' + arr[prop])
};

for (var i = 0; i < arr.length; i++) {
  console.log(i + ': ' + arr[i])
}

資料來源

2016年6月28日

[筆記] 談談 JavaScript 中內建的 function constructors 及應注意的地方

img
在這篇筆記中讓我們來看一些JavaScript中內建的 function constructors

new Number()

new Number();
首先,讓我們輸入:
var num = new Number(3);
console.log(num);
console.log(Number.prototype);
new 這種用法我們可以理解到 Number 是 JavaScript 中內建的一個 function constructor,也就是說它有內建的 prototype 在內,而且很重要的是,因為它是透過 function construct 加以建立的,所以它雖然看起來是個數值,但它實際上還是個物件
因此,輸出之後我們會看到以下結果:

new String()

new String();
類似的方式我們也可以看看用 String() 這個 function construct 所建立的物件:
var str = new String('網頁設計前端資源');
console.log(str);
console.log(String.prototype);
console.log(str.indexOf("設計"));
在這裡, str 一樣是透過 function constructor 所建立的物件,而不是真正的字串!同時 str 繼承String.prototype 裡面的內容,所以我們可以直接使用 str.indexOf 這樣的用法來查找字串,結果如下:

new Date( )

即使是 Date(),它輸出的結果雖然看起來很像是一個字串,但它實際上還是物件,我們可以這樣做:
var date = new Date("6/28/2016");
console.log(date);
console.log(typeof(date));
console.log(Date.prototype);
如此,我們會發現它依然是個物件,而這個物件繼承了來自 Date.prototype 的屬性和方法:
很重要的一點是:利用這些 function constructors 所建立的東西,看起來可能像數值、字串等等,但實際上,它們都還是物件
有些時候,JavaScript 會知道你想要針對原生值(primitive type)去做一些物件才能處理的事,因此它會幫你將所輸入的原生值進行物件般的處理。
例如,我們輸入:
console.log("PJCHENder".length);    //  9
這時候,一般的原生值是沒有屬性(property)和方法(method)的,我們之所以這樣輸入一樣可以得到 9 的結果,是因為 JavaScript 自動的幫我們把這個原生值進行了物件的處理

針對內建的 function constructor 去增添屬性和方法

在清楚瞭解了原型(prototype)、函式建構式(function constructor)和繼承(inheritance)的概念後,現在我們也可以針對內建的 function construct 去增添裡面的屬性和方法,像是這樣:
String.prototype.isLengthGreaterThan = function(limit){
  return this.length > limit;
}

console.log("PJCHENder".isLengthGreaterThan(4));    //  True
透過這樣的方法,我們幫原本內建的 String 這個 function constructor 新增了一個名為 isLengthGreaterThan 的方法,如此當我們輸入 "PJCHENder".isLengthGreaterThan(4) 就會回傳 true 的結果給我們。
在這裡,一樣可以注意到,原本我們輸入的 "PJCHENder" 是原生的字串值,但 JavaScript 知道我們希望對它進行和物件有關的方法,因此會自動用物件的方式將它做處理,也因此才會回傳 true 的結果。
❗️ 透過這種方式來增添屬性或方法時,要留意的是,不要不小心把原本內建的屬性或方法給無意間覆蓋掉了。
那麼,如果我們想要用同樣的方法,來針對 Number 這個內建函式來操作的話呢?
Number.prototype.isPositive = function(){
  return this > 0;
}

console.log(3.isPositive());
結果會出現錯誤:
之所以會有這個錯誤,主要是因為在上面 console.log(3.isPositive()); 的地方,在先前 JavaScript 雖然能夠幫我們順利將原生值的字串轉成物件來使用內部的方法,但直接使用數值的話會造成 JavaScript 引擎在解析時的錯誤。要解決這個問題只需把 3 先存成一個變數,或在用 () 包住 3 之後再執行就可以了:
Number.prototype.isPositive = function(){
  return this > 0;
}

var d = 3;
console.log(d.isPositive());    // true
console.log((3).isPositive());  // true
如此就能跑出預期的結果了!!
關於 prototype, function constructorinheritance 如果還有觀念不清楚的地方,建議先閱讀先前的文章,以幫助了解這篇的內容。

但最好不要使用內建的函式建構式來產生原生值

如果可以的話,最好不要使用和原生值有關的內建 function constructors。
在上半篇的筆記當中,我們雖然示範了如果針對和原生值有關的內建函式去增添其 prototype 的方法,但其實應該盡可能不要使用和原生值有關的這些內建函式建構式,為什麼呢?
讓我們看一下這段程式碼:
var numPrimitive = 5 ;
var numFromConstructor = new Number(5);
console.log(typeof numPrimitive + "; " + typeof numFromConstructor);    //    number; object
console.log(numPrimitive == numFromConstructor);    //  true
console.log(numPrimitive === numFromConstructor);   //  false
你會發現, numPrimitivenumFromConstructor 雖然看起來很像都是一樣的 5,但如同上面所提的,numPrimitive 是原生的數值,而 numFromConstructor 是物件,這也就是為什麼如果使用 numPrimitive == numFromConstructor 時會回傳 true ,但如果是使用 numPrimitive === numFromConstructor 時則會回傳 false
因為使用 == 時,它會把前後兩個東西強制轉換(coerce)成可比較的型式,但如果是 === 則不會強制轉換,而是會進一步比較兩個的類型(type),當兩者類型不同的時候,就會回傳 false
這也就是為什麼如果可以的話,建議不要使用和原生值有關的 function constructors,因為如果交替使用的話,可能會讓自己最後搞不清楚現在這個值到底是原生值,還是透過 function constructor 所建立出來的物件!
可以的話盡量不要使用內建的函式建構式來產生原生值,因為透過函式建構式產生出來的「原生值」其實是「物件」,這樣可能會導致後來型別判斷上的困難。

程式範例

/*--------------------*/

var a = new Number(3);
console.log(a);
console.log(Number.prototype);

/*--------------------*/

var b = new String("網頁設計前端資源");
console.log(b);
console.log(String.prototype);
console.log(b.indexOf("設計"));

/*--------------------*/

console.log("PJCHENder".length); //  9

/*--------------------*/

var c = new Date("6/28/2016");
console.log(c);
console.log(typeof c);
console.log(Date.prototype);

/*--------------------*/

String.prototype.isLengthGreaterThan = function (limit) {
  return this.length > limit;
}

console.log("PJCHENder".isLengthGreaterThan(4)); //  True

/*--------------------*/

Number.prototype.isPositive = function () {
  return this > 0;
}

var d = 3;
console.log(d.isPositive()); // true

/*--------------------*/

var e = 5;
var f = new Number(5);
console.log(typeof e + "; " + typeof f);
console.log(e == f); //  true
console.log(e === f); //  false

資料來源

2016年6月22日

[筆記] 談談 JavaScript 中的 function constructor 和 prototype 的建立

img
在上一篇筆記中我們說明了如何透過函式建構式(function constructor)搭配關鍵字 new 來建立物件,但其實這樣只學了一半,在這篇我們會補齊另一半,說明 function constructor 如何用來設定該物件的原型(prototype)
我們之前有提到,在 JavaScript 中的函式其實也是一種物件,其中包含一些屬性像是該函式的名稱(Name)和該函式的內容(Code),但其實 function 這裡面還有一個屬性,這個屬性稱做 prototype,這個屬性會以空物件的型式呈現。
除非你是把 function 當做 function constructor 來使用,否則這個屬性就沒有特別的用途;但如果你是把它當做 function constructor,透過 new 這個關鍵字來執行這個 function 的話,它就有特別的意義了。
要進入這個 function 的 prototype 屬性只要直接透過 .prototype 就可以了。
然而,有一點很容易令人困惑的地方,我們會以為如果我使用 .prototype 時,就可以進入這個函式的原型,但實際上並不是這樣的!
函式當中 prototype 這個屬性並不是這個函式的 prototype,它指的是所有透過這個 function constructor 所建立出來的物件的 prototype,聽起來好像有聽沒有懂...沒關係,讓我們來看一些程式碼幫我們理解這個概念。

說明函式中的 prototype 屬性

1. function 中的 prototype 屬性一開始是空物件

我們先執行上篇筆記最後所寫的程式碼:
function Person(firstName, lastName){
  this.firstName = firstName;
  this.lastName = lastName;
}

var john = new Person('John', 'Doe');
console.log(john);

var jane = new Person('Jane', 'Doe');
console.log(jane);
到 Google Chrome 的 console 視窗中,我們輸入 Person.prototype 得到的結果會得到一個空物件,如下圖:

2. 透過 function constructor 所建立的物件會繼承該 function 中 prototype 的內容

接著,讓我們在 Person.prototype 裡面增加一個 getFullName 的函式:
function Person(firstName, lastName){
  this.firstName = firstName;
  this.lastName = lastName;
}

Person.prototype.getFullName = function() {
  return this.firstName + ' ' + this.lastName;
}

var john = new Person('John', 'Doe');
console.log(john);

var jane = new Person('Jane', 'Doe');
console.log(jane);
在上面的程式第 7 - 9 行中,我們為 Person.prototype 添加了一個函式,所以當我們在 Google Chrome 的 console 視窗中呼叫 Person.prototype 時,會多了這個函式在內:
剛剛,我們有提到很重要的一句話,「函式當中 prototype 這個屬性並不是這個函式的 prototype,它指的是所有透過這個 function constructor 所建立出來的物件的 prototype」。
翻成程式可能比較好說明,這句話的意思是說 Person.prototype 並不是 Person.__proto__,但是所有透過 Person 這個 function constructor 所建立的物件,在該物件實例的 __proto__ 中,會包含有Person.prototype 的內容。
也就是說,當我們使用 new 這個運算子來執行函式建構式時,它會先建立一個空物件,同時將該建構式中prototype 這個屬性的內容(Person.prototype),設置到該物件實例的 prototype 中(john.__proto__)。
因此,當我們在 Google Chrome 的 console 中輸入 john.__proto__ 時,我們就可以看到剛剛在Person.prototype 所建立的函式 getFullName 已經繼承在裡面了:

實際運用

由於 Person.prototype 中的方法已經被繼承到由 Person 這個 function constructor 所建立的物件實例 john 中,所以這時侯,我們就可以順利的使用 john.getFullName 這個方法:
function Person(firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
}

Person.prototype.getFullName = function(){
  return this.firstName + ' ' + this.lastName;
}

var john = new Person('John', 'Doe');
console.log(john);
console.log(john.getFullName());
如此,可以正確的執行 getFullName 這個函式並得到如下的結果:

透過函式建構式與 Prototype 的實用處

透過這樣的方法,我們可以讓所有根據這個函式建構式(function constructor)所建立的物件都包含有某些我們想要使用的方法。如果我們有 1000 個物件是根據這個函式建構式所建立,那麼我們只需要使用 .prototype 這樣的方法,就可以讓這 1000 個物件都可以使用到我們想要執行的某個 method。
有的人可能會好奇說,為什麼我們不要把 getFullName 這個方法直接寫在函式建構式當中呢?
❗️ 我們不該把方法放在 function constructor 中。
把方法放在函式建構式中這麼做雖然程式仍然可以正確執行並得到結果,但是這麼做會有個問題,如果我們是把這個方法直接寫在函式建構式中,那麼每一個物件都會包含有這個方法,如果我們有 1000 個物件根據這個函式建構式所建立,那麼這 1000 個物件都會包含這個方法在內,如此將會占據相當多的記憶體;但如果是建立在 prototype 中,我們只會有一個這樣的方法。
所以,為了效能上的考量,通常會把方法(method)放在建構式的 prototype 中,因為它們可以是通用的;把屬性(property)放在建構式當中,因為每一個物件可能都會有不同的屬性內容,如此將能有效減少記憶體的問題。

程式範例

function Person(firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
}

Person.prototype.getFullName = function () {
  return this.firstName + ' ' + this.lastName;
}

var john = new Person('John', 'Doe');
console.log(john);
console.log(john.getFullName());

Person.prototype.getFormalFullName = function () {
  return this.lastName + ',' + this.firstName;
}

var jane = new Person('Jane', 'Doe');
console.log(jane);
console.log(jane.getFormalFullName());

資料來源

[筆記] 談談 JavaScript 中的 function constructor 和關鍵字 new

img
在前幾篇的筆記中,我們已經對於物件(object)、原型(prototype)、繼承(inheritance)和原型鏈等等有更多的了解,現在讓我們來更深入的談論一下JavaScript中建立物件的方法。
先前我們有說明使用 object literal,也就是大括號的方式來建立物件,或者是使用 new Object( ) 的方式,但除此之外,我們還可以使用**函式建構式(function constructor)**的方式來建立物件。但在開始深入了解之前,讓我們先來簡單談一下 JavaScript 發展的歷史,如此我們將更清楚為什麼會使用這樣的方式。

new 和 JavaScript 的關係史

JavaScript 是由一個叫做 Brendon Eich 所開發的,就像現在有 Google VS. Apple,在當時也有很多不同的程式語言之間在競爭,而之所以這個語言會叫做 JavaScript 其中一個原因是當初宣傳這套語言時,想要吸引使用 JAVA 的程式開發者,這就很像微軟發展了 VBScript,之所以稱作 VBScript 也是因為想要吸引使用 Visual Basic 的程式開發者。
JavaScript 雖然聽起來或看起來和 JAVA 很像,但實際上可以說是非常的不同,然而,當初在推廣 JavaScript 這套語言的時候,為了讓它和 JAVA 有更多的相似性,因此用了和 JAVA 相似的語法 var XXX = new ooo( ) ,這樣的方式在 JAVA 中可以根據某個類別(class)來建立物件,但在 JavaScript 中並沒有真正的 class 這樣的東西,這麼做的目的只是為了讓 JAVA 的使用者在看到 JavaScript 的時候覺得有股熟悉感。
在了解這段小歷史之後讓我們進一步來看函式建構式(function constructor)new

談談函式建構式(function constructor)

我們先建立一個函式,名稱叫做 Person,要注意的是我們在第9行的地方是使用 new 這個關鍵字,後面放的則是 function Person:
function Person (){
  this.firstName = 'John';
  this.lastName = 'Doe';
}

var john = new Person();
console.log(john);
接著我們把 John 呼叫出來看,會得到如下的結果,透過 new 它會幫我們建立一個物件,然後裡面有Person 這個 function 裡面的內容,並且變成了屬性名稱和屬性值:
img

進一步來看 new 讓這個過程發生了什麼

在JavaScript中 new 這個關鍵字其實是眾多運算子(operators)的其中一種:
img
圖片來源:MDN Operator Precedence
當我們使用 new 這個關鍵字時,實際上會先有一個空的物件被建立。
接著 People 這個函式會被執行(invoke)。我們知道當函式執行的時候,在 execution context 中會有 this 被建立,而當我們使用 new 的時候,函式裡面的 this 會被指定成剛剛所建立的那個空物件
所以當執行 People 這個 function,執行到 this.firstNamethis.lastName 時,因為 this 現在指稱的是那個空物件,所以實際上是在幫這個空物件賦予屬性名稱和屬性值。
在這樣的過程中,只要這個函式建構式 People 沒有指定 return 為其他物件,它就會直接回傳給我們這個新建立的物件
接著讓我們透過程式碼來更清楚的了解這個執行的過程:

該函式有被執行

讓我們在原本的程式中加入第6行的內容:
function Person () {
  this.firstName = 'John';
  this.lastName = 'Doe';
  console.log('這個函式有被執行');
}

var john = new Person();
console.log(john);
這時候在 chrome 中呼叫出來的結果如下,說明了當我們使用 new 在建構物件的時候 People 這個 function 確實有被執行:
img

透過 new 會幫我們建立一個空的物件

現在我把我們原本的程式碼改成這樣:
function Person(){
  console.log(this);
}

var john = new Person();
//    console.log(john);
這時候程式碼回傳的結果如下,表示的確在執行這個程式的過程中幫我們建立了一個新的空物件
img

函式的最後若return其他物件,則原新物件內容會被覆蓋

現在,讓我們把原本的程式碼稍微做如下修改,增加第六行的內容:
function Person (){
  this.firstName = 'John';
  this.lastName = 'Doe';
  return  {"RETURN":"原本this的內容就不會被回傳"};
}

var john = new Person();
console.log(john);
回傳的結果如下,原本被建立的新物件不會被回傳,而是回傳我們最後return給它的內容:
img

function constructor 的實際應用

由上面的方法,我們可以透過 function 的方式來建立一個新的物件,如果我們想要建立出同屬性名稱但不同屬性值的物件內容,我們可以把物件的屬性值變成參數,如此就能透過此 function constructor 建立出許多不同的物件:
function Person (firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
}

var john = new Person('John', 'Doe');
console.log(john);
var jane = new Person('Jane', 'Doe');
console.log(jane);
如此,我們就可以透過同一個函式建構式建立出很多不同的物件:
img
此外,我們會把根據建構子(constructor)所建立出來的物件稱作是實例(instance)
透過建構子(constructor)所建立出來的物件,我們稱為實例(instance)

注意!如果我們忘了加上關鍵字 new

這裡有一個地方我們需要非常留意,如果你在程式撰寫的過程當中,忘記加上 new 這個關鍵字的話,例如:
var john = Person('John', 'Doe');
console.log(john);
如此,因為 JavaScript 不知道你是要執行這個程式,還是要根據這個 function 去建立object,因次最後回傳 undefined 的結果:
img

最後,讓我們總結一下函式建構式

  • 其實函式建構式(function constructor)就是普通的 function,只是我們可以透過這個 function 來建立物件。
  • 透過在 function 前面加上 new 這個運算子,它會把函式中 this 這個關鍵字建立成一個新的物件,然後如果你沒有在該函式的最後指定回傳出其它物件的話,它就會自動回傳這個新的物件給你。
那....如何透過 function constructors 來設定我們的原型(prototype)呢?讓我們在下一篇筆記來談吧!

程式範例

function Person(firstName, lastName){
  this.firstName = firstName;
  this.lastName = lastName;
}

var john = new Person('John', 'Doe');
console.log(john);

var jane = new Person('Jane', 'Doe');
console.log(jane);

延伸閱讀

資料來源

2016年6月20日

[筆記] JavaScript 中任何東西的 prototype 到最後都是物件

img
這篇筆記延伸上一篇所提到的原型(prototype)的概念,說明了在 JavaScript 當中,所有的東西(字串、數值、布林值、函式、陣列、物件)的 prototype 的最後都是物件!
什麼意思呢?
假設我們現在分別建立物件、函式和陣列:
//  Object
var a = {
  name:'pjchender'
}

//  function
var b = function(){
  console.log('Hello');
}

//  Array
var c = ['this','is','an','array'];

//  string
var d = 'This is string';

//  number
var e = 3;

//  boolean
var f = false;
接著,我們會在 Google Chrome 的 console 視窗來分別檢視這些變數的 prototype。

物件(object)

我們可以看到 a 這個物件本身的 prototype 也是一個物件,在下去就沒了(null),也就是 Object → Object → null
img
a.__proto__ 就是物件的原型,打開來看會發現裡面有許多內建的方法。
img
如果我在 console 中輸入 a.,則會出現對於物件而言內建的方法:
img
由於 a.__proto__ 仍然是一個物件,所以如果我輸入 a.__proto__.的話,會得到一樣對於物件可以使用的方法:
img

函式(function)

接著我們來看看函式,它會是 function → function → object → null,所以最後還是結束在 object。
img
如果我們想要看函式有哪些內建的方法可以用,我們可以輸入 b.b.__proto__. 就可以看到了,這裡我們就可以看到我們之前使用的 apply, bind, 和 call
img

陣列(Array)

再來陣列的話則是 array → array → object → null
img
如果我們要看陣列有哪些方法可以用,一樣可以透過 c.c.__proto__. 來檢視:
img
同樣的方法也可以用來檢視字串、數字和布林值,透過 chrome 來試試看吧!

資料來源

[筆記] 了解JavaScript中原型(prototype)、原型鍊(prototype chain)和繼承(inheritance)的概念

img
這篇筆記主要說明了 JavaScript 中非常重要的概念,也就是繼承(inheritance)、原型(prototype)和原型鍊(prototype chain)。

談繼承(inheritance)

讓我們先來了解一下繼承的意思,繼承的意思其實不用想得太複雜,簡單來說就是指一個物件可以提取到其他物件中的屬性(property)或方法(method)
繼承可以分成兩種,一種是 classical inheritance,這種方式用在 C#JAVA 當中;另一種則是 JavaScript 所使用的,是屬於 prototypal inheritance

談原型鍊(prototype chain)

由於 JavaScript 使用的是 prototypal inheritance,所以必然會包含原型(prototype)的概念,讓我們看一下這張圖:
img
一個物件裡面除了所給予的屬性值外,另外也包含原型 prototype。
obj.prop1:假設我們現在有一個物件,就稱作 obj ,而這個物件包含一個屬性(property),我們稱作 prop1,現在我們可以使用 obj.prop1 來讀取這個屬性的值,就可以直接讀取到 prop1的屬性值了。
obj.prop2:從之前的筆記 ([筆記] 了解 function borrowing 和 function currying ─ bind(), call(), apply() 的應用)中,我們可以知道,JavaScript 中會有一些預設的屬性和方法,所有的物件和函式都包含 prototype 這個屬性,假設我們把 prototype 叫做 proto,這時候如果我們使用 obj.prop2 的時候,JavaScript 引擎會先在 obj 這個物件的屬性裡去尋找有沒有叫作 prop2 的屬性,如果它找不到,這時候它就會再進一步往該物件的 proto 裡面去尋找。所以,雖然我們輸入 obj.prop2 的時候會得到回傳值,但實際上這不是 obj 裡面直接的屬性名稱,而是在 obj 的 proto 裡面找到的屬性名稱( 即,obj.proto.prop2,但我們不需要這樣打)。
obj.prop3:同樣地,每一個物件裡面都包含一個 prototype,包括物件 proto 本身也不例外,所以,如果輸入 obj.prop3 時,JavaScript 會先在 obj 這個物件裡去尋找有沒有 prop3 這個屬性名稱,找不到時會再往 objproto 去尋找,如果還是找不到時,就再往 proto 這個物件裡面的 proto 找下去,最後找到後回傳屬性值給我們(obj.proto.proto.prop3)。
雖然乍看之下,prop3 很像是在物件 obj 裡面的屬性,但實在上它是在 obj → prop → prop 的物件裡面,而這樣從物件本身往 proto 尋找下去的鍊我們就稱作「原型鍊(prototype chain)」。這樣一直往下找會找到什麼時候呢?它會直到某個對象的原型為 null 為止(也就是不再有原型指向)

讓我們來看個例子幫助了解

讓我們實際來看個例子幫助我們了解 prototype chain 這個概念,但是**要注意!要注意!要注意!**這個例子只是為了用來說明 prototype chain 的概念,實際上撰寫程式時萬萬不可使用這樣的方式!
首先,我們先建立一個物件 person 和一個物件 john:
var person = {
  firstName: 'Default',
  lastName: 'Default',
  getFullName: function() {
    return this.firstName + ' ' + this.lastName;
  },
};

var john = {
  firstName: 'John',
  lastName: 'Doe',
};
再次提醒,下面的示範只是為了說明原型鍊,在平常的情況下絕對不要這樣做,這樣做會拖慢整個瀏覽器的效能。
接著,我們知道所有的物件裡面都會包含原型(prototype)這個物件,在 JavaScript 中這個物件的名稱為 __proto__。如同上述原型鍊(prototype chain)的概念,如果在原本的物件中找不到指定的屬性名稱或方法時,就會進一步到 __proto__ 這裡面來找。
為了示範,我們來對 __proto__ 做一些事:
//千萬不要照著下面這樣做,這麼做只是為了示範
john.__proto__ = person;
如此,john 這個物件就繼承了 person 物件。在這種情況下,如果我們想要呼叫某個屬性或方法,但在原本 john 這個物件中找不到這個屬性名稱或方法時,JavaScript 引擎就會到 __proto__ 裡面去找,所以當接著執行如下的程式碼時,並不會噴錯:
console.log(john.getFullName())        //    John Doe;
我們可以得到 "John Doe" 的結果。原本在 john 的這個物件中,是沒有 getFullName() 這個方法的,但由於我讓 __proto__ 裡面繼承了 person 這個物件,所以當 JavaScript 引擎在 john 物件裡面找不到getFullName() 這個方法時,它便會到 __proto__ 裡面去找,最後它找到了,於是它回傳 "John Doe"的結果。
如果我是執行:
console.log(john.firstName);        //  John
我們會得到的是 John 而不是 'Default',因為 JavaScript 引擎在尋找 john.firstName 這個屬性時,在john 這個物件裡就可以找到了,因此它不會在往 __proto__ 裡面找。這也就是剛剛在上面所的原型鍊(prototype chain)的概念, 一旦它在上層的部分找到該屬性或方法時,就不會在往下層的prototype去尋找
在了解了prototype chain這樣的概念後,讓我們接著看下面這段程式碼:
var jane ={
  firstName: 'Jane'
}

jane.__proto__ = person;
console.log(jane.getFullName());
現在,你可以理解到會輸出什麼結果嗎?
答案是 "Jane Default" 。
因為在 jane 這個物件裡只有 firstName 這個屬性,所以當 JavaScript 引擎要尋找 getFullName() 這個方法和 lastName 這個屬性時,它都會去找 __proto__ 裡面,而這裡面找到的就是一開始建立的person 這個物件的內容。

程式範例

var person = {
  firstName:'Default',
  lastName:'Default',
  getFullName: function(){
    return this.firstName+ ' ' + this.lastName;
  }
}


var john = {
  firstName:'John',
  lastName:'Doe'
}

//千萬不要照著下面這樣做,這麼做只是為了示範
john.__proto__ = person;
console.log(john.getFullName());    //  John Doe
console.log(john.firstName);        //  John

var jane ={
  firstName: 'Jane'
}

jane.__proto__ = person;
console.log(jane.getFullName());

延伸閱讀

資料來源