什麼是物件導向?
物件導向的世界 call function 不太一樣。
- 物件導向:call function 的方式會是對這個物件做操作、
- 優點:更容易模組化、隱藏資訊。
myWallet.add(1)
add(myWallet(1))
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 是一個物件,事實上他就是一個物件
myWallet.deduct(100)
console.log(myWallet.getMoney()) // 90
物件導向的基礎範例
ES6 以後有 class 語法可以使用,先從 ES6 如何實作物件導向談起。
class 名稱一定大寫開頭。class 是設計圖定義這個類別會有什麼樣的行為、有什麼樣的方法進行呼叫。
// 最基本 class 雛形
class Dog {
// function,表 class 裡面的 method
sayHello() {
console.log('hello')
}
}
實際使,須用 new
去實體化,從這個設計圖裡去實際產生 instance(實例)。
var d = new Dog() // 產生一個 instance
d.sayHello() // hello
目前代碼長相
class Dog {
sayHello() {
console.log('hello')
}
}
var d = new Dog()
d.sayHello()
如何設定 d.setName('abc') 就是 abc ?
答:透過 this,this 一開始就存在物件導向裡面。先不管任何在物件導向外面用 this 的情形,當 this 在物件導向裡面用,他所指到的值就是誰呼叫他就指到他。
d.setName('abc') 我要對 d 做 setName,所以 this.name = name
這個 this 就是 d,所以狗的名稱就會是他傳進來的名稱。
class Dog {
setName(name) {
this.name = name
}
sayHello() {
console.log(this.name)
}
}
var d = new Dog()
d.setName('abc')
d.sayHello() // abc
透過 getter、setter 進行操作
- setter:因為無法直接從外面存取裡面的值,所以要在 class 內部進行相對應的設定
- getter:拿到相對應的值
想知道狗的名字就 console.log(d.getName())
class Dog {
// setter
setName(name) {
this.name = name
}
// getter
getName() {
return this.name
}
sayHello() {
console.log(this.name)
}
}
var d = new Dog()
d.setName('abc')
console.log(d.getName())
也可以透過 console.log(d.name)
做到相同效果,d.name 進行存取。
d.name = 'aaa'
console.log(d.name) // aaa
console.log(d.getName()) // aaa
但不建議這麼寫。還是建議透過 setter、getter 進行存取因為有可能有時候需要做到其他事情。舉例來說:不會想自己操作這些東西。這是一個比較好的開發上習慣。
getName() {
return this.firstName + this.lastName
}
再次觀察代碼
class Dog {
// setter
setName(name) {
this.name = name
}
// getter
getName() {
return this.firstName + this.lastName
}
sayHello() {
console.log(this.name)
}
}
var d = new Dog()
d.setName('abc')
d.sayHello()
當我們在新建每一隻狗都會想給她名字,new instance 有點像 function call 的感覺,可以傳參數進去 instance。
可以透過 constructor 這個 function 去進行接收。當我 call new Dog() 其實就是在 call constructor() {略}
,所以我傳進來的參數就會出現在這邊 constructor(參數)
class Dog {
// 建構子
constructor(name) {
this.name = name // this 指向對它操作的 instance
}
setName(name) {
this.name = name
}
getName() {
return this.firstName + this.lastName
}
sayHello() {
console.log(this.name)
}
}
var d = new Dog('abc')
d.setName('abc')
d.sayHello()
var e = new Dog('xxx')
e.sayHello()
小結
- 物件導向有設計圖 class
- 以 new instance 的方式將東西弄出來
- new ClassName(參數) call 這個參數進行初始化,就是 call class 的 constructor
- class 內使用 this 取指向對他操作的 instance,畢竟沒有 this 也不知道怎麼改值。
ES5 的 class
ES6 之前並沒有 class
這個關鍵字,所以我們可以怎麼做達到相同的效果呢 ?
作法一、建構 function
可以達到原本的效果。
只是每宣告一次 Dog 都會產生一個新的物件,新的物件會回傳這兩個 function : getName、sayHello,所以今天對兩隻狗 console.log(d.sayHello === b.sayHello)
,會發現 false,這兩個是不同的 function。
但他們做的功能相同,應該共用相同的 function,想想看如果是作法一的模式,若有一百萬隻狗,就會有一百萬個 getName function,每個 function 都做差不多的事情。
如果是 es6 的作法 console.log(d.sayHello() === b.sayHello() )
,它 log 的就會是 true,因為它都指 console.log(this.name),會根據 this 的不同印出不同的內容。
作法一每創建一隻狗,就會有一個新的 getName function,這樣的做法非常耗費記憶體。
function Dog(name) {
var myName = name
return {
getName: function() {
return myName
},
sayHello: function() {
console.log(myName)
}
}
}
var d = Dog('abc')
d.sayHello() // abc
var b = Dog('xxx')
b.sayHello() // xxx
作法二、
希望保留這個語法:var d = new Dog('abc')
- 將 new instance 的 instance 名稱變成 function 名稱
- 以舊語法建構 es6 的 constructor,ES5 裡面可以把 function 當作 constructor 用。有加 new 才會把 function 當作 constructor 用。
- 在這個 constructor 內使用 this
- 透過 prototype 實作 class Dog 內部的 function
// 相當於 es6 constructor
function Dog(name) {
this.name = name
}
var d = new Dog('abc') // Dog 的 instance
console.log(d) // Dog { name: 'abc' }
有加 new 才會把 function 當作 constructor 用。
// 相當於 es6 constructor
function Dog(name) {
this.name = name
}
var d = Dog('abc') // undefined
console.log(d) // Dog { name: 'abc' }
一樣成功呼叫兩隻狗,而且最下面呼叫的方式和 es6 一樣。
console.log(d.sayHello === e.sayHello) 是 true,因為他們都是 prototype 上面這個 function。
// 相當於 es6 constructor
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')
d.sayHello() // abc
var e = new Dog('xxx')
e.sayHello() // xxx
小結
ES5 裡面,只要將 constructor 換成同名的 function,然後把 ES6 的 method 放在 prototype 上面,ClassName.prototype.methodName = function( ){略},就可以實作出 ES6 的 class。
從 prototype 來看「原型鍊」
d.sayHello() 會呼叫 Dog.prototype.sayHello,所以 d 跟 Dog.prototype 一定是透過某種方式把這兩個東西給連結在一起。
// 相當於 es6 constructor
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')
d.sayHello() // abc
js 內部有一個屬性叫做 __proto__
。
以下面代碼為例:如果在 d 身上找不到 sayHello,應該就去這裡找,log 出 d.__proto__
=> { getName: [Function (anonymous)], sayHello: [Function (anonymous)] },所以它其實就是 Dog.prototype
var d = new Dog('abc')
d.sayHello() // abc
console.log(d.__proto__) // { getName: [Function (anonymous)], sayHello: [Function (anonymous)] }
console.log(d.__proto__ === Dog.prototype) // true
ES6 class 底層是用這套機制進行實作。我們需要理解底層實作的原理 !
d.sayHello()
d 身上有無 sayHello => 沒有
d.__proto__
身上有無 sayHello,透過 new 設定達到d.__proto__
=== Dog.prototype,所以會去 Dog.prototype 身上找有無 sayHello => 有,call 這個 sayHello,它的 this 就是 d如果還是找不到,步驟 2 失敗 =>
d.__proto__.__proto__
身上有無 sayHello => 因為,d.__proto__.__proto__
=== Object.prototype,所以會去 Dog.prototype 身上找有無 sayHello如果還是找不到,步驟 2,3 失敗 =>
d.__proto__.__proto__.__proto__
身上有無 sayHello => 因為,d.__proto__.__proto__.__proto__
=== null,所以 null 代表找到頂,如果還是沒有就代表沒有這個 function,就會拋出錯誤
function Dog(name) {
this.name = name
}
Dog.prototype.getName = function() {
return this.name
}
var d = new Dog('abc')
d.sayHello() // TypeError: d.sayHello is not a function
幫 Dog、Object 都各加一個,會呼叫到 Dog 這個
function Dog(name) {
this.name = name
}
Dog.prototype.getName = function() {
return this.name
}
Dog.prototype.sayHello = function() {
console.log('dog', this.name)
}
Object.prototype.sayHello = function() {
console.log('object', this.name)
}
var d = new Dog('abc')
d.sayHello() // dog abc
經由 __proto__
構成的一連串 chain,就叫做 prototype chain (原型鍊)。透過 __proto__
看能不能找到對應的 function。透過 prototype 進行連結,讓底下的東西可以共同享有同個 function。好處是只要寫一個 function,不用每個 instance 都寫一個 function。
js 很多 chain 相關的,像 scope chain。
console.log(Dog.__proto__)
會是一個 Function,{ }。
function Dog(name) {
this.name = name
}
Dog.prototype.getName = function() {
return this.name
}
var d = new Dog('abc')
console.log(Dog.__proto__) // { },Dog 是一個 function
console.log(Dog.__proto__ === Function.prototype) // true
為甚麼兩個 call 的結果不同 ?
Object.prototype.toString.call('123') // [object String]
var a = '123'
a.toString() // 123
a 本身沒有 toString,它一樣是放在 prototype 上面你才呼叫的到。所以當我們 call a.toString()
其實就是在 call String.prototype.toString
。如果要再往上找一層,call object,就沒辦法直接 call a.toString()
,要用其他方法去 call function 才能呼叫到它,因為它被原型鍊給擋住了。
字串一樣會有自己的原型鍊,
var a = '123'
a.toString() // 123
console.log(a.__proto__ === String.prototype) // true
console.log(a.toString === String.prototype.toString) // true
可以幫 String 加 prototype,這樣每一個 string 都可以呼叫到這個 function。
同理,也可以幫 array、object 加
String.prototype.first = function() {
return this[0]
}
var a = '123'
console.log(a.first()) // 1
console.log(a.__proto__ === String.prototype) // true
小結
__proto__
將 instance 與 prototype 連接起來,透過這個屬性往上找
所以,new 到底做了什麼事?
this 的值會是一個非常大、非常奇怪的值
function test() {
console.log(this)
}
test()
/*
<ref *1> Object [global] {
global: [Circular *1],
clearInterval: [Function: clearInterval],
clearTimeout: [Function: clearTimeout],
setInterval: [Function: setInterval],
setTimeout: [Function: setTimeout] {
[Symbol(nodejs.util.promisify.custom)]: [Function (anonymous)]
},
queueMicrotask: [Function: queueMicrotask],
clearImmediate: [Function: clearImmediate],
setImmediate: [Function: setImmediate] {
[Symbol(nodejs.util.promisify.custom)]: [Function (anonymous)]
}
}
*/
除了這種 test( ) 呼叫方式,也可以用 .call(參數) 呼叫 function
當用 .call 的方法,第一個參數就會是裡面的 this。我傳甚麼裡面的 this 就是甚麼
function test() {
console.log(this)
}
test.call('123') // [String: '123']
test.call({}) // { }
實作 new 機制
現在,newDog 就要做 new
所做的事情
// constructor
function Dog(name) {
this.name = name
}
// 希望達成目標
var b = newDog('hello')
b.sayHello()
function newDog(name) {
}
- 宣告一個新的 object
- 執行建構子(constructor)
- Dog 裡面的 this 就是 obj,Dog 的引數就是我傳進去的 name =>
console.log(obj) // { name: 'hello' }
=> 這個 this.name 就等於 obj.name 就等於傳進去的 name 'hello',現在有了一個執行完建構子把資料放到裡面的物件 - 串 prototype,
obj.__proto__ = Dog.prototype
,相互連結後就可以呼叫產生的 instance - 回傳產生的物件
function Dog(name) {
this.name = name
}
Dog.prototype.sayHello = function() {
console.log(this.name)
}
Dog.prototype.getName = function() {
return this.name
}
// 希望達成目標
var b = newDog('小狗汪汪')
b.sayHello()
console.log(b.getName())
function newDog(name) {
var obj = {} // 建立物件
Dog.call(obj, name) // Dog 裡面的 this 就是 obj,Dog 的引數就是我傳進去的 name
obj.__proto__ = Dog.prototype
return obj
}
new 可以接收任何 type,這邊只以 Dog 當範例,也可以改成更普遍的 function,只是換參數而已
小結
.call(參數),物件導向裡的 this 就會是我傳進去的參數
new 關鍵字所做的事情 :
- 產生新的物件
- 呼叫建構子
- 將新產生的物件當作 this 丟入建構子 => 完成物件初始化
- 設定 proto 讓他連到相對應的 prototype
- 將弄好的物件回傳 => 他就是你想要的 instance 了
物件導向的繼承:Inheritance
需要用到一些共同屬性,不用所有東西都自己重做,比如:狗它有名字、它會握手、它會說話,今天有別的品種的狗,它也具備狗的行為是狗底下的分支,例如:黑狗、白狗,這個時候只要繼承狗就會繼承他的 method,只要狗有的 function 都可以用,因為已經繼承了。
解析黑狗繼承狗這段代碼
const d = new BlackDog('小黑')
,因為 BlackDog 裡面沒寫建構子的關係,所以它就會去找他的 parent 也就是 Dog,他去找 Dog 裡面的建構子執行 => 黑狗現在有名字了- d.test() => 在
class BlackDog extends Dog {略}
這裡面找到,所以執行代碼 => this.name = 小黑 => 印出 test 小黑 - 也可以 call d.sayHello()
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('小黑')
d.test() // test 小黑
d.sayHello() // 小黑
希望黑狗被建立,就 sayHello,和我們打招呼
ReferenceError: Must call super constructor in derived class before accessing 't
因為 call 建構子裡面的 this.sayHello(),class Dog 要 this.name,但尚未初始化,因此如果有用到需要初始化的東西就會出現錯誤。因此強制一定要 call super。
class Dog {
constructor(name) {
this.name = name
}
sayHello() {
console.log(this.name)
}
}
class BlackDog extends Dog {
constructor() {
this.sayHello()
}
test() {
console.log('test', this.name)
}
}
const d = new BlackDog('小黑')
super( ) 就是上一層的 constructor,所以當我 call super,就是再 call Dog 的 constructor
印出 undefined
顯然單 call super 是沒有用的,因為我 call super Class Dog 的 name 還是 undefined。既然已經覆寫了這個建構子,我就必須接收一個 name,將 name 一起傳上 parent 的建構子,讓他成功的初始化,初始化後再 call this.sayHello 才會有正確的值。
// 單 call super 是沒有用的,印出 undefined
class BlackDog extends Dog {
constructor() {
super() // Dog.constructor
this.sayHello()
}
test() {
console.log('test', this.name)
}
}
const d = new BlackDog('小黑')
如果 super 不傳值,我的 parent 的建構子因為沒有進行初始化,所以 parent 的 sayHello function,就是 undefined,因為裡面的 this.name 沒有被初始化過
// 成功存取到值
class BlackDog extends Dog {
constructor(name) {
super(name) // Dog.constructor
this.sayHello()
}
test() {
console.log('test', this.name)
}
}
const d = new BlackDog('小黑') // 小黑
小結
- 繼承某個 class 後,在建構子內用 super 呼叫 parent 的建構子,並設定初始化的東西。
- 完成設定後,可以使用繼承的 method
繼承的優點:
可以先將東西寫好,其餘變化較小的再用繼承去改行為