最近一个月再做一个开源 javascript data structure 库(js-sdsl),感兴趣可以看这篇文章
由于目标是兼容 es5,但是库中需要编写类,包括类的私有、保护成员,但是 es5 中并没有提供相关的语法,使用 es6 中的 class 语法编译成 es5 后私有成员仍可直接访问,于是我想探寻一种兼容 es5 的继承方式
官方继承方法
先看看官方推荐的继承方式
function foo(){}
foo.prototype = {
foo_prop: "foo val"
};
function bar(){}
var proto = new foo;
proto.bar_prop = "bar val";
bar.prototype = proto;
var inst = new bar;
console.log(inst.foo_prop);
console.log(inst.bar_prop);
这种方式在不需要私有成员时表现非常良好,但是并不能满足要求
Object.defineProperty 法
为了一步步探索,首先我们创建一个函数 people,并 new 一个对象 p
function People(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
this.print = function () {
console.log(`name = ${this.name}, age = ${this.age}, sex = ${this.sex}`);
}
}
const p = new People('p', 18, 'F');
p.print(); // name = p, age = 18, sex = F
然后我们为 People 类创建一个属性 income,显然你不想让别人知道你的收入,那么这个属性应当时私有的(
在 js 中想隐藏对象的属性可以使用 Object.defineProperty 的 enumerable 属性
-
enumerable当且仅当该属性的
enumerable键值为true时,该属性才会出现在对象的枚举属性中。
默认为false。
尝试使用 enumerable = false 对其进行隐藏
function People(name, age, sex, income) {
this.name = name;
this.age = age;
this.sex = sex;
Object.defineProperty(this, 'income', {
value: income,
});
this.print = function () {
console.log(`name = ${this.name}, age = ${this.age}, sex = ${this.sex}`);
}
this.getIncome = function () {
return this.income;
}
}
const p = new People('p', 18, 'F', 100);
p.print(); // name = p, age = 18, sex = F
console.log(p.income); // 100
console.log(p.getIncome()); // 100
很不幸,我们仍然能够访问它,甚至当 writable 为 true 时可以进行改写
闭包法
由于 js 中万物皆为对象,所以我们不能将私有属性写入到 this 中,考虑借用闭包的方式在函数中定义一个变量,在成员函数中调用
function People(name, age, sex, _income) {
let income = _income;
this.name = name;
this.age = age;
this.sex = sex;
this.print = function () {
console.log(`name = ${this.name}, age = ${this.age}, sex = ${this.sex}`);
}
this.getIncome = function () {
return income;
}
}
const p = new People('p', 18, 'F', 100);
p.print(); // name = p, age = 18, sex = F
console.log(p.income); // undefined
console.log(p.getIncome()); // 100
Oooops!
在这种情况下我们不仅无法直接读取,也不能直接修改,达到了私有的要求,但是这种方式如何对保护成员进行继承呢
闭包法继承
假设我们新增了一个 Programmer 类,其要继承 People 的相关属性,很直观的,我们可以使用 call 方法初始化
function People(name, age, sex, _income) {
let income = _income;
this.name = name;
this.age = age;
this.sex = sex;
this.print = function () {
console.log(`name = ${this.name}, age = ${this.age}, sex = ${this.sex}`);
}
this.getIncome = function () {
return income;
}
}
function Programmer(name, age, sex, _income) {
People.call(this, name, age, sex, _income);
}
const p = new People('p', 18, 'F', 100);
p.print(); // name = p, age = 18, sex = F
console.log(p.income); // undefined
console.log(p.getIncome()); // 100
const pro = new Programmer('p', 18, 'F', 100);
pro.print(); // name = p, age = 18, sex = F
console.log(pro.getIncome()); // 100
但是很显然,我们无法在 Programmer 中拿到 income 属性,比如当前我们需要给 pro 涨工资,新增 addIncome 函数,我们只能把它添加到 People 中,但是这违背了类的设计理念,应为现在 People 并不需要该函数
思考了许久,我终于想到一个办法
由于 js 中的类是一个函数对象,考虑将私有属性放到函数的返回值中即可在 Programmer 中获取
function People(name, age, sex, income) {
const self = {
income
}
this.name = name;
this.age = age;
this.sex = sex;
this.print = function () {
console.log(`name = ${this.name}, age = ${this.age}, sex = ${this.sex}`);
}
this.getIncome = function () {
return self.income;
}
return self;
}
function Programmer(name, age, sex, _income) {
const self = People.call(this, name, age, sex, _income);
this.addIncome = function (num) {
self.income += num;
}
}
const pro = new Programmer('p', 18, 'F', 100);
pro.print(); // name = p, age = 18, sex = F
console.log(pro.getIncome()); // 100
pro.addIncome(100);
console.log(pro.getIncome()); // 200
我们看上去好像成功了!pro 成功拿到了父类的私有属性,并且可以进行修改
但是我们发现创建 People 对象时出现了错误
p.print();
^
TypeError: p.print is not a function
众所周知,js 中的 new 运算符会查看函数有没有返回值,若有,则会将返回值代替 this 为对象赋值,这时候我们需要判断当前操作是 new 还是 call
function People(name, age, sex, income) {
const self = {
income
}
this.name = name;
this.age = age;
this.sex = sex;
this.print = function () {
console.log(`name = ${this.name}, age = ${this.age}, sex = ${this.sex}`);
}
this.getIncome = function () {
return self.income;
}
if (!new.target) return self;
}
function Programmer(name, age, sex, _income) {
const self = People.call(this, name, age, sex, _income);
this.addIncome = function (num) {
self.income += num;
}
if (!new.target) return self;
}
const pro = new Programmer('p', 18, 'F', 100);
pro.print(); // name = p, age = 18, sex = F
console.log(pro.getIncome()); // 100
pro.addIncome(100);
console.log(pro.getIncome()); // 200
const p = new People('p', 18, 'F', 100);
p.print(); // name = p, age = 18, sex = F
console.log(p.income); // undefined
console.log(p.getIncome()); // 100
这下,我们达到了私有继承的目的,但是不要高兴的太早,我们并没有处理父类原型上的属性
注意,在此方法下一般不需要将函数写到原型链上,因为原型链本就是在官方继承方法下才使用的
如果您要使用 instanceof 判断子类,我们推荐以下解决方案
function People(name, age, sex, income) {
const self = {
income
}
this.name = name;
this.age = age;
this.sex = sex;
this.print = function () {
console.log(`name = ${this.name}, age = ${this.age}, sex = ${this.sex}`);
}
this.getIncome = function () {
return self.income;
}
if (!new.target) return self;
}
People.prototype.type = "People";
function Programmer(name, age, sex, _income) {
const self = People.call(this, name, age, sex, _income);
this.addIncome = function (num) {
self.income += num;
}
if (!new.target) return self;
}
Programmer.prototype = Object.create(People.prototype);
Programmer.prototype.constructor = Programmer;
const pro = new Programmer('p', 18, 'F', 100);
pro.print(); // name = p, age = 18, sex = F
console.log(pro.getIncome()); // 100
pro.addIncome(100);
console.log(pro.getIncome()); // 200
console.log(pro.type); // People
console.log(pro.constructor); // [Function: Programmer]
const p = new People('p', 18, 'F', 100);
p.print(); // name = p, age = 18, sex = F
console.log(p.income); // undefined
console.log(p.getIncome()); // 100
console.log(p.type); // People
console.log(p.constructor); // [Function: People]
console.log(pro instanceof People); // true
至此,我们优雅的在 es5 中实现了类的私有、保护、公有成员以及类的继承
总结
-
私有成员
采用闭包的形式写在函数中,仅供内部调用
-
保护成员
将保护成员写在
self对象中作为返回值返回,注意区分new和call -
公有成员
写在
this指针上 -
继承
在父类中返回
self,在子类中使用FatherClass.call(this)接收