在 [進階 js 09] Closure & Scope Chain最後有一個改 money 的程式碼,這跟物件導向的概念很類似
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(10)
console.log(myWallet.getMoney())
在這段程式碼 call myWallet.add(1)
,就好像 myWallet
是一個物件,他不像我們一般直接 call 一個 function,而是對 myWallet
這個物件做一些操作,感覺更模組化。
ES6 談物件導向
ES6 以後有 class 這個語法可以用
class 名稱一定是大寫開頭,這點跟 Java class 命名一樣
實際運用一定要用 new
去 instantiated (實例、實體化)
可以使用 getName()
跟 setName()
去 get 和 set 裡面資料
以下就是物件導向最基本的樣子
class Dog{
setName(name) {
this.name = name
}
getName(name) {
return this.name
}
sayHello() {
console.log(this.name)
}
}
var d = new Dog()
d.setName('abc')
d.sayHello()
通常不建議用 d.name = 123
去做存取,還是建議用 getName()
跟 setName()
因為你可能會這樣寫
getName() {
return this.firstName + this.lastName
}
你不會想另外寫東西去操作這些 return this.firstName + this.lastName
東西
constructor(建構子)
以上例子 new Dog()
這邊蠻像 function code
所以是可以傳參數進去 new Dog('abc')
那要怎麼接收呢?
constructor(參數) { }
class Dog{
constructor(name) {
this.name = name
}
getName(name) {
return this.name
}
sayHello() {
console.log(this.name)
}
}
var d = new Dog('abc')
d.sayHello()
var a = new Dog('def')
a.sayHello()
便可以根據這個設計圖產生非常多隻不同名字的狗狗
ES5 沒有 class 的替代方法
function Dog(name){
var myName = name
return {
getName: function(){
return myName
},
sayHello: function(){
console.log(myName)
}
}
}
var d = Dog('abc')
var a = Dog('def')
差別只在於不用 new
但這邊會有個小地方是個問題
如果去比較 a.sayHello === d.sayHello
答案會是 false
他們兩個是不同的 function
但他們只要共用同一個 function,都是要做一樣的事情
不然如果你要有一千隻狗,就會一千個 getName 這個 function
這樣是很浪費記憶體空間的
ES5 類似機制
function Dog(name) {
this.name = name
}
var d = new Dog('abc')
console.log(d)
在這邊可以把一個 function 當作 constructor 用
那這樣要怎麼知道這個 function 是 constructor 還是普通 function
只有加
new
的時候
才會把這個 function 當作 constructor
上述例子如果沒有加 new,那log 出來就是 undefined
設定屬性的問題解決,接下來要怎麼解決 getName 這個 function
prototype
function Dog(name) {
this.name = name
}
Dog.prototype.getName = function() {
return this.name
}
Dog.prototype.sayHello = function() {
console.log(this.name)
}
var d = new Dog('abc')
var a = new Dog('def')
此時去比較 a.sayHello === d.sayHello
答案就會是 true
從 prototype 來看「原型鍊」
從上述例子 d.sayHello()
跟 Dog.prototype
之間一定要透過某種方式來連結,不然 js 引擎要怎麼知道d.sayHello()
要去 Dog.prototype.sayHello
找
JavaScript 有個內部屬性 __proto__
這個屬性就是暗示說如果在 d
身上找不到 sayHello
的話,就去 __proto__
找
可以比較 d.__proto__ === Dog.prototype
,答案會是 true
在 call d.sayHello()
的時候,會按照以下步驟去找:
- d 身上有沒有 sayHello
d.__proto__
有沒有 sayHellod.__proto__.__proto__
有沒有 sayHellod.__proto__.__proto__.__proto__
有沒有 sayHello- log 出來是 null 代表找到頂了
d.__proto__
= Dog.prototype
d.__proto__.__proto__
= Object.prototype
Dog.prototype.__proto__
= Object.prototype
以上經由 proto 構成一連串的 chain,就叫做 prototype chain 原型鍊
透過 prototype 的方式,讓底下的東西連起來,共享相同 function
console.log(Dog.__proto__)
印出來會是 [Function]
Dog.__proto__ === Function.prototype
答案會是 true
透過 __proto__
這個底線機制,就可以做很多有趣事情
String.prototype.first = function() {
return this[0]
}
var a = '123'
console.log(a.first()) //印出 1
new
在理解 new 在背後做了什麼事之前,先來看個預備知識
function test() {
console.log(this)
}
test();
發現 this
是一個超級大的值,圖片擷取不完
function 還有另外一個呼叫方式,叫 .call()
function test() {
console.log(this)
}
test.call('123'); // 發現 this 的值就變成 [String: '123']
test.call({}); // this 的值就變成 {}
代表 .call()
傳進去的第一個參數就是裡面 function 的 this
理解 new 背後在做的事情
function Dog(name) {
this.name = name;
}
Dog.prototype.getName = function () {
return this.name;
}
Dog.prototype.sayHello = function () {
console.log(this.name);
}
// 下面 function 就等同於 new 在做的事情
function newDog(name) {
var obj = {};
Dog.call(obj, name);
obj.__proto__ = Dog.prototype;
return obj;
}
var d = newDog('abc')
d.sayHello(); // 印出 abc
// 所以上面兩行就等於下面兩行
var a = new Dog('hi')
a.sayHello();
所以 new 背後在做的事情就是:
- 先建立一個空物件 obj
- 呼叫 constructor,然後利用
.call()
帶入空物件,並設定參數,Dog.call(obj, name)
: 第一個參數代表this
指向obj
,第二個參數代表要帶入的值 - 建立 prototype 關聯 :
obj.__proto__ = Dog.prototype
- return 完成的
obj
Inheritance 物件導向的繼承
當其他 class 需要用到共同屬性時,可以利用 Inheritance 繼承的方法,來直接存取父層的屬性。
class Dog {
constructor(name) {
this.name = name;
}
sayHello() {
console.log(this.name);
}
}
class BlackDog extends Dog {
test() {
console.log('test', this.name);
}
}
const d = new BlackDog('hello');
d.test(); // test hello
d.sayHello() // hello
解析程式碼 :
在 const d = new BlackDog('hello');
因為在 class BlackDog extends Dog {...}
沒有寫 constructor
的關係
所以他會往 parent(父類別)的 constructor
找並執行
在 d.test();
在 class BlackDog extends Dog {...}
有找到並直接執行
那如果想要黑狗被建立時就呼叫一個 function 直接 say hello
那可以在繼承裡面直接加 constructor
class BlackDog extends Dog {
constructor() {
this.sayHello();
}
}
結果會出現錯誤
Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
這句話是在說 : call this
之前一定要 call super
super
就是上一層的 constructor
class BlackDog extends Dog {
constructor(name) {
super(name);
this.sayHello();
}
}
為什麼要這樣做呢?
假設沒有 call super
那在 this.sayHello();
的時候,發現父類別的 sayHello() {}
裡又有 console.log(this.name);
,但此時又還沒初始化(父類別還沒 constructor
),那用到需要初始化的 this
,不就造成一個 bug 了,所以才會強制一定要 call super
但單 call super
是沒有用的,父類別的 constructor
要傳參數 name 進去,不然就會變 undefined。所以既然已經覆寫了 constructor
,那就要接收一個 name constructor(name)
,然後在把 name 傳上去 super(name);
,讓父類別的 constructor
成功初始化,此時再 call this.sayHello();
才會有正確的值
文章參考 :