1. 程式人生 > 實用技巧 >第8章 控制物件的訪問(setter、getter、proxy)

第8章 控制物件的訪問(setter、getter、proxy)

目錄

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

代理效率不高,在需要執行多次的程式碼中需要謹慎使用