給自己看的 JS 進階-物件導向


Posted by 生菜 on 2020-10-23

給自己看的 JS 進階:(建議按照順序看)
給自己看的 JS 進階-變數
給自己看的 JS 進階-Hoisting
給自己看的 JS 進階-Closure
給自己看的 JS 進階-物件導向

什麼是物件導向

從剛剛的例子開始說吧:

function createWallet(init) {
    var money = init
    return {
        add: function(num) {
            money += num
        },
        deduct: function(num) {
            money -= num
        }
    }, getMoney() {
        return money
    }
}

var myWallet = createWallet(99)
myWallet.add(1)
myWallet.deduct(10)
console.log(myWallet.getMoney()) // 90

這個例子中回傳的值是一個物件,其實就算是物件導向。在使用 JS 時,也時常不是直接 call 一個 function ,而是對某個物件做操作,這種做法的好處是方便模組化。

class

從 ES6 的 class 開始談起。

首先, class 的名稱一定是大寫開頭,例如:

class Dog {
    sayHello() {
        console.log('hello')
    }
}

class 有點像設計圖,當我們實際使用前時,要用 new 將 class 實體化 (instance):

var d = new Dog()
d.sayHello() // hello

另一個概念是 this ,它會指向呼叫它的東西:

class Dog {
    setName(name) {
        this.name = name
    }
    getName() {
        return this.name
    }
}

var d = new Dog()
d.setName('jojo')
console.log(getName()) // jojo

上面範例中 d.setName('jojo') 中的 this 因為是由 d 呼喚的,因此 this 當然就指向變數 d 。
class 中 setName(name) 這樣的函數被稱為 setter ,讓裡面存取到外面的值;而 sayHello() 則叫 getter ,是讓外面得到 class 的值。 另外我們也可以直接這樣寫:

d.name = 'dio'
console.log(d.name) // dio

但還是建議用 setter 和 getter 。

如果想要用像是函式傳參數的方式設定,可以用 建構子 constructor

class Dog {
    constructor(name) {
        this.name = name
    }
    getName() {
        return this.name
    }
}

var d = new Dog('jojo') // 字串 'jojo' 被傳入 constructor() 中
console.log(getName()) // jojo

var d = new Dog('dio')
console.log(getName()) // dio

ES5 的 class

在 ES5 中沒有 class ,因此要這樣寫:

function Dog(name) {
    var myName = name
    return {
        getName: function() {
            return myName
        },
        sayHello: funcrion() {
            console.log(myName)
        }
    }
}

var d = Dog('jojo')
d.sayHello // jojo

var b = Dog('dio')
d.sayHello // dio

不過因為每次都是呼叫一個新的物件,會出現這種狀況:

console.log(b.sayHello === d.sayHello) // false

不過兩個是同個 function ,所以共用同個 function 比較省記憶體吧?

因此在 ES5 中,可以將 function 當作 constructor 用:

function Dog(name) {
    this.name = name
}

var d = new Dog('abc')
console.log(d) // Dog { name: 'abc' }

自動變成物件了!
不過這樣要怎麼知道是 constructor 還是平常的 function?只有加 new 才會被認定是 constructor ,如果沒加就是 function 。

設定屬性的問題搞定了,但要怎麼設定輸出名字和其他操作ㄋ?這時候可以把東西掛在 .prototype 上:

Dog.prototype.sayHello = function() {
    console.log(this.name)
}

var d = new Dog('jojo')
d.sayHello // jojo

Prototype

JavaScript 中,每個變數都有個隱藏屬性 __proto__ ,暗示如果在 d 上面找不到 sayHello 的屬性:

function Dog(name) {
    this.name = name
}

Dog.prototype.sayHello = function() {
    console.log(this.name)
}

var d = new Dog('jojo')
d.sayHello // jojo

console.log(d.__proto__)
// Dog { sayHello: [Function (anonymous)] }
// 其實就是 Dog.prototype

當我們呼叫 d.sayHello 時,我們其實是做了:

  1. d 本身是否有 sayHello
  2. d.__proto__ 是否有 sayHello ,也就是 Dog.prototype
  3. 沒有的話就找 d.__proto__.__proto__ ,也就是 Object.prototype
  4. 如果還是沒有就找 d.__proto__.__proto__.__proto__ ,沒有的話會回傳 null。
  5. null 代表找到頂了,沒有的話就會拋出錯誤。

以上都是只要有就會回傳值,沒有的話才往下進行,這個步驟被稱為原型練(Prototype Chain)。

我們來看一下:

console.log(d.__proto__)
// Dog.prototype
// 結果:Dog { sayHello: [Function (anonymous)] }
console.log(d.__proto__.__proto__)
// Dog.prototype.__proto__
// Object.prototype
// 結果:{}
console.log(d.__proto__.__proto__.__proto__)
// null

他們之間的關係如下:

d.__proto__ = Dog.prototype
d.__proto__.__proto__ = Object.prototype
Dog.prototype.__proto__ = Object.prototype

因此我們也可以設定 Object 的 prototype ,這樣就會在第三個步驟呼叫到結果:

Object.prototype.sayHello = function() {
    console.log('object', this.name)
}

var d = new Dog('jojo')
d.sayHello // object jojo

如果同時設定 Object 和 Dog 的 prototype ,則會因為原型鍊會先選到 Dog 的:

Dog.prototype.sayHello = function() {
    console.log(this.name)
}

Object.prototype.sayHello = function() {
    console.log('object', this.name)
}

var d = new Dog('jojo')
d.sayHello // jojo

同理,此處的 Object 如果被換成 Function ,第四個步驟就會被換成 Function.prototype 。

new 到底做了什麼

function.call() 這個函數可以指定 function 中的 this 值:

function test() {
    console.log(this)
}

test.call(123) // [Number: 123]

接著來拆解 new 到底幫我們做了甚麼,因此用另一個 function 來模擬:

function newDog(name) {
    // 模擬 new 做了一些事情
}

// 最後目標
var a = newDog('jojo')
a.sayHello() // 印出 jojo
  1. 建立一個 object,並將值傳入
    ```
    function newDog(name) {
    var obj = {}
    Dog.call(obj, name) // 第一個是 this ,後面依序是傳入值
    console.log(obj)
    }

var a = newDog('jojo') //{ name: 'jojo' }

2. 設定 prototype 連結

function newDog(name) {
var obj = {}
Dog.call(obj, name) // 第一個是 this ,後面依序是傳入值
obj.proto = Dog.prototype
}

var a = newDog('jojo')

3. 回傳 object

function newDog(name) {
var obj = {}
Dog.call(obj, name) // 第一個是 this ,後面依序是傳入值
obj.proto = Dog.prototype
return obj
}

var a = newDog('jojo')
a.sayHello() // 印出 jojo


就完成ㄌ!

### Inheritance

設想有一個狗的 class ,今天我需要設定黑狗和白狗,這時有名字、會叫、丟飛盤會去接回來之類的和狗有關的屬性就不用再設定一次了。要是有人問你「黑狗有幾個眼睛」時,只要回頭查看「狗」的條目就可以了。這就是 `Inheritance` 繼承的概念。

ES6 中的繼承可以這樣寫:

class BlackDog extands Dog{
// 其他黑狗的屬性
}

const d = BlackDog('jojo')
d.sayHello()

上面的例子中 `d.sayHello()` 實際上是往上找到 Dog 的屬性。

此時若我們想讓黑狗被建立的時候就呼叫 `sayHello()`:

class BlackDog extands Dog{
constructure() {
this.sayHello()
}
}

const d = BlackDog('jojo')

這樣會噴錯,因為在 `constructor` 中呼叫 `this` 前要用 `super()` 另外引入上一層的`constructor` ,如下:

class BlackDog extands Dog{
constructure(name) {
super(name)
this.sayHello()
}
}

const d = BlackDog('jojo') // jojo


### this

`this` 在物件導向中被使用,可以用代表其所對應到的 instance 。

如果直接呼叫 `this` 例如:

function test() {
console.log(this)
}

test()

會出現一長串的東西。

若不是物件導向的環境下,預設值為 Global ,node.js 跑是 `global` 的變數,瀏覽器則是 `window` 。也可以在檔案最上方輸入 `'use strict';` 進入嚴格模式,此時的預設值就會是 `undefined` 。

另一個例外是使用 DOM 的時候:

document.querySelector('.dom').addEventListener('click', function() {
console.log(this) // 點擊到的東西
})


### call 和 apply

`.call()` 的第一個值被預設為 this 的值:

function test() {
console.log(this)
}

test.call(123) // [Number: 123]


`apply` 也是:

function test() {
console.log(this)
}

test.apply(123) // [Number: 123]


兩個的差別是後面的參數引入的方法, call 就是用逗號連接,但 apply 只有兩個參數,第二個參數則是將要傳入的參數們用陣列包起來。

#### 怎麼看 this

const obj = {
a: 123,
test: function() {
console.log(this)
}
}

obj.test() // this 對應到 obj 本身


`this` 和放在哪裡無關,而是看呼叫的方法。例如以下寫法雖然一樣,結果卻不同:

const obj = {
a: 123,
test: function() {
console.log(this)
}
}

const func = obj.test
func() // undefined


因為第一個寫法 `obj.test()` 可被視為 `obj.test.call(obj)` ,因此會呼叫到 obj 。

#### bind

小小練習,自己先猜猜看答案:

function log() {
console.log(this);
}

var a = { a: 1, log: log };
var b = { a: 2, log: log };

log(); // global
a.log(); // a

b.log.apply(a) // a,因為 call 的值優先


如果希望不管怎麼呼叫, this 的值都不會變,可以是用 `.bind()` :

const bindTest = obj.test.bind(obj)

之後不管從哪裡呼叫 `bindTest()` , this 的結果都是 obj 。

`bind` 和 `call` / `apply` 的差別在於,前者會回傳一個新的  function ,後者則是直接呼叫。

#### 碰到箭頭函式,一切都不一樣ㄌ

使用到箭頭函式時, this 的值和如何呼叫沒有關係,此時的規則和 scope 比較像,也就是和定義在哪裡有關係。

可以看這個例子:

class Test {
run() {
consoel.log(this) // Test
setTimeOut(function() {
console.log(this) // unefined
}, 1000)
}
}

const t = newTest()
t.run()

但如果用箭頭函式:

class Test {
run() {
consoel.log(this) // Test
setTimeOut(()=>{
console.log(this) // Test
}, 1000)
}
}

const t = newTest()
t.run()
```










Related Posts

box model、display 的三種形式、定位(position)

box model、display 的三種形式、定位(position)

MTR04_0617

MTR04_0617

Why and How I start learning programming language?

Why and How I start learning programming language?


Comments