在前一篇的最後,有示範了一個簡單的錢包程式碼
function createWallet(initMoney){
var money = initMoney
return {
add: function(num){
money += num
},
deduct: function(num){
if(num >= 10){
money -= 10
} else {
money -= num
}
},
getMoney(){
return money
}
}
}
var myWallet = createWallet(99)
myWallet.add(1)
myWallet.deduct(100)
console.log(myWallet.getMoney())
在這段程式碼中利用 var myWallet = createWallet(99)
,然後再 call myWallet 裡的 add 這個 method,他不像我們一般直接 call 一個 function,myWallet 在這邊就像一個物件一樣,這就是物件導向。
ES6 中的物件導向
JavaScript 在 ES6 之後有了 class 這個語法
class 名稱一定是大寫開頭,例如 class Dog{ }
,可以在裡面寫很多功能
看以下程式碼應該就可以知道物件導向最基本的樣子了
class Dog{
setName(name) {
this.name = name
}
getName(name) {
return this.name
}
sayHello() {
console.log(this.name + ': hello')
}
}
var yoyo = new Dog()
yoyo.setName('yoyo')
console.log(yoyo.getName())
yoyo.sayHello()
var bigMac = new Dog()
bigMac.setName('bigMac')
bigMac.sayHello()
setName 這種功能的我們通常稱為 setter
getName 這種功能的我們通常稱為 getter
// this 在這邊指的是要動作的對象
例如 yoyo.setName('yoyo')
,表示要對 yoyo 去做 setName 的動作,所以 this 指的是 yoyo。
-
那當我們在建立一個新狗的時候,var yoyo = new Dog()
這邊看起來很像是在呼叫一個 function 的樣子
那如果在這裡傳進參數的話會傳到哪呢?
答案是會到 constructor(建構子)裡
class Dog{
constructor(name) {
this.name = name
}
}
當你在 call Dog 的時候,其實就是在 call 這個 function
此時 constructor 就會被建立,因此 constructor 很適合作為初始化之用,這裡也可以對他做操作,如設定名字等等。
ES5 以前的物件導向
ES6 可以利用 class 的方式實現物件導向,那在 ES5 的時候要怎麼做呢?
你可能會想到用之前我們在講閉包時候的方法
function Dog(name){
var myName = name
return {
getName: function(){
return myName
},
sayHello: function(){
console.log(myName)
}
}
}
var yoyo = Dog('yoyo')
yoyo.sayHello()
沒錯的確可以這樣做,但這樣做會有一個問題
你每創造一隻新狗的時候,他就會 return 你一個 getName & sayHello 的 function
所以如果有一萬隻狗,就會回傳一萬個
這樣是很浪費記憶體空間的。
-
所以在 ES5 做物件導向時,其實是用以下的程式碼去實作
function Dog(name) {
this.name = name
}
Dog.prototype.getName = function() {
return this.name
}
Dog.prototype.sayHello = function() {
console.log(this.name)
}
var yoyo = new Dog('yoyo')
yoyo.sayHello()
跟 ES6 很像,這個 function Dog 可以把它看成 ES6 class 裡的 constructor,那我要怎麼知道 Dog 這個函式是一般的函式還是當作constructor 用。
⇒ 只要呼叫時有加 new,就是當 constructor 用,反之則是一般 function
var yoyo = new Dog('yoyo')
-> 物件導向
var yoyo = Dog('yoyo')
-> 一般呼叫 function
⇒ 在新增 method 時,要用 prototype
以上這種方法就不會每次創一個新的狗時,回傳新的 function 佔記憶體空間了。
原型鍊(prototype)
那你可能會很好奇,yoyo.sayHello() 是怎麼 call 到 Dog.prototype.sayHello
的,他們之間應該有某種連結才對
的確,在 JavaScript 中有個隱藏的屬性叫做 __proto__
我們將他 log 出來 console.log(yoyo.__proto__)
,他會印出 Dog { getName: [Function], sayHello: [Function] }
,跟上面的 method 是吻合的
所以我們可以知道 yoyo.__proto__ === Dog.prototype
這是 new 幫你設定好的(稍後會更詳細說明)
當你在 call yoyo.sayHello() 的時候,會按照以下步驟去找:
- yoyo 本身有沒有 sayHello
yoyo.__proto__
有沒有 sayHello(我們剛知道yoyo.__proto__
等於 Dog.prototype,所以會去這找)yoyo.__proto__.__proto__
有沒有 sayHello(其實就等於 Object.prototype)yoyo.__proto__.__proto__.__proto__
這個 log 出來就會是 null,也就是已經找到頂了
以上經由 __proto__
構成的 chain,就叫做 prototype chain 原型鍊。
-
知道了這個就可以做很多有趣的事
String.prototype.first = function() {
return this[0]
}
var a = '123'
console.log(a.first())
答案就會是 1,也就是會回傳他的第一個字
之前說過的 Object.prototype.toString.call()
,就是利用這個原理
而 ES6 的 class 底層也是用 prototype 去實作的。
new 做了什麼
OK,了解原型鍊之後,要來說說 new 做了什麼
但說這個之前要先講一個重要的東西
當我們有一個 function 時,我們用 .call
的方式去 call 這個 function,傳進去的參數就會是 this 的值
function test(){
console.log(this)
}
test.call('123')
這邊只要先記得,用 .call
的時候,this 就是我傳進去的值。
-
繼續來講 new,先附上剛剛 ES5 的寫法
function Dog(name) {
this.name = name
}
Dog.prototype.getName = function() {
return this.name
}
Dog.prototype.sayHello = function() {
console.log(this.name)
}
var yoyo = new Dog('yoyo')
yoyo.sayHello()
現在我們想做一個新的狗 bobo
var bobo = newDog('bobo')
bobo.sayHello()
並且自己實作 new 的功能,讓 bobo.sayHello()
可以 work
-
第一步我們要先讓 newDog('bobo’) 被 call 的時候,能夠將資訊放入 constructor
function Dog(name){
this.name = name
}
做法就是用我們剛剛講的 .call
並且傳入參數的方式
function newDog(name) {
var obj = {}
Dog.call(obj, name)
}
因為 var bobo = newDog('bobo’)
,所以這裡的 name 是 bobo
Dog.call 傳入的第一個參數會是 this 的值,第二個參數會是 function Dog 的參數
所以帶入後會是這樣
function Dog(‘bobo’){
{}.name = ‘bobo’
}
也就是會等於 {name: ‘bobo’}
-
第二步要設定 proto,讓他連到 Dog.prototype
最後再讓他 return 回來就好了
function newDog(name){
var obj = {}
Dog.call(obj, name)
obj.__proto__ = Dog.prototype
return obj
}
上面這樣寫其實就跟 var bobo = new Dog('bobo’)
一樣
以上就是 new 所做的事。
繼承(Inheritance)
在物件導向的最後要講一個十分重要的概念:繼承
我們現在已經有狗的各種 method,假如我們現在有一種狗叫黑狗
他有狗的特性,但還有一些黑狗特殊的 method,我們就可以用繼承來實作
class Dog{
constructor(name) {
this.name = name
}
sayHello(){
console.log(this.name + ': hello')
}
}
class BlackDog extends Dog{
test(){
console.log('test', this.name)
}
}
const d = new BlackDog('fofo')
d.test()
因為在 BlackDog 這裡我們沒有 constructor,所以 fofo 傳入後會再往 parents 那邊找,this.name
就是 fofo。
因為繼承的關係,這隻黑狗 d 兩邊 function 的 method 都可以使用。
-
那如果我現在想在黑狗初始化時,就順便 sayHello
應該只要直接在 BlackDog 這裡加一個 constructor 就好了吧!
constructor(name){
this.sayHello()
}
不行哦,這邊需要先將傳進來的資訊先往上傳到 Dog 的 constructor 初始化,才能使用 this。
所以必須寫成
constructor(name) {
super(name)
this.sayHello()
}
用 super 將帶入的資訊再往上傳去初始化。
有關繼承可以參考這篇:Javascript继承机制的设计思想