
我们都知道,在 js 中是没有私有属性、私有方法这个概念的。
一般来说,当我们不想让模块或类中的某些属性或方法被调用时,会通过在它们的属性名前加上 _
来表示这是一个私有属性,使用者不应该调用。然而这个方法只是在语义上告诉你是私有的,实际上依然可以使用。
当然也可以通过立即执行函数创建一个函数内的作用域,这样函数外就无法访问到内部的属性和方法等,而需要暴露出来的属性和方法通过 return
的方式返回,这样就可以在外部使用。但是这种方法比较适合模块化的编程,而对于类来说并不合适。
好在 ES6 为我们提供了一个新的数据类型:Symbol,通过 Symbol
我们可以模拟实现私有属性和方法。当然,模拟并不是真正的具有,只是通过正常的方法无法使用到而已。下面先一起来了解下这个新的数据类型到底是做什么用的。
Symbol 是什么?
Symbol 是 ES6 中新增的一种数据类型,它和 Number Boolean String Array Object Null Undefined 一样,是一种基本数据类型。
Symbol 就和它的名字一样,它的作用就是标识符,并且在当前浏览器环境下,不会存在两个相同的 Symbol,这就保证了每一个 Symbol 值的唯一性。
生成一个 Symbol
// Symbol 是一种基本数据类型,所以不需要 new 关键字
const s1 = Symbol()
console.log(s1)
// Symbol()
// Symbol 的生成函数可以接受一个参数(任何类型)作为当前 Symbol 值的标识,方便 debug
const s2 = Symbol('apple')
console.log(s2)
// Symbol(apple)
// 即便两个 Symbol 的标识相同,它们的值也是不同的
const s3 = Symbol('test')
const s4 = Symbol('test')
s3 == s4
// false
然而某些情况下,我们确实需要能够通过传入的参数标识来创建出两个相同的 Symbol 时该怎么办呢?Symbol 也为我们提供了这种情况下的方法:
// 通过 Symbol.for() 方法创建的 Symbol,在第一次创建时会在全局环境下注册这个 Symbol 值
// 当之后再次创建时,如果已经注册过了,就会直接返回这个值
const s5 = Symbol.for('pear')
const s6 = Symbol.for('pear')
s5 === s6 // true
// 不通过 Symbol.for() 创建的 Symbol 不会被注册到全局,
const s7 = Symbol('pear')
s5 == s7 // false
Symbol 的用处
我们知道,对象的属性名是一个字符串。当我们需要为一个对象或者模块扩展方法时,往往需要先判断这个它本身是不是已经有了这个同名方法,否则直接扩展上去很可能会覆盖掉原有的方法。
在实际使用中,Symbol 在某些情况下可以当做字符串来使用,比如上面情况中,作为对象的属性名。由于 Symbol 的唯一性特性,使得我们不需要再考虑是否会覆盖原有方法,而可以直接扩展:
const module1 = {
func1: function () {}
}
const s8 = Symbol('module1Func2')
module1[s8] = function () {}
但是通过 Symbol 作为属性名扩展的属性,并不能够直接通过点语法的方式来调用,因为点语法实际上是将点后面的属性名转为字符串后在对象中查找的。如果需要获取 Symbol 属性名的属性,应该通过 []
的方式:
module1.s8 // undefined
module1[[s8] // f () {}
下面试着遍历 module1
上面的属性:
for (let k in module1) {
console.log(k)
}
// "func1"
会发现遍历属性的结果中,并不存在我们后来扩展上去的 s8
,如果再试着通过 Object.keys(module1)
来获取属性名,也发现它返回的结果是 [“func1”]
,也没有 s8
。然而 s8
却是实际存在的。这是为什么呢?
因为通过 Symbol 作为属性名的属性,在常规的属性遍历和获取方法中,并不能够查询到。
这就为我们模拟私有属性和方法提供了一个很好的方式,因为在使用者不看内部代码的情况下,通过正常方法是无法获取和使用到这些属性的。关于这一点,下面再说,先看看该如何获取到对象上的 Symbol 值属性名:
// 通过 Object.getOwnPropertySymbols() 方法能够将对象中的所有 Symbol 值属性名作为一个数组返回
Object.getOwnPropertySymbols(module1)
// [Symbol(module1Func2)]
Symbol 除了作为对象的属性以外,还能用在需要唯一性但是对值没要求的情况中,如作为判断条件出现:
const fruit = s2
if (fruit === s2) {
console.log('apple')
} else if (fruit === s7) {
console.log('pear')
} else {
console.log('other fruit')
}
上面这个例子中,fruit 的值是什么并不重要,只要能够达到我们判断的目的就可以了。
如何通过 Symbol 模拟私有属性和方法?
class Fruit {
constructor () {
const number = Symbol('number')
class F {
constructor () {
this[number] = 1
}
getNumber () {
return this[number]
}
setNumber (num) {
this[number] = num
}
}
return new F()
}
}
const apple = new Fruit()
apple.getNumber() // 1
apple.setNumber(5)
apple.getNumber() // 5
apple[number] // Uncaught ReferenceError: number is not defined
在上面的代码中,可以通过 getNumber
和 setNumber
来访问和修改属性 number
的值,但是却不能直接通过 apple[number]
的方式来访问和修改,这样就实现了私有属性的模拟实现。
对于私有方法,也可以采取类似的方式来实现。然而这并不代表完全无法访问 number
,将对象中的所有 Symbol 值属性通过 Object.getOwnPropertySymbols()
遍历出来后,依然可以获取到,只是大部分情况下并不会刻意这样去做。
当浏览器不支持 Symbol 时,能否模拟 Symbol 的实现?
除了文章开头提到的方式,当不支持 Symbol 时,还有没有方法可以模拟呢?在模拟之前,我们先来考虑一下为什么要私有:私有是为了让使用者无法调用——而通常我们在写一个对象时为了语义化,方法名都会很明了,看一眼就知道是做什么的。如果我们把方法名改成不具有实际意义的随机数呢?这样使用者即使看到了这个方法也不知道它的作用是什么,更不会去调用这样一个方法(毕竟随机数的方法名不好写,也很容易写错)。这样也就曲线模拟了私有方法。
由于随机数几乎可以保证不出现重复(并不是100%,需要对随机数的生成算法加以修改来达到100%),那么将这个生成随机数方法名的方法封装一下,就能在不存在 Symbol 的浏览器中,模拟一个 Symbol。
上面这个思路是作者在写微信小游戏的时候看到的,小游戏的模板框架中自己就带有了这样一个模块,下面将这个模块的代码贴出来,需要的小伙伴可以直接拿去使用~这个模块中对随机数的算法做了处理,从而保证实现了 Symbol 的唯一性特性。
/**
* 对于ES6中Symbol的极简兼容
* 方便模拟私有变量
*/
let Symbol = window.Symbol
let idCounter = 0
if (!Symbol) {
Symbol = function Symbol(key) {
return `__${key}_${Math.floor(Math.random() * 1e9)}_${++idCounter}__`
}
Symbol.iterator = Symbol('Symbol.iterator')
}
window.Symbol = Symbol


网友评论