是 Ray 不是 Array

Welcome.Web.World
歡迎來到網路世界

前言

我們對於函數建構子已經有一個基本瞭解了,所以這邊就可以來討論一下內建的函數建構子。

內建的函數建構子

這邊將會開始講一下已經存在於 JavaScript 的內建函數建構子,所以我們可以看看內建的函數建構子有什麼,或許我們已經用了許多次,但不一定瞭解它。

來試著撰寫程式碼瞭解吧。

1
var a = new Number(3);

首先這是一個函數,一個函數建構子,用了首字母大寫的傳統。

圖片

我們可以看到輸出的是一個物件,為什麼?函數建構子會建立什麼?物件。

所以我們可以試試看我們所瞭解的內建建構子。

1
2
3
var a = new Number(3);
var b = new String('Hellow');
var c = new Object({firstname: 'jack'});

圖片

那函數建構子本身有原型,所以我們可以查看原型,而它內建一些方法。

1
2
var a = new Number(3);
Number.prototype

圖片

所以我們可以這樣使用

1
2
var a = new Number(3);
a.toFixed(2)

圖片

另外小提一下 b,並不是字串,也不是純值。

1
var b = new String('Hellow');

圖片

它是一個物件,所以我們才能夠這樣子使用 indexOf()

1
2
var b = new String('Hellow');
b.indexOf('e');

圖片

所以這些內建的函數建構子看起來很像是幫我們建議字串,但其實是物件,且包含了字串。

那其實下面這個做法

1
"Hello".length;

等於這個樣子

1
new String("Hello");

所以說下次若我們看到 new,例如 new Date

1
var a = new Date('2019/05/23');

圖片

我們其實是取得了一個物件,然後使用了 JavaScript 內建提供的方法。

圖片

而這些方法實際是存在於 Date

圖片

那如果今天我們要自己加入另一個原型呢?

1
2
3
4
5
String.prototype.isLengthGreaterThan = function (limit) {
return this.length > limit;
}

console.log('jack'.isLengthGreaterThan(3));

圖片

那這時候所有字串就都可以使用 isLengthGreaterThan 這個方法,而這就是原型繼承的強大,許多框架都會這樣做,但是撰寫時要注意不要複寫到原本提供的建構子。

字串可以這樣做,那數字呢?

1
2
3
4
5
6
7
8
9
10
String.prototype.isLengthGreaterThan = function (limit) {
return this.length > limit;
}

console.log('jack'.isLengthGreaterThan(3));

Number.prototype.isPositive = function () {
return this.length > 0;
}
console.log(10.isPositive());

圖片

得到了一個錯誤,為什麼?

雖然 JavaScript 會將字串轉換為物件,但數字並不會。

但是可以這樣寫

1
2
3
4
5
Number.prototype.isPositive = function () {
return this.length > 0;
}
var a = new Number(3);
console.log(a.isPositive());

圖片

所以我們可以替這些原型增加功能,進而方便我們。

圖源

JavaScript 全攻略:克服 JS 奇怪的部分

前言

這章節是關於「new」與函數的危險小叮嚀,在前面我們有大致上講過,當加上 new 時,會建立空的物件,然後 this 會指向空的物件,如果你不回傳任何東西的話,它會回傳新物件,但這邊就是函數危險的地方。

「new」與函數

如果今天我們忘記加上 new 會發生什麼事情呢?

1
2
3
4
5
6
7
8
9
function Person(first, last) {
console.log(this);
this.firstname = first;
this.lastname = last;
}

var john = Person('John', 'Doe');

console.log(john);

圖片

第一個我們可以發現 this 指向了 window,然後 JavaScript 並不知道我們要對這個函數執行,對他來講只是一個函數,也因為這樣子所以他只會回傳 undefined。

那這樣會有甚麼問題?當我們取用 proto 就會出現錯誤。

1
2
3
4
5
6
7
8
9
10
11
12
function Person(first, last) {
console.log(this);
this.firstname = first;
this.lastname = last;
}
Person.prototype.getFullName = function() {
return this.firstname + ' ' + this.lastname;
}

var john = Person('John', 'Doe');

console.log(john.getFullName());

圖片

這個錯誤是因為 john 是一個 undefined,所以無法取得方法。

通常這都是大家認為的函數建構子缺點,但如果我們遵照一些規範來設計的話,就可以減少一些問題,例如任何我們要作為函數建構子的函數首字
母寫成大寫,第一個字母大寫,這樣子會比較方便於 debug,而這是一個傳統方式。

任何我們要作為函數建構子的函數,永遠都要第一個字母大寫作為建構子名稱。

圖源

JavaScript 全攻略:克服 JS 奇怪的部分

前言

我們已經知道函數建構子可以幫助新物件設定屬性及方法,但原型呢?所以接下來就要來做講檢該如何設定原型。

prototype

這是上一章節的範例

1
2
3
4
5
6
7
8
function Person(first, last) {
this.firstname = first;
this.lastname = last;
}

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

console.log(john);

當我們使用函數建構子的時候,就已經被 JavaScript 給準備好了,所以我們可以試著輸入 john.__proto__

圖片

我們會得到一個物件,不信?我收合給你看。

圖片

那這個物件是什麼?我們先前有講過函數就是物件,在 JavaScript 函數就是物件,然後所有物件都有原型屬性 (prototype property),但是一般來講我們並不會用到,他只是掛在那裏而已。

圖片

除非我們將函數作為函數建構子來使用。

圖片

讓我們來試試看。

1
2
3
4
5
6
7
8
9
10
11
12
function Person(first, last) {
this.firstname = first;
this.lastname = last;
}

Person.prototype.getFullName = function() {
return this.firstname + ' ' + this.lastname;
}

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

console.log(john);

圖片

我們可以看到我們可以原型下有了 getFullName(),換一個角度來看。

圖片

所以我們可以知道,當然函數被當作函數建構子使用時,就會有原型,那為什麼我們要將 getFullName 放進原型內?我們也可以直接放在函數建構子內?

1
2
3
4
5
6
7
8
9
10
11
function Person(first, last) {
this.firstname = first;
this.lastname = last;
this.getFullName = function() {
return this.firstname + ' ' + this.lastname;
}
}

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

console.log(john);

圖片

假設今天有一千種物件都放在 getFullName 內,就會佔據比較多的記憶體,但若放在原型上就會比較好。

以效能來講將重複的屬性與方法放在原型上會比較好,因為這樣做我們就不用每一次都在 Person 裡面加入物件,只需要將重複需要使用的放在原型,然後透過原型去取得使用就好了。

所以就可以這樣寫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Person(first, last) {
this.firstname = first;
this.lastname = last;
}

Person.prototype.getFullName = function() {
return this.firstname + ' ' + this.lastname;
}

Person.prototype.getFormaFullName = function() {
return this.firstname + ', ' + this.lastname;
}

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

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

圖片

這樣兩者皆可以使用。

圖源

JavaScript 全攻略:克服 JS 奇怪的部分

前言

前面我們已經瞭解許多物件、原型繼承、原型鏈和物件屬性及方法,接下將更深入討論建立物件。

「new」

前面我們已經有講過幾種建立物件的方式,其中一個是物件實體語法。

1
2
3
4
var jack = {
firstname: 'jack',
lastname: 'Dec',
}

而其實還有別的物件建立方式。

1
var jack = new Person();

圖片

那為什麼會有 new?,這主要與 JavaScript 的誕生有關係,一個語言若都沒有人使用就代表著死亡,所以當初 JavaScript 會叫 JavaScript 就是希望可以吸引 Java 開發者,而 Java 開發者習慣使用 new 的關鍵字來定義物件等等,所以當 Java 開發者看到這樣寫會感覺比較親切,但是使用這種方式來建立物件有些許問題,但是我們還是需要瞭解,因為許多專案及原始碼都會有它的存在。

下面將用課堂的範例來做一點好玩的事情。

1
2
3
4
5
6
7
8
function Person() {
this.fistname = 'John';
this.lastname = 'Doe';
}

var john = new Person();

console.log(john);

圖片

我們發現 john 變成一個物件了,但是這邊我們要記得一件事情,我們只是在利用一些方法在 JavaScript 中建立物件,那為了建立物件我們必須給它屬性和方法,然後建立原型,而這是為了讓我們看更清楚而做的,所以接下來將使用比較正確的方式來增加屬性和方法、設定原型。

new 其實是一個運算子,為什麼這樣講呢?讓我們翻一下先前的文件>MDN 的文件 (在下方一點地方)

圖片

上面的例子中 new 會改變 this 指向的東西,this 指向到一個空的物件,而 new 建立了一個新物件,然後呼叫了 Person(),所以現在 this 變數指向 new 創造的新的空物件的記憶體位置。

所以當我們打 .firstname.lastname 就會增加到空物件上,就像這樣。

1
2
3
4
5
6
var john = {};

john.fistname = 'John';
john.lastname = 'Doe';

console.log(john);

而在 JavaScript 中會回傳 new 運算子建立的物件,這會建立一個新物件,而成為物件的一部分也會回傳東西,所以我們試著這樣嘗試看看。

1
2
3
4
5
6
7
8
9
function Person() {
this.fistname = 'John';
this.lastname = 'Doe';
console.log('我回傳囉');
}

var john = new Person();

console.log(john);

圖片

由此可知,new 運算子會對這個函數呼叫,然後在回傳裡面的東西變成物件。

那如果 this 我們什麼都不對他做的話,他將會回傳本身。

1
2
3
4
5
6
7
8
9
10
function Person() {
console.log(this);
this.fistname = 'John';
this.lastname = 'Doe';
console.log('我回傳囉');
}

var john = new Person();

console.log(john);

圖片

但是如果我們本身若對函數回傳的話,就會變成只回傳那個資料。

1
2
3
4
5
6
7
8
9
10
11
function Person() {
console.log(this);
this.fistname = 'John';
this.lastname = 'Doe';
console.log('我回傳囉');
return { greet: '我是回傳的物件' };
}

var john = new Person();

console.log(john);

圖片

如果沒有回傳時,JavaScript 就會改找我們設定 this 的變數。

所以我們可以從這邊知道,我們可以透過函數來建構物件,而這叫做函數建構子。

那我們可以依照 函數建構子 來做一點進階的作法,假設今天 firstnamelastname 是改由我們自己傳入呢?

1
2
3
4
5
6
7
8
function Person(first, last) {
this.firstname = first;
this.lastname = last;
}

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

console.log(john);

圖片

所以我們可以利用函數參數的特性來傳入資料進而改變 firstnamelastname

圖片

簡單來講 new 運算子可以做出新物件,函數建構子是被用來增加新物件的屬性和方法

圖源

JavaScript 全攻略:克服 JS 奇怪的部分

前言

接下來將要講解另一個建立物件的函數很有趣很有效的東西,許多資料庫都會使用到,而這稱為 extend。

而通常我們都是用一個叫 Reflection 的東西來做到 extend。

Reflection

圖片

Reflection: 一個物件可以看到自己的東西,然後改變自己的屬性和方法。

讓我們試著從範例來學習。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var person = {
firstname: 'Default',
lastname: 'Default',
getFullName: function() {
return this.firstname + ' ' + this.lastname;
}
}

var john = {
firstname: 'John',
lastname: 'Doe',
}
// 千萬不要使用這種方式在真正的專案開發上,這只是為了理解原型而已。
john.__proto__ = person;

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

圖片

這時候會看到一個很神奇的東西,getFullName(),這是因為 for in 會到外面取用屬性和方法的 不只在物件本身,還會到原型上找

那如果今天我們要

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var person = {
firstname: 'Default',
lastname: 'Default',
getFullName: function() {
return this.firstname + ' ' + this.lastname;
}
}

var john = {
firstname: 'John',
lastname: 'Doe',
}
// 千萬不要使用這種方式在真正的專案開發上,這只是為了理解原型而已。
john.__proto__ = person;

for(var prop in john) {
if(john.hasOwnProperty(prop)) { // hasOwnProperty 是原型物件的東西,我們可以透過這種方式來確定是否物件有這個屬性。
console.log(prop + ':' + john[prop]);
}
}

圖片

這樣 getFullName() 就沒有出現了,因為 john 的物件屬性並沒有 getFullName()

而這個概念可以用來幫助我們做一些事情【補足原型繼承】,所以這邊試試看引入 underscore.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var newscript = document.createElement('script');
newscript.setAttribute('type','text/javascript');
newscript.setAttribute('src','https://underscorejs.org/underscore-min.js');
var john = {
firstname: 'John',
lastname: 'Doe',
}

var jane = {
address: '111 Main St.',
getFormFullName: function() {
return this.lastname + ', ' + this.firstname;
}
}

var jim = {
getFirstName: function() {
return firstname;
}
}

_.extend(john, jane, jim); // 這樣做將會把物件結合起來
console.log(john);

圖片

這時候我們可以看到 johnjane 的物件地址、jim 物件的 getFirstName 函數,還有 jane 物件的 getFormalFullName()

那這是怎麼做到的?首先先開啟 underscore.js,開啟之前記得千萬不要害怕開源的專案,這是一個非常好學習教材,我們試著找 extend 看看。

圖片

首先我們可以看到他建立了屬性或方法,而這個叫 createAssigner 的函數我們試著找看看。

圖片

我們可以發現 createAssigner 是一個函數,其中他接受了 keys 還有 defaults,然後回傳函數本身,我們可以看到這是一個閉包。

首先它會先取得傳入參數的長度

圖片

再來是包裝物件,他將物件包裝起來(避免參考問題) (((我猜)))

圖片

接下來就是當物件長度若小於兩筆,或是物件為空,就返回物件

圖片

接下來他會用 for 將所有物件跑一遍,因為陣列第一筆是 john ,所以起始會直接跳過 01 開始

圖片

所以因為經過 underscore.js 的處理, jim 就可以使用 getFormFullName()了。

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
var newscript = document.createElement('script');
newscript.setAttribute('type','text/javascript');
newscript.setAttribute('src','https://underscorejs.org/underscore-min.js');
var john = {
firstname: 'John',
lastname: 'Doe',
}

var jane = {
address: '111 Main St.',
getFormFullName: function() {
return this.lastname + ', ' + this.firstname;
}
}

var jim = {
getFirstName: function() {
return firstname;
}
}

_.extend(john, jane, jim); // 這樣做將會把物件結合起來
console.log(john);

john.getFormFullName();

圖片

圖源

JavaScript 全攻略:克服 JS 奇怪的部分

前言

現在我們知道物件原型,接著我們可以深入瞭解到一件事情 JavaScript 所有東西都是物件或是純值

數值、布林、字串、函數、陣列、物件他們都有原型,除了基本物件(base object)。

所有東西都是物件(或純值)

讓我們從範例來瞭解,接下將會利用這三個東西 物件、函數、陣列來講解為什麼所有東西都有原型。

1
2
3
var a = { };
var b = function() { };
var c = [];

首先讓我們試著在瀏覽器輸入以上範例,然後再輸入 a.__proto__

Image

我們會得到一個基本物件,這在原型鏈上非常底層,而這基本物件有屬性與方法。

Image

那函數呢?我們試著輸入 b.__proto__

Image

這就是所有函數的原型,所有我們建立的函數都有這個原型,當然也有相關的屬性與方法(你會看到熟悉的 apply、call、bind)。

Image

接下來是陣列 c.__proto__

Image

這就是一個原型陣列,我們也來看看它是否也有屬性與方法(這裡你也會看到許多熟悉的字眼)。

Image

所以由上面這三個範例我們可以知道一件事情。

JavaScript 所有物件、所有陣列及所有函數都有原型。

那這邊在講一個好玩的問題,原型的原型是什麼?

Image

是原型,所以我們要記得,原型鏈最底下的東西就是原型物件。

圖源

JavaScript 全攻略:克服 JS 奇怪的部分

前言

JavaScript 用了原型繼承,所以代表有個叫作原型(prototype)的概念。

瞭解原型

首先我們知道物件可以有屬性和方法,然後我們可以使用點運算子取得屬性或方法,JavaScript 中所有物件、函數,都有原型屬性,而這個屬性會參考到另一個屬性通常被稱為 proto。

假設今天我們有一個 obj 的物件,底下有一個叫 prop1,所以我們可以透過 obj.prop1 來取得。

圖片

JavaScript 中所有物件、函數,都有原型屬性。

圖片

而這個屬性會參考到另一個物件,稱之為 proto,而 proto 也可以有屬性,例如 prop2,所以當我們要取用 prop2時就可以這樣寫 obj.prop2

圖片

而我們使用點運算子去取用 obj 中的 prop2 時,會找不到,所以他會往原型裡面找。

圖片

那原型物件也可以指向到另一個物件。

圖片

每個物件都可以有自己的原型,當這個 proto 裡面有一個 prop3,我們就可以用 obj.prop3 取得。

圖片

而這過程就像一個鏈子,所以又稱為原型鏈 (prototype chain),但是不要把他跟範圍鏈搞混了,雖然很相似,可是範圍鏈是尋找可以取用的變數,但原型鏈是尋找屬性及方法。

圖片

而一般來講這 proto 是隱藏起來的,所以我們才不用這樣撰寫 obj.proto.proto.prop3,只需要 obj.prop3 就好。

但是 JavaScript 中有一個很有趣的狀況,當若有第二個 obj 時,他可以指向同一個原型。

圖片

所以當我們呼叫 obj2.prop2,他一樣會回傳相同位子。

圖片

而以上這些就是原型及原型鏈個概念,只需要想簡單一點只是有一個特別的參考到我們的物件而已,那接下來我們直接來看點範例。

1
2
3
4
5
6
7
8
9
10
11
12
var person = {
firstname: 'Default',
lastname: 'Default',
getFullName: function() {
return this.firstname + ' ' + this.lastname;
}
}

var john = {
firstname: 'John',
lastname: 'Doe',
}

這邊我們有兩個物件,接下來我們要將 john 設定成原型,但以下範例千萬不要使用於現實中,這只是為了簡單理解觀念而已。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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());

圖片

為什麼不是抓到 Default?因為原型鏈的原因導致,所以點運算子會在 john 裡面找到 firstname,所以就會停下來不會再找了,接下來在加一點物件上去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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());

var jane = {
firstname: 'jane',
}

// 千萬不要使用這種方式在真正的專案開發上,這只是為了理解原型而已。
jane.__proto__ = person;
console.log(jane.lastname);
console.log(jane.getFullName());

圖片

相信看到這邊已經越來越清楚怎麼跑了。

圖源

JavaScript 全攻略:克服 JS 奇怪的部分

古典和原型繼承

圖片

繼承表示一個物件取用另一個物件屬性或方法,只要了解其簡單觀念就好了,許多人在解釋這區塊時常會用各種火車、汽車等等例子來做舉例,但講師認為直接講清楚會比較簡顯易懂。

那古典繼承和原型繼承是什麼呢?古典繼承在 C#、Java 裡都有,而且非常熱門。

而古典繼承裡面有非常多方法可以用

  • friend
  • protected
  • private
  • interface

但我們必須了解他才能夠知道該如何操作。

原型繼承呢?東西就簡單許多了

  • 彈性 (flexible)
  • 可擴充性 (extensible)
  • 簡單易懂 (east to understand)

古典和原型繼承各自都有他的好壞,所以並沒有一定,所以當有人在講繼承時,就是在講

一個物件取用另一個物件屬性或方法

圖源

JavaScript 全攻略:克服 JS 奇怪的部分

物件與函數的章節結尾

圖片

以往我對於 IFFE 及閉包的觀念其實並不是很清楚,儘管先前已經寫過一些文章有類似,但是總覺得哪邊好像怪怪的?

而且其實我也沒想過在 JavaScript 函數就是物件這個想法,經過深入了解才知道這個問題,更不用說自動插入分號了。

最後的兩堂課其實我覺得非常不錯,但是說真的必須要多花點時間去練習就是了…

好哩,那就繼續往下深入吧。

圖源

JavaScript 全攻略:克服 JS 奇怪的部分

前言

前面我們已經了解到 函數程式設計 的強大,所以這堂課將會花點時間來講一些資源例子。

underscore.js

underscore.js 是一個滿有名的 JavaScript 資源庫,可以幫助我們處理陣列及物件。

講師也建議我們可以從這些開源資料來了解進而學習,但要挑戰之前我們一定要有觀念,所以這堂課也希望我們可以藉由開源的資料來幫助我們學習了解函數程式設計。

Lodash

Lodash與 underscore 類似,但執行速度比 underscore 快。

但作者建議我們去 underscore 看一下學習了解 underscore 如何撰寫,這可以協助我們更加深了解函數程式設計。

最後我附上我找的幾個資源

underscore.js (1.7.0) 中文版

Underscore.js (1.9.1) 中文版

UnderscoreJS精巧而强大工具包

圖源

JavaScript 全攻略:克服 JS 奇怪的部分

0%