JavaScript继承与模块

7 3月

JavaScript作为一门语法比较松散的语言,在ES6之前并没有像C++/Java等传统OO语言一样有class关键字,也不能通过private,public等关键字来限定权限。本篇就介绍一下JavaScript是如何实现继承的。

JavaScript的继承可以分为两类:

  • 基于对象的继承
  • 基于类型的继承
  • 模块(保护隐私)

基于对象的继承

基于对象的继承也叫原型继承。我们知道通过JavaScript字面量创建的对象都会连接到Object.prototype,因此我们用Object.prototype来实现继承。本质上是摒弃类,不调用构造函数,是一种行为委托的方式,用Object.create()直接让新对象继承旧对象的属性。例如:

var foo = {
    a: 42
};

var bar = Object.create(foo);
bar.b = "hello world";

bar.b;    // hello world
bar.a;    // 42

本质上是将bar的prototype指向foo:

Object.create()还可以指定第二个参数,即数据属性,将其添加到新对象中。数据属性可设4个描述符value, writable,enumerable,configurable 。后3个看名字也能猜出意思,不指定的话默认为false。因为和本篇关系不大,就不跑题了,只看看设置value的情况:

var person = {
    name: "Jack",
    getName: function () { return this.name; }
}
var p1 = Object.create(person);
p1.getName();    //Jack

var p2 = Object.create(person, {
    name: {
        value: "Zhang"
    }
});
p2.getName();    //Zhang

从结果上看,用Object.create()相当于创建了一个全新的对象,你可以给该对象任意新增,重载它的属性和方法:

var person = {
    name: "Jack",
    getName: function () { return this.name; },
    getAge: function() { return this.age; }     //注意并没有age这个成员变量,依赖子类实现
}

var p3 = Object.create(person);
p3.name = 'Rose';
p3.age = 17;
p3.location = '上海';
p3.getLocation = function() { return this.location; }  // 新增一个全新的方法

console.log(person.getName());       // Jack
console.log(person.getAge());        // undefined,因为没有 age 属性
console.log(person.getLocation());   // 直接挂掉,因为饼没有这个方法

console.log(p3.getName());        // Rose
console.log(p3.getAge());         // 17
console.log(p3.getLocation());    // 上海

delete p3.name;
console.log(person.getName());    // Jack
console.log(p3.getName());        // 删除对象的属性和方法后,会访问prototype上同名的属性和方法

本质上对新对象操作的的属性和方法都在新对象上,但prototype上的属性和方法都还在,只不过不可见了。只有你delete新对象上的属性和方法后,才会访问prototype上同名的属性和方法。

基于类型的继承

基于类型的继承是通过构造函数依赖于原型的继承,而非依赖于对象。例如:

function Person(name) {
    this.name = name;
    this.getName = function () { return this.name; };  
}
function Student(name, age) {
    Person.call(this, name);
    this.age = age;
    this.getAge = function () { return this.age; }; 
}
Student.prototype = new Person();    //需要通过new来访问基类的构造函数

var p = new Person('Cathy');
var s = new Student('Bill', 23);

console.log(p.getName());    //Cathy
console.log(s.getName());    //Bill
console.log(s.getAge());     //23

Student继承自Person。name虽然是在基类Person里被定义的,但用new调用Person的构造函数后,this将被绑定到子类Student对象上,因此name最终是定义在子类Student对象上的。结果如上所示,不赘述。

模块(保护隐私)

之所以定义getName,getAge等方法就是不想让用户直接访问name,age等属性。可惜上面两种继承均无法保护隐私,均可通过p.name,p.age这样直接访问属性。如果认为这些属性的隐私非常重要,希望模拟出OO语言中private属性的效果,可以用模块。

模块本质上用了闭包的能力。在模块文件内部的内容被视为像是包围在一个作用域闭包中。

代码层面就是在函数内新建一个对象,将属性都挂到新对象上,最终return这个新对象:

var person = function(spec) {
    var obj = {};        // 新对象
    obj.getName = function () { return spec.name; };  // 挂到新对象上
    obj.getAge = function() { return spec.age; };     // 挂到新对象上
    return obj;          // 返回新对象
}

// 另一种更通用的写法:
var person = function(spec) {
    function getName() { return spec.name; };
    function getAge() { return spec.age; };

    return {
    	getName,
    	getAge
    };
}

var p4 = person({name: 'Jack', age: 10});

console.log(p4.name);         //undefined
console.log(p4.age);          //undefined
console.log(p4.getName());    //Jane
console.log(p4.getAge());     //10

因为函数person返回的是新对象,新对象里并没有name和age属性,因此直接访问会得到undefined。只能通过新对象暴露出的两个接口来获取name和age。

进一步实现多层继承也非常方便,效果如下,不赘述:

var student = function(spec) {
    var obj = person(spec);
    obj.getRole = function() { return 'student'; };  // 新对象上增加方法
    obj.getInfo = function() {
        return spec.name + ' ' + spec.age + ' ' + obj.getRole();
    };
    return obj;    //返回新对象
};

var p5 = student({name:'Andy', age:12});

console.log(p5.name);       //undefined
console.log(p5.getName());  //Andy
console.log(p5.getRole());  //student
console.log(p5.getInfo());  //Andy 12 student

发表评论

电子邮件地址不会被公开。 必填项已用*标注