動態

詳情 返回 返回

JavaScript 面向對象編程 - 動態 詳情

面向對象編程 Object Oriented Programming

面向對象編程用對象把數據和方法聚合起來。

面向對象編程的優點

  • 能寫出模塊化的代碼
  • 能使得代碼更靈活
  • 能提高代碼的可重用性

面向對象編程的原則

  • 繼承(inheritance):子類/派生類從父類/基類/超類中派生,形成繼承結構
  • 封裝(encapsulation):代碼的實現對用户不可見,例如調用 toUpperCase(),直接調用即可,不用考慮函數內部的實現
  • 抽象(abstraction):抽象就是抽象出你正在嘗試做的事情的概念,而不是處理這個概念的具體表現形式
  • 多態(polymorphism):多種形態

對象

創建對象

  • 使用對象字面量創建:
let dog = {
    name: "Mit",
    numLegs: 2
}
  • 使用 Object.create() 創建:(所有的對象都來自某個類,對象是類的實例,也是一種層級結構)
class Animal {
    /* code */
}

var myDog = Object.create(Animal);
console.log(Animal);
  • 使用 new 關鍵字:(當使用默認或者空構造函數方法,JS 會隱式調用對象的超類來創建實例)
class Animal { /* code here */ }

var myDog = new Animal();
console.log(Animal)

訪問對象屬性

let dog = {
    name: "Mit",
    numLegs: 2
}
console.log(dog.name); // Mit

對象方法 Method

對象可以有一個叫做 method 的特殊屬性。

方法屬性也就是函數。 這給對象添加了不同的行為。

let dog = {
  name: "Spot",
  numLegs: 4,
  sayLegs: function() {
    return "This dog has 4 legs."
  }
};

dog.sayLegs();

this

this 代表上下文環境中引用的對象。

let dog = {
  name: "Spot",
  numLegs: 4,
  sayLegs: function() {return "This dog has " + this.numLegs + " legs.";}
};

dog.sayLegs(); // This dog has 4 legs.

構造函數

constructors 是創建對象的函數。 函數給這個新對象定義屬性和行為。 可將它們視為創建的新對象的藍圖。

構造函數遵循一些慣例規則:

  • 構造函數函數名的首字母大寫,這是為了方便區分構造函數( constructors)和其他非構造函數。
  • 構造函數使用 this 關鍵字來給它將創建的這個對象設置新的屬性。 在構造函數裏面,this 指向的就是它新創建的這個對象。
  • 構造函數定義了屬性和行為就可創建對象,而不是像其他函數一樣需要設置返回值。
function Dog() {
  this.name = "N";
  this.color = "red";
  this.numLegs = 2;
}

function Dog(name, color) {
  this.name = name;
  this.color = color;
  this.numLegs = 4;
}

let terrier = new Dog("fsd","fa");

JS 中有很多內置對象類型,稱為“原生對象(native object)”。有些類型可以使用構造函數獲取一個實例,但是有些類型沒有也不需要構造函數。例如 Date 可以用 new 關鍵字創建對象,但是 Math 對象就不行。

如果對象類型是基本類型(primitive),使用構造函數通常不是最佳實踐:

let apple = new String('apple');
apple; // string

對於常規對象,最好使用對象字面量語法 {},而不是 new。例如:數組,函數,正則表達式等。

下列類型可以考慮構造函數:

new Date();
new Error();
new Map();
new Promise();
new Set();
new WeakSet();
new WeakMap();

使用構造函數創建對象

注意: 構造函數內的 this 總是指被創建的對象。

注意:通過構造函數創建對象的時候要使用 new 操作符。

function Dog() {
  this.name = "Rupert";
  this.color = "brown";
  this.numLegs = 4;
}
let hound = new Dog();

instanceof

凡是通過構造函數創建出的新對象,這個對象都叫做這個構造函數的 instance。 JavaScript 提供了一種很簡便的方法來驗證這個事實,那就是通過 instanceof 操作符。 instanceof 允許將對象與構造函數之間進行比較,根據對象是否由這個構造函數創建的返回 true 或者 false。 以下是一個示例:

let Bird = function(name, color) {
  this.name = name;
  this.color = color;
  this.numLegs = 2;
}

let crow = new Bird("Alexis", "black");

crow instanceof Bird; // true

屬性

自身屬性 Own Property

請看下面的實例,Bird 構造函數定義了兩個屬性:namenumLegs

function Bird(name) {
  this.name  = name;
  this.numLegs = 2;
}

let duck = new Bird("Donald");
let canary = new Bird("Tweety");

namenumLegs 被叫做 自身屬性,因為它們是直接在實例對象上定義的。 這就意味着 duckcanary 這兩個對象分別擁有這些屬性的獨立副本。 事實上,Bird 的所有實例都將擁有這些屬性的獨立副本。

原型屬性 Prototype Property

所有 Bird 實例可能會有相同的 numLegs 值,所以在每一個 Bird 的實例中本質上都有一個重複的變量 numLegs

當只有兩個實例時可能並不是什麼問題,但想象一下如果有數百萬個實例。 這將會產生許許多多重複的變量。

更好的方法是使用 Birdprototypeprototype 是一個可以在所有 Bird 實例之間共享的對象。 以下是一個在 Bird prototype 中添加 numLegs 屬性的示例:

Bird.prototype.numLegs = 2;

現在所有的 Bird 實例都擁有了共同的 numLegs 屬性值。

console.log(duck.numLegs);
console.log(canary.numLegs);

由於所有的實例都可以繼承 prototype 上的屬性,所以可以把 prototype 看作是創建對象的 "配方"。 請注意:duckcanaryprototype 屬於 Bird 的構造函數,即 Bird 的原型 Bird.prototype。 JavaScript 中幾乎所有的對象都有一個 prototype 屬性,這個屬性是屬於它所在的構造函數。

迭代屬性 Iterate Property

自身屬性是直接在對象上定義的。 而原型屬性在 prototype 上定義。

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

Dog.prototype.numLegs = 4;

let beagle = new Dog("Snoopy");

let ownProps = [];
let prototypeProps = [];

for (let property in beagle) {
  if (beagle.hasOwnProperty(property)) {
    ownProps.push(property); // let in 只能遍歷自身屬性
  } else {
    prototypeProps.push(property);
  }
}

構造函數屬性 Constructor Property

在上一個挑戰中創建的實例對象 duckbeagle 都有一個特殊的 constructor 屬性:

let duck = new Bird();
let beagle = new Dog();

console.log(duck.constructor === Bird); // true
console.log(beagle.constructor === Dog); // true

需要注意到的是: constructor 屬性是對創建實例的構造函數的一個引用。 constructor 屬性的一個好處是可以通過檢查這個屬性來找出它是一個什麼對象。
注意: 由於 constructor 屬性可以被重寫,所以最好使用instanceof 方法來檢查對象的類型。

給對象添加屬性

一種更有效的方法就是給對象的 prototype 設置為一個已經包含了屬性的新對象。 這樣一來,所有屬性都可以一次性添加進來:

Bird.prototype = {
  numLegs: 2, 
  eat: function() {
    console.log("nom nom nom");
  },
  describe: function() {
    console.log("My name is " + this.name);
  }
};

更改原型時,記得設置構造函數屬性

手動設置一個新對象的原型有一個重要的副作用。 它清除了 constructor 屬性! 此屬性可以用來檢查是哪個構造函數創建了實例,但由於該屬性已被覆蓋,它現在給出了錯誤的結果:

duck.constructor === Bird; // false
duck.constructor === Object; // true
duck instanceof Bird; // true

為了解決這個問題,凡是手動給新對象重新設置過原型對象的,都別忘記在原型對象中定義一個 constructor 屬性:

Bird.prototype = {
  constructor: Bird,
  numLegs: 2,
  eat: function() {
    console.log("nom nom nom");
  },
  describe: function() {
    console.log("My name is " + this.name); 
  }
};

原型 Prototype

在 JS 中,原型是一種屬性可以被多個其他對象共享的對象。

對象的原型

來自同一原型的對象有相同的功能。

對象也可直接從創建它的構造函數那裏繼承其 prototype。 請看下面的例子:Bird 構造函數創建了一個 duck 對象:

function Bird(name) {
  this.name = name;
}

let duck = new Bird("Donald");

duckBird 構造函數那裏繼承了它的 prototype。 你可以使用 isPrototypeOf 方法來驗證他們之間的關係:

Bird.prototype.isPrototypeOf(duck); // true

原型鏈 Prototype Chain

JavaScript 中所有的對象(除了少數例外)都有自己的 prototype。 而且,對象的 prototype 本身也是一個對象。

function Bird(name) {
  this.name = name;
}

typeof Bird.prototype; // object

正因為 prototype 是一個對象,所以 prototype 對象也有它自己的 prototype! 這樣看來的話,Bird.prototypeprototype 就是 Object.prototype

Object.prototype.isPrototypeOf(Bird.prototype);

hasOwnProperty 是定義在 Object.prototype 上的一個方法,儘管在 Bird.prototypeduck上並沒有定義該方法,但是依然可以在這兩個對象上訪問到。 這就是 prototype 鏈的一個例子。 在這個prototype 鏈中,Birdducksupertype,而 ducksubtypeObject 則是 Birdduck 實例共同的 supertypeObject 是 JavaScript 中所有對象的 supertype,也就是原型鏈的最頂層。 因此,所有對象都可以訪問 hasOwnProperty 方法。

繼承 Inherit

根據以上所説的 DRY 原則,通過創建一個 Animal supertype(或者父類)來重寫這段代碼:

function Animal() { };

Animal.prototype = {
  constructor: Animal, 
  describe: function() {
    console.log("My name is " + this.name);
  }
};

Animal 構造函數中定義了 describe 方法,可將 BirdDog 這兩個構造函數的方法刪除掉:

Bird.prototype = {
  constructor: Bird
};

Dog.prototype = {
  constructor: Dog
};

繼承使用 extends 關鍵字,例如 class B extends class A

class Animal { /* code */ }
class Bird extends Animal { /* code */ }
class Eagle extends Bird { /* code */ }

從超類繼承行為 Inherit Behaviors from a Supertype

這裏用到構造函數的繼承特性。 首先創建一個超類 supertype(或者叫父類)的實例。

let animal = new Animal();

此語法用於繼承時會存在一些缺點。相反,另外一種沒有這些缺點的方法來替代 new 操作:

let animal = Object.create(Animal.prototype);

Object.create(obj) 創建了一個新對象,並指定了 obj 作為新對象的 prototype。 回憶一下,之前説過 prototype 就像是創建對象的“配方”。 如果把 animalprototype 設置為與 Animal構造函數的 prototype 一樣,那麼就相當於讓 animal 這個實例具有與 Animal 的其他實例相同的“配方”了。

第二個步驟:給子類型(或者子類)設置 prototype。 這樣一來,Bird 就是 Animal 的一個實例了。

Bird.prototype = Object.create(Animal.prototype);

請記住,prototype 類似於創建對象的“配方”。 從某種意義上來説,Bird 對象的配方包含了 Animal 的所有關鍵“成分”。

let duck = new Bird("Donald");
duck.eat();

duck 繼承了Animal 的所有屬性,其中包括了 eat 方法。

super 關鍵字可以借用父類中的功能。

class Animal {
    constructor(color = 'yellow', energy = 100) {
        this.color = color;
        this.energy = energy;
    }
    isActive() {
        if(this.energy > 0) {
            this.energy -= 20;
            console.log('Energy is decreasing, currently at:', this.energy)
        } else if(this.energy == 0){
            this.sleep();
        }
    }
    sleep() {
        this.energy += 20;
        console.log('Energy is increasing, currently at:', this.energy)
    }
    getColor() {
        console.log(this.color)
    }
}

class Cat extends Animal {
    constructor(sound = 'purr', canJumpHigh = true, canClimbTrees = true, color, energy) {
        super(color, energy);
        this.sound = sound;
        this.canClimbTrees = canClimbTrees;
        this.canJumpHigh = canJumpHigh;
    }
    makeSound() {
        console.log(this.sound);
    }
}

class Bird extends Animal {
    constructor(sound = 'chirp', canFly = true, color, energy) {
        super(color, energy);
        this.sound = sound;
        this.canFly = canFly;
    }
    makeSound() {
        console.log(this.sound);
    }

注意⚠️:如果在子類中不使用 super 關鍵字,一旦運行上面的程序,會得到 Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor. 錯誤。

重置構造屬性 Reset

當一個對象從另一個對象那裏繼承了其 prototype 時,那它也繼承了父類的 constructor 屬性。

請看下面的舉例:

function Bird() { }
Bird.prototype = Object.create(Animal.prototype);
let duck = new Bird();
duck.constructor // Bird

但是 duck 和其他所有 Bird 的實例都應該表明它們是由 Bird 創建的,而不是由 Animal 創建的。 為此,你可以手動將 Bird 的構造函數屬性設置為 Bird 對象:

Bird.prototype.constructor = Bird;
duck.constructor // Bird

繼承後添加方法

從超類構造函數繼承其 prototype 對象的構造函數,除了繼承的方法外,還可以擁有自己的方法。

除了從 Animal 構造函數繼承的行為之外,還需要給 Bird 對象添加它獨有的行為。 這裏,給 Bird 對象添加一個 fly() 函數。 函數會以一種與其他構造函數相同的方式添加到 Bird'sprototype中:

Bird.prototype.fly = function() {
  console.log("I'm flying!");
};

現在 Bird 的實例中就有了 eat()fly() 這兩個方法:

let duck = new Bird();
duck.eat();
duck.fly();

duck.eat() 將在控制枱中顯示字符串 nom nom nomduck.fly()將顯示字符串 I'm flying!

重寫繼承的方法 Override

通過使用一個與需要重寫的方法相同的方法名,向ChildObject.prototype 中添加方法。 請看下面的舉例:Bird 重寫了從 Animal 繼承來的 eat() 方法:

function Animal() { }
Animal.prototype.eat = function() {
  return "nom nom nom";
};
function Bird() { }

Bird.prototype = Object.create(Animal.prototype);

Bird.prototype.eat = function() {
  return "peck peck peck";
};

如果一個實例:let duck = new Bird();,然後調用了 duck.eat(),以下就是 JavaScript 在 duckprototype 鏈上尋找方法的過程:

  1. duck => eat() 是定義在這裏嗎? 不是。
  2. Bird => eat() 是定義在這裏嗎? => 是的。 執行它並停止往上搜索。
  3. Animal => 這裏也定義了 eat() 方法,但是 JavaScript 在到達這層原型鏈之前已停止了搜索。
  4. Object => JavaScript 在到達這層原型鏈之前也已經停止了搜索

多態 Polymorphism

多態來源於希臘語,意思是“多種形態”。

例如門和自行車上都有鈴,但是它們的使用目的,形態都可能不同。

function ringTheBell(thing) {
  console.log(thing.bell())
}

ringTheBell(door);
ringTheBell(bicycle);

例如 concat() 方法和 + 都可以起到字符串拼接的作用,但是它們的形態不同。

多態性允許開發人員構建具有完全相同功能的對象,即具有完全相同名稱、行為完全相同的函數。但同時,開發者可以在 OOP 結構的其他部分覆蓋共享功能的某些部分,甚至覆蓋完整功能。

舉例:

class Bird {
  useWings() {
    console.log('flying');
  }
}

class Eagle {
  useWings() {
    super.useWings();
    console.log('barely flying');
  }
}

class Penguin extends Bird {
  useWings() {
    console.log('diving');
  }
}

var baldEagle = new Eagle();
var kingPenguin = new Penguin();
baldEagle.useWings(); // flying barely flying
kingPenguin.useWings() // diving

使用 Mixin 在不相關對象之間添加共同行為

對於不相關的對象,更好的方法是使用 mixins。 mixin 允許其他對象使用函數集合。

let bird = {
  name: "Donald",
  numLegs: 2
};

let boat = {
  name: "Warrior",
  type: "race-boat"
};

let glideMixin = function(obj) {
  obj.glide = function() {
    console.log("glide");
  }
};

glideMixin(bird);
glideMixin(boat);

私有化 Private

使屬性私有化最簡單的方法就是在構造函數中創建變量。 可以將該變量範圍限定在構造函數中,而不是全局可用。 這樣,屬性只能由構造函數中的方法訪問和更改。

在 JavaScript 中,函數總是可以訪問創建它的上下文。 這就叫做 closure(閉包)。

function Bird() {
  let hatchedEgg = 10;

  this.getHatchedEggCount = function() { 
    return hatchedEgg;
  };
}
let ducky = new Bird();
ducky.getHatchedEggCount();

立即調用函數表達式(IIFE)

JavaScript 中的一個常見模式就是,函數在聲明後立刻執行:

(function () {
  console.log("Chirp, chirp!");
})();

這是一個匿名函數表達式,立即執行並輸出 Chirp, chirp!

請注意,函數沒有名稱,也不存儲在變量中。 函數表達式末尾的兩個括號()會讓它被立即執行或調用。 這種模式被叫做立即調用函數表達式(immediately invoked function expression) 或者IIFE。

一個立即調用函數表達式(IIFE)通常用於將相關功能分組到單個對象或者是 module 中。 例如,先前的挑戰中定義了兩個 mixins:

function glideMixin(obj) {
  obj.glide = function() {
    console.log("Gliding on the water");
  };
}
function flyMixin(obj) {
  obj.fly = function() {
    console.log("Flying, wooosh!");
  };
}

可以將這些 mixins 分成以下模塊:

let motionModule = (function () {
  return {
    glideMixin: function(obj) {
      obj.glide = function() {
        console.log("Gliding on the water");
      };
    },
    flyMixin: function(obj) {
      obj.fly = function() {
        console.log("Flying, wooosh!");
      };
    }
  }
})();

注意:一個立即調用函數表達式(IIFE)返回了一個 motionModule對象。 返回的這個對象包含了作為對象屬性的所有 mixin 行為。 module 模式的優點是,所有的運動相關的行為都可以打包成一個對象,然後由代碼的其他部分使用。 下面是一個使用它的例子:

motionModule.glideMixin(duck);
duck.glide();

Add a new 評論

Some HTML is okay.