和很多高级语言不同,JavaScript 中没有 public
、private
、protected
这些访问修饰符(access modifiers),而且长期以来也没有私有属性这个概念,对象的属性/方法默认都是public的。虽然目前 class 的私有属性特性已经进入了 Stage3 实验阶段(Spec),通过 Babel 已经可以使用,并且 Node v12 中也增加了对私有属性的支持,但这并不妨碍我们用 JS 的现有功能实现一个私有属性特性,以加深对这一概念的理解。
私有属性(方法)的意义在于将模块的内部实现隐藏起来,而对外接口只通过public成员进行暴露,以减少其他模块对该模块内部实现的依赖或修改,降低模块的维护成本。
IIFE 实现
IIFE(立即执行函数) 大家应该耳熟能详了,IIFE 经常被用来:
- 定义一个自执行的匿名函数
- 创建一个局部作用域,避免对全局产生污染
基于以上特性,用 IIFE 可以给一个对象实现简单的私有属性:
let person = (function () {
// 私有属性
let _name = "bruce";
return {
age: 30,
// getter
get name() {
return _name;
},
// setter
set name(val) {
_name = val;
},
greet: function () {
console.log(`hi, i'm ${_name} and i'm ${this.age} years old`);
}
};
})();
测试一下:
console.log(person.name); // 'bruce'
console.log(person._name); // undefined
person.name = "frank";
console.log(person.name); // 'frank'
console.log(Object.keys(person)); // ['age', 'name']
person.greet(); // hi, i'm frank and i'm 30 years old
IIFE 的实现简单易懂,但是只能作用于单个对象,而不能给 Class 或者构造函数定义私有属性。
构造函数实现
利用在构造函数中创建的局部变量可以作为 “私有属性” 使用:
function Person(name, age) {
// 私有属性
let _name = name;
this.age = age;
this.setName = function (name) {
_name = name;
};
this.getName = function () {
return _name;
};
}
Person.prototype.greet = function (){
console.log(`hi, i'm ${this.getName()} and i'm ${this.age} years old`);
}
测试一下:
const person = new Person("bruce", 30);
console.log(person.getName()); // bruce
person.setName('frank');
console.log(person.getName()); // frank
person.greet(); // hi, i'm frank and i'm 30 years old
看起来还行,但是该实现方式需要在构造函数中定义 getter
、setter
方法,这两个方法是绑定在实例上而不是原型上的,如果私有属性增加会导致实例方法暴增,对内存不太友好。
Class实现
Class中实现和构造函数类似,因为JavaScript中的class本质上是构造函数和原型的语法糖,实现如下:
class Person {
constructor(name, age) {
// 私有属性
let _name = name;
this.age = age;
this.setName = function (name) {
_name = name;
};
this.getName = function () {
return _name;
};
}
greet() {
console.log(`hi, i'm ${this.getName()} and i'm ${this.age} years old`);
}
}
Class中的实现也会存在和构造函数中一样的问题,而且在 greet()
方法中无法访问 _name
,需要通过调用 getter
方法。这和一般意义上的私有属性还是有差别的,真正的私有属性在class内部应该是可以正常访问的,而不仅仅是在构造函数内部可以访问。
原生实现
以上三种实现或多或少都有一些问题,还好在ES2019中已经增加了对 class 私有属性的原生支持,只需要在属性/方法名前面加上 '#'
就可以将其定义为私有,并且支持定义私有的 static
属性/方法。例如:
class Person {
// 私有属性
#name;
constructor(name, age) {
this.#name = name;
this.age = age;
}
greet() {
console.log(`hi, i'm ${this.#name} and i'm ${this.age} years old`);
}
}
测试一下:
const person = new Person("bruce", 30);
console.log(person.name); // undefine
person.greet(); // hi, i'm bruce and i'm 30 years old
更多语法可以参考 MDN: Private class field。
我们可以去babel里面将原生的代码转换一下,看看babel的polyfill是怎么实现的:
发现主要思路居然使用 WeakMap
。。好吧,还是太年轻。格式化后的polyfill代码贴在下面,有兴趣的同学可以研究一下:
"use strict";
function _classPrivateFieldGet(receiver, privateMap) {
var descriptor = privateMap.get(receiver);
if (!descriptor) {
throw new TypeError("attempted to get private field on non-instance");
}
if (descriptor.get) {
return descriptor.get.call(receiver);
}
return descriptor.value;
}
function _classPrivateFieldSet(receiver, privateMap, value) {
var descriptor = privateMap.get(receiver);
if (!descriptor) {
throw new TypeError("attempted to set private field on non-instance");
}
if (descriptor.set) {
descriptor.set.call(receiver, value);
} else {
if (!descriptor.writable) {
throw new TypeError("attempted to set read only private field");
}
descriptor.value = value;
}
return value;
}
var _name = new WeakMap();
class Person {
constructor(name, age) {
_name.set(this, {
writable: true,
value: void 0,
});
_classPrivateFieldSet(this, _name, name);
this.age = age;
}
greet() {
console.log(
"hi, i'm "
.concat(_classPrivateFieldGet(this, _name), " and i'm ")
.concat(this.age, " years old")
);
}
}
每天一个小技巧(Tricks by Day),量变引起质变,希望你和我一起每天多学一点,让技术有趣一点。所有示例将会汇总到我的 tricks-by-day github 项目中,欢迎大家莅临指导