javascript 在 es5 中实现私有成员、保护成员、公有成员以及类的继承

385 阅读3分钟

最近一个月再做一个开源 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.definePropertyenumerable 属性

MDN 上的属性描述如下

  • 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 中实现了类的私有、保护、公有成员以及类的继承

总结

  1. 私有成员

    采用闭包的形式写在函数中,仅供内部调用

  2. 保护成员

    将保护成员写在 self 对象中作为返回值返回,注意区分 newcall

  3. 公有成员

    写在 this 指针上

  4. 继承

    在父类中返回 self,在子类中使用 FatherClass.call(this) 接收