Access Control over JavaScript Objects
Contents
getter, setter
用闭包实现“私有”变量时用到了 getter 和 setter 的概念,但使用起来比较繁琐。
JavaScript 支持两种方式来定义 getter 和 setter:
- 对象字面量或 ES6 的
class
- 用这种方式定义的 getter 和 setter 与需要做访问控制的变量不在同一个作用域,因此无法通过闭包实现访问控制。
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 举例如下:
apply
在方法调用时触发,construct
在使用new
操作符时触发。get
和set
在读写属性时触发。enumerate
在使用for-in
时触发。getPrototypeOf
和setPrototypeOf
在读、写原型时触发。
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy
不能拦截(逻辑不能交由用户自定义)的操作包括:==
,===
,instanceof
,typeof
。
以相等操作符为例,x == y
检查 x
和 y
是否指向同一个对象,x === y
比较两个对象的值是否相等。相等操作符有一些假定,比如比较两个对象的结果应该总是一致的,若相等操作符也可以被截获,即判断相等的逻辑由用户提供,此时无法保证比较的结果总是一致,不满足相等操作符的假定。除此之外,比较两个对象是否相等的操作也不应由被比较的对象来进行。
instanceof
和 typeof
不能被拦截的原因也类似。
日志功能:
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 会对性能产生较大影响。