[JS] JavaScript OOP basic
- Encapsulation: Reduce complexity + increase reusability (封裝,像是新增一個 Class 裝入共用的 Class,形狀包含三角形、長方形、圓形)
- Abstraction: Reduce complexity + isolate impact of changes(hide the details, show the essentials)
- Inheritance: Eliminate redundant code (繼承)
- 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
其實我們平常在建立 object 或是 string 等等,都是透過new
一個 class 來建立,ex.
let x = {};
// let x = new Object();
let y = "";
// let y = new String();
new
Keywords
new
這個關鍵字做了 4 件事情
- 建立空白的 object
- 將
this
指向到新建立的 object - 回傳這個 object ,也就是回傳
this
- 將這個 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
之中,新建立的 Circle
的 prototype
是這個 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
,重複以上的過程,直到 prototype
是 null
為止。
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 ... in
的for
迴圈可以枚舉 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
我們將currentLocation
、power
設為私有 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 無法改變