getter, setter

用闭包实现“私有”变量时用到了 getter 和 setter 的概念,但使用起来比较繁琐。

JavaScript 支持两种方式来定义 getter 和 setter:

  1. 对象字面量或 ES6 的 class
    • 用这种方式定义的 getter 和 setter 与需要做访问控制的变量不在同一个作用域,因此无法通过闭包实现访问控制。
  2. Object.defineProperty
    • 可以实现对对象属性的访问控制。

getter 和 setter 让我们用正常的方式访问对象属性,同时支持在访问的过程中插入额外逻辑

getter 和 setter 通常用来控制对对象私有属性的访问。

一对 getter 和 setter 只能控制对象的一个属性。

若一个属性只定义了 getter 而没有定义 setter,此时若对该属性进行赋值,非严格模式下赋值操作会被无视,严格模式下会报类型错误。

const coderCollection = {
    coders: ["Linus", "Richard"],
    get firstCoder() {
        console.log("getting first coder")
        return this.coders[0]
    },
    set firstCoder(value) {
        console.log("setting first coder")
        this.coders[0] = value
    }
}
console.log(coderCollection.firstCoder) // "Linus"
coderCollection.firstCoder = "John"
console.log(coderCollection.firstCoder) // "John"

class CoderCollection2 {
    constructor() {
        this.coders = ["Linus", "Richard"]
    }
    get firstCoder() {
        console.log("getting first coder")
        return this.coders[0]
    }
    set firstCoder(value) {
        console.log("setting first coder")
        this.coders[0] = value
    } 
}
const coderCollection2 = new CoderCollection2()
console.log(coderCollection2.firstCoder) // "Linus"
coderCollection2.firstCoder = "John"
console.log(coderCollection2.firstCoder) // "John"


function Person() {
    let _level = 0
    Object.defineProperty(this, "level", {
        get: () => {
            console.log("get is called")
            return _level // 闭包
        },
        set: value => {
            console.log("set is called")
            _level = value
        }
    })
}
const person = new Person()
console.log(typeof _level, person.level) // undefined 0
person.level = 6
console.log(person.level) // 6

用 setter 校验属性值:

function Person() {
    let _level = 0
    Object.defineProperty(this, "level", {
        get: () => _level,
        set: value => {
            if (!Number.isInteger(value)) {
                throw new TypeError("must be a number")
            }
            _level = value
        }
    })
}
const person = new Person()
try {
    person.level = "great"
} catch(e) {
    console.log(e)
}

用 setter 定义计算而来的属性(computed property):

const name = {
    firstName: "sb",
    lastName: "ho",
    get fullName() {
        return `${this.firstName} ${this.lastName}`
    },
    set fullName(value) {
        const segments = value.split(" ")
        this.firstName = segments[0]
        this.lastName = segments[1]
    }
}
console.log(name.fullName) // sb ho
name.fullName = "Slack Buffer"
console.log(name) // {firstName: "Slack", lastName: "Buffer"}

proxy

ES6 引入了 proxy 类型的对象,可以把对整个对象(包括方法调用)的访问控制交由 proxy 对象代理。

内置的 Proxy 构造器可以用来创建代理。

const person = { name: "sb" }
// get 和 set 称为陷阱 trap
const proxy = new Proxy(person, {
    get: (target, key) => {
        console.log(`reading ${key} through a proxy`)
        return key in target ? target[key] : `${key} not existed`
    },
    set: (target, key, value) => {
        console.log(`writing ${key} through a proxy`)
        target[key] = value
    }
})
console.log(person.name, proxy.name) // sb sb
console.log(person.nickname, proxy.nickname) // undefined "nickname not existed"
proxy.nickname = "joker"

console.log(person.nickname, proxy.nickname) // joker joker

proxy 可以拦截许多操作。内置 trap 举例如下:

  1. apply 在方法调用时触发,construct 在使用 new 操作符时触发。
  2. getset 在读写属性时触发。
  3. enumerate 在使用 for-in 时触发。
  4. getPrototypeOfsetPrototypeOf 在读、写原型时触发。

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy

不能拦截(逻辑不能交由用户自定义)的操作包括:=====instanceoftypeof
以相等操作符为例,x == y 检查 xy 是否指向同一个对象,x === y 比较两个对象的值是否相等。相等操作符有一些假定,比如比较两个对象的结果应该总是一致的,若相等操作符也可以被截获,即判断相等的逻辑由用户提供,此时无法保证比较的结果总是一致,不满足相等操作符的假定。除此之外,比较两个对象是否相等的操作也不应由被比较的对象来进行。
instanceoftypeof 不能被拦截的原因也类似。

日志功能:

function makeLoggable(target) {
    return new Proxy(target, {
        get: (target, property) => {
            // log...
            return target[property]
        },
        set: (target, property, value) => {
            // log...
            target[property] = value
        }
    })
}
let person = { name: "sb" }
person = makeLoggable(person)

自动填充对象属性:

function Folder() { // 显式返回对象的构造函数
    return new Proxy({}, {
        get: (target, property) => {
            console.log(`reading ${property}`)
            if (!(property in target)) {
                target[property] = new Folder()
            }
            return target[property]
        }
    })
}
const rootFolder = new Folder()
try {
    // .js 不是 get 而是 set
    rootFolder.codebase.firstFolder.js = "notes.md"
    // rootFolder.js = "notes.md"
    console.log("s'all good, man")
} catch(e) {
    console.log(e)
}

测试性能:

function isPrime(number) {
    if (number < 2) { return false }
    for (let i = 2; i < number; i++) {
        if (number % 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(1299827)

数组负下标:

// https://github.com/sindresorhus/negative-array
function createNegativeArrayProxy(array) {
    if (!Array.isArray(array)) {
        throw new TypeError("expected an array")
    }
    return new Proxy(array, {
        get: (target, index) => {
            // console.log(typeof index) // string
            index = +index
            return target[index < 0 ? target.length + index : index]
        },
        set: (target, index, val) => {
            index = +index
            return target[index < 0 ? target.length + index : index] = val
        }
    })
}
const arr = ["a", "b", "c"]
const proxiedArr = createNegativeArrayProxy(arr)
console.log(arr[-1], proxiedArr[-1]) // undefined c

function measure(items) {
    const startTime = new Date().getTime()
    for (let i = 0; i < 500000; i++) {
        items[0] === "a"
        items[1] === "b"
        items[2] === "c"
    }
    return new Date().getTime() - startTime
}
console.log(`Proxies are around ${Math.round(measure(proxiedArr) / measure(arr))} times slower`) // 29 (chrome), 56 (firefox)

由于多了一层代理,proxy 会对性能产生较大影响