Skip to main content

[JS] JavaScript OOP basic

  1. Encapsulation: Reduce complexity + increase reusability (封裝,像是新增一個 Class 裝入共用的 Class,形狀包含三角形、長方形、圓形)
  2. Abstraction: Reduce complexity + isolate impact of changes(hide the details, show the essentials)
  3. Inheritance: Eliminate redundant code (繼承)
  4. Polymorphism(more than one form): Refactor ugly switch/case statements (render method in different shape)

Object literals

最基本的建立 object 的方式,literal 表示說直接的描述object 的狀態

const circle = {
radius: 1,
location: {
x: 1,
y: 1,
},
draw: function () {
console.log("draw");
},
};

circle.draw(); // draw

factories (factory function)

建立一個回傳 object 的函式,透過參數來建立不同的 object。

function createCircle(radius) {
return {
radius,
draw: function () {
console.log("draw");
},
};
}

const circle1 = createCircle(1);
console.log(circle1.radius); // 1;
const circle2 = createCircle(2);
circle2.draw(); // draw

Constructors (constructor function)

同樣是建立函式,函式名字首為大寫的時候,會被認定為宣告一個類別(Class)。建立時使用關鍵字new一個 class,這時候的this會指向新建立的物件,依照 constructor 去建立新元件的 propeties 和 methods。

function Circle(radius) {
this.radius = radius;
this.draw = function () {
console.log("draw");
};
}

const circle3 = new Circle(1);
circle3.draw(); // draw
info

其實我們平常在建立 object 或是 string 等等,都是透過new一個 class 來建立,ex.

let x = {};
// let x = new Object();
let y = "";
// let y = new String();

new Keywords

new 這個關鍵字做了 4 件事情

  1. 建立空白的 object
  2. this 指向到新建立的 object
  3. 回傳這個 object ,也就是回傳 this
  4. 將這個 object 的 prototype 連結到 Object

透過上面的 Circle 的例子,看到有 draw 這個函式,我們可以有以下三種定義方式

function Circle(radius) {
this.radius = radius;
this.draw1= function () {
console.log("draw1");
};
}

這是寫在每個 Circle 物件的屬性之中,每次 new Circle() 都會再次定義這個屬性。

function Circle(radius) {
this.radius = radius;
}

Circle.prototype.draw2 = function () {
console.log("draw2");
}

這是寫在 Contructor Function 之中,新建立的 Circleprototype 是這個 Constructor Function ,裡面會有 draw2 的方法。

function Circle(radius) {
this.radius = radius;
this.__proto__ = Object;
this.__proto__.draw3 = function () {
console.log("draw3");
}
}

最後一種是將 Circle 物件的 prototype 指向到 Object,並且定義了 Object 裡面的 draw 方法。要注意的是 __proto__ 這個屬性可以在大部分的瀏覽器拿到該物件的 prototype ,但是不推薦使用,另外,在 new Class() 的時候,其實 js 會使用 Object.getPrototypeOf 的方法拿到新建立的物件的 prototype ,再把它連結到 Object 上。

故 JavaScript 的這種物件連結的方式被稱為 "The Prototype Chain" ,在一個物件的屬性中找不到你要的屬性時,他會往他的 prototype裡面去做尋找,再找不到,會再去裡面的 prototype ,重複以上的過程,直到 prototypenull 為止。

Object 傳址不傳值

可以由下知道,JavaScript 的原始型別和參考型別

value types: Number, String, Boolean, Symbol, undefined, null

reference types: Object, Function, Array

Primitives are copied by their value

Objects are copied by their reference

我們先看到 JavaSript 的原始型別: number

let number = 10;
function increase(number) {
// the value 10 is passed into this parameter that is local to this function
number++;
}

increase(number);
console.log(number); // 10

再看到 object

// obj are copied by its reference
let obj = { value: 10 };
function increaseObj(obj) {
obj.value++;
}

increaseObj(obj);
console.log(obj); // { value: 11 }

修改 object

透過 dot.或是 bracket[]修改資料,delete刪除資料

obj.location = { x: 1, y: 2 };
obj["color"] = "red";
console.log(obj); // { value: 10, location: { x: 1, y: 2}, color: "red" }
delete obj["color"];
console.log(obj); // { value: 10, location: { x: 1, y: 2} }

enumerate object

透過let ... infor迴圈可以枚舉 object

const obj = { value: 10, location: { x: 1, y: 2 } };
for (let key in obj) {
if (typeof obj[key] !== "number") console.log(key, obj[key]);
} // location {x: 1, y: 2}

const keys = Object.keys(obj);
console.log(keys); // ["value", "location"]

if ("value" in obj) console.log("value is in obj"); // value is in obj

private methods and properties

範例一 : 汽車 class 我們將currentLocationpower設為私有 property,enhance設為私有 method,透過定義 method 才能取到私有值,像是getCurrentLocation(通稱為 getter function)和ride可以被新建立的物件呼叫,進而取得私有值。而另一種取得私有值的方法是透過Object.defineProperty(obj, attr, {get, set}),可以建立 setter、getter 的方法去取得或更變私有值。

class Car {
constructor(tire, width, length) {
this.tire = tire;
this.width = width;
this.length = length;
this.getCurrentLocation = function () {
return currentLocation;
};
let currentLocation = { x: 0, y: 0 };
let power = 2; // private properties;
let enhance = function () {
power *= 2;
}; // private methods;
this.ride = function () {
console.log({ power });
console.log(this.width * this.length * power);
enhance();
console.log({ power });
console.log(this.width * this.length * power);
};
Object.defineProperty(this, "currentLocation", {
get: function () {
return currentLocation;
},
set: function (value) {
if (!value.x || !value.y) throw new Error("Invalid location.");
currentLocation = value;
},
});
}
}

const car1 = new Car(4, 100, 400);
car1.ride();
// {power: 2}
// 80000
// {power: 4}
// 160000
car1.currentLocation; // {x: 0, y: 0} this is a getter
car1.currentLocation = { x: 3, y: 4 }; // {x: 3, y: 4} this is a setter

範例二 : 計時器(stopwatch) startTime, endTime, running 都是私有 property,外部無法取得;duration 雖是私有 property,透過 getter 的方法可以取得,並顯示出時間。

class Stopwatch {
constructor() {
let startTime,
endTime,
running,
duration = 0;

this.start = function () {
if (running) throw new Error("Stopwatch has already started.");

running = true;

startTime = new Date();
};
this.stop = function () {
if (!running) throw new Error("Stopwatch is not started");

running = false;
endTime = new Date();

const seconds = (endTime.getTime() - startTime.getTime()) / 1000;
duration += seconds;
};
this.reset = function () {
(startTime = null), (endTime = null), (running = false);
duration = 0;
};
Object.defineProperty(this, "duration", {
get: function () {
return duration;
},
});
}
}

const sw = new Stopwatch();
sw.start(); // 開始計時
sw.stop(); // 停止計時
sw.duration; // 總共多久

// 小測試
sw.duration; // 4.164 原本的秒數
sw.duration = 3.14; // 3.14 沒有setter,但我試試看改變他的值
sw.duration; // 4.164 無法改變