第8章 控制物件的訪問(setter、getter、proxy)
阿新 • • 發佈:2020-11-24
目錄
1. 使用getter和setter控制屬性訪問
1.1 定義getter與setter
通過物件字面量定義,或在ES6的class中定義
// 通過物件字面量定義 const students = { student: ["Wango", "Lily", "Jack"], // 使用set和get關鍵字,相當於給物件新增了一個屬性(而不是方法) // 在這個屬性被賦值或被讀取時,隱式呼叫getter或setter方法 get firstStudent() { // getter方法不接收任何引數 return this.student[0]; }, set firstStudent(val) { this.student[0] = val; } } // 如同訪問標準物件屬性一樣訪問firstStudent屬性 console.log(students.firstStudent); // Wango // 如同操作標準物件屬性一樣為fristStudent賦值 students.firstStudent = "Tom"; console.log(students.firstStudent); // Tom // 在class中定義setter與getter class Student { constructor() { this.students = ["Wango", "Lily", "Jack"]; } get firstStudent() { return this.students[0]; } set firstStudent(val) { this.students[0] = val; } } const s1 = new Student(); console.log(s1.firstStudent); // Wango s1.firstStudent = "Tom"; console.log(s1.firstStudent); // Tom /** * 針對指定的屬性不一定需要同時定義getter和setter, * 通常僅提供getter,如果在這種試圖寫入屬性值 * 非嚴格模式下寫入的屬性值會被忽略 * 嚴格模式下會丟擲異常 */
通常來講,setter和getter是用於控制訪問私有屬性的,但以上兩種方式都是控制的公共屬性。因為JS沒有私有屬性,只能通過閉包來模擬私有。而字面量和類中getter/setter和屬性不是在同一個作用域中定義的,因此無法控制私有屬性。
通過使用內建的Object.defineProperty方法
function Student(name) { // 建構函式引數初始化屬性值,需要注意的是: // 這個初始化的值沒有經過校驗,可能會出錯 let _name = name; Object.defineProperty(this, "name", { get: () => _name, set: val => _name = val }); } const s = new Student("Wango"); // 只能通過setter和getter設定和訪問屬性 console.log(s.name); // Wango s.name = "Tom"; console.log(s.name); // Tom // 無法直接訪問私有屬性 console.log(typeof s._name === "undefined"); // true
// 在類中使用這個方法同樣有效 class Student { constructor(name) { // 建構函式引數初始化屬性值,需要注意的是: // 這個初始化的值沒有經過校驗,可能會出錯 let _name = name; Object.defineProperty(this, "name", { get: () => _name, set: val => _name = val }); } } const s = new Student("Wango"); console.log(s.name); // Wango s.name = "Tom"; console.log(s.name); // Tom console.log(typeof s._name === "undefined"); // true
1.2 使用setter和getter校驗屬性值
function Student() {
// 直接定義初始值,不由外界輸入,確保安全
let _age = 0;
Object.defineProperty(this, "age", {
get: () => _age,
set: val => {
// 檢查輸入是否是整數
if(!Number.isInteger(val)) {
throw new TypeError("Age should be an Integer");
}
_age = val;
}
});
}
const s = new Student();
// 整數型別通過
s.age = 24
console.log(s.age);
// 24
// 字串型別被攔截
s.age = "25";
// Uncaught TypeError: Age should be an Integer
使用setter還可以跟蹤值的變化,提供效能日誌,提供值發生變化的提示等
1.3 使用getter與setter定義如何計算屬性值
class Student {
constructor() {
// 設定倆個公共屬性
this.firstName;
this.lastName;
}
// 對引數分割並單獨存放
set fullName(name) {
const segment = name.split(" ");
this.firstName = segment[0];
this.lastName = segment[1];
}
// 拼接兩個屬性
get fullName() {
return this.firstName + " " + this.lastName;
}
}
const s = new Student();
s.fullName = "Wango Liu";
console.log(s.firstName);
// Wango
console.log(s.lastName);
// Liu
2. 使用代理控制訪問
const student = {
name: "Wango",
age: 24
}
// 初始化代理物件
// 第一個引數為目標物件
// 第二個引數為一個物件,其中定義了在物件執行特定行為時觸發的函式
const proxy = new Proxy(student, {
// 獲取屬性時檢測是否存在該屬性
get: (target, key) => {
return key in target ? target[key] : "This property do not exist.";
},
set: (target, key, value) => {
// 在這裡可以進行型別判斷、數值追蹤等操作
target[key] = value;
}
});
console.log(proxy.name);
// Wango
console.log(proxy.addr);
// This property do not exist.
proxy.addr = "China";
console.log(proxy.addr);
// China
console.log(student.addr);
// China
物件內部的getter和setter作用於某個屬性,代理作用於整個代理目標
- 代理還有很多其他方法,包括但不限於:
- 呼叫函式時啟用apply
- 使用new操作符時啟用construct
- 讀取/寫入屬性時啟用get/set
- 執行for-in語句時啟用enumerate
2.1 使用代理記錄日誌
// 定義函式為每個引數物件提供代理
function makeLoggable(target) {
// 代理的工作為記錄日誌
return new Proxy(target, {
set: (target, key, value) => {
console.log(`Writing value: ${value} to ${key}`);
target[key] = value;
},
get: (target, key) => {
console.log(`Reading: ${key}`);
return target[key];
}
});
}
let student = {
name: "Wango",
age: 24
}
student = makeLoggable(student);
console.log(student.name);
// Reading: name
// Wango
student.age = 25;
// Writing value: 25 to age
2.2 使用代理檢測效能
function isPrime(num) {
if(num < 2) { return false; }
for(let i = 2; i < num; i++) {
if(num % i === 0) {
return false;
}
}
return true;
}
isPrime = new Proxy(isPrime, {
apply: (target, thisArg, args) => {
// 啟動計時器記錄時間
console.time("isPrime");
const result = target.apply(thisArg, args);
console.timeEnd("isPrime");
// 要記得儲存和返回函式的計算結果
return result;
}
});
isPrime(129982790);
// isPrime: 0.034931640625 ms
2.3 使用代理自動填充屬性
function Address() {
return new Proxy({}, {
get: (target, key) => {
// 如果物件不具有該屬性就建立該屬性
if(!target[key]) {
target[key] = new Address();
}
return target[key];
}
});
}
const addr = new Address();
// 自動建立屬性,不會報錯
addr.Asia.China.Chongqing = "Hot-pot";
console.log(addr.Asia.China.Chongqing);
// Hot-pot
2.4 使用代理實現負陣列索引
function creatNegativeArrayProxy(array) {
// 型別檢測
if(!Array.isArray(array)) {
throw new TypeError("Expected an Array.");
}
return new Proxy(array, {
get: (array, index)=> {
index = +index; // 使用一元操作符將屬性名變為數值
// 如果訪問的是負向索引,則逆向訪問陣列
return array[index < 0 ? array.length + index : index];
},
set: (array, index, value) => {
index = +index;
array[index < 0 ? array.length + index : index] = value;
}
});
}
let arr = [0, 1, 2];
arr = creatNegativeArrayProxy(arr);
// 負向索引可以正常使用
console.log(arr[1]);
// 1
console.log(arr[-1]);
// 2
arr[-1] = -1;
console.log(arr[-1]);
// -1
// 後面就有一些迷惑行為
console.log(arr);
// Proxy {0: 0, 1: 1, 2: -1}
console.log(arr.length);
// undefined
console.log(Array.isArray(arr));
// true
2.5 代理的效能消耗
function creatNegativeArrayProxy(array) {
if(!Array.isArray(array)) {
throw new TypeError("Expected an Array.");
}
return new Proxy(array, {
get: (array, index)=> {
index = +index;
return array[index < 0 ? array.length + index : index];
},
set: (array, index, value) => {
index = +index;
array[index < 0 ? array.length + index : index] = value;
}
});
}
function measure(items) {
const startTime = new Date().getTime();
for(let i = 0; i < 500000; i++) {
items[0] = "Wango";
items[1] = "Lily";
items[2] = "Tom";
}
return new Date().getTime() - startTime;
}
let arr = ["Wango", "Lily", "Tom"];
arrProxy = creatNegativeArrayProxy(arr);
console.log(Math.round(measure(arrProxy) / measure(arr)));
// 49 --> Chrome瀏覽器在50左右
// 42 --> Edge瀏覽器在40左右
// 60-120之間 Firefox瀏覽器 最低值55,最高值124
代理效率不高,在需要執行多次的程式碼中需要謹慎使用