写在最前面,为什么已经入行一年半才写这样一篇文章呢?其实并不是自己平时开发没有使用,但是很多时候自己的代码会有些“四不像”,各种风格糅杂在一起,因此写这么一篇文章,从编程范式的角度规范自己的代码。
面向对象的特性
面向对象就要提到以下的“名词”,那么JavaScript应该如何实现这些呢?
- 类与对象
- 封装性
- 继承
- 多态
- 抽象性
类与对象
基于原型的编程不是面向对象编程中体现的风格,且行为重用(在基于类的语言中也称为继承)是通过装饰它作为原型的现有对象的过程实现的。这种模式也被称为弱类化,原型化,或基于实例的编程。
而在JavaScript中,实现的方式就是通过函数内置的Prototype属性完成这种 原型继承 。
对象
JavaScript内的对象,其实就是Object,那么Object有两种方法可以用来创建:
var Person = {}; // 字面量
var Person = new Object(); // 使用new运算符
那么他们有什么区别呢?首先要说的就是new,new做了哪些呢?这里引用MDN的解释:
- 创建一个空的简单JavaScript对象(即{});
- 链接该对象(即设置该对象的构造函数)到另一个对象 ;
- 将步骤1新创建的对象作为this的上下文 ;
- 如果该函数没有返回对象,则返回this。
不难发现,new的第一步其实和字面量方式完全一致,所以,new运算符的链接与设定this其实是最核心的不同点。
我们一般不常用new Object()
,而直接用字面量方式的原因,其实是因为他们的prototype都指向了null。
正是因为这样,我们在日常的使用中,如果希望通过OOP实现逻辑,应该通过new的方式来进行,因为他可以继承我们期望的对象。
代码风格
JavaScript有很多的引用类型数据,因此在面向对象时,要注意分离自有的部分与暴露的方法。
function Person (name){
this.name = name;
}
Person.prototype.sayIntro = function() {
return 'Hello, my name is ' + name + '.'
}
var person1 = new Person('peter'); // Person是一个构造器。
person1.sayIntro(); // 为什么可以调用prototype上的方法呢?参见“原型链”
Object.create()
Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。
Object.create()是ES2015规范内的新方法,同时在ES5.1进行了加强,它可以polyfill使得我们可以在生产中尽可能使用来让我们的代码更为清晰。
其内部核心的实现其实在JS高程有对应的代码:
// 在自己的实现里要注意当前环境是否支持create,以及目标是否可以作为create的对象。
Object.create = function (proto, propertiesObject) {
function F() {}
F.prototype = proto;
return new F();
};
那么如果使用Object.create
应该如何实现继承呢?下面是MDN的一个例子:
// Shape - 父类(superclass)
function Shape() {
this.x = 0;
this.y = 0;
}
// 父类的方法
Shape.prototype.move = function(x, y) {
this.x += x;
this.y += y;
console.info('Shape moved.');
};
// Rectangle - 子类(subclass)
function Rectangle() {
Shape.call(this); // call super constructor.
}
// 子类续承父类
Rectangle.prototype = Object.create(Shape.prototype); // create的目标是父类的prototype,注意,JS的继承是原型继承。
Rectangle.prototype.constructor = Rectangle; // 修复constructor指向
上面是一个单继承的例子,如果我们希望做多继承,其实只需要修改Rectangle.prototype,让其的赋值为其多继承的对象的prototype的合并(Object.assign)。需要注意需要在子类内部显式绑定this。
类
ES6最大的变化就是对Class的支持,虽然JS只能通过原型继承,但是Class的出现使得JS的OOP“看起来更像那么回事了”。
为什么说看起来呢?目前还没有实现私有与公有特性,它只是原来构造模式的一个语法糖,但是,它也有它的不同之处。
- Class内部只是原型的实现,在实际使用时还是需要通过new运算符执行构造函数;
- 类声明不能被提升;
- 类声明中的代码自动运行在严格模式下;
- 类中的所有方法都是不可枚举的 ;
- 每个类都有一个
[[construct]]
的方法,constructor方法默认返回实例对象(即this);
派生类
class HelloWorld extends React.Component {}
上面这种写法在React的开发中非常常见,extends是一个派生类, 在派生类中,如果使用了构造方法,就必须使用super() ,在构造函数中访问this之前要调用super(), 负责初始化this 。
class Shape {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class Rectangle extends Shape {
constructor() {
super(); // 这里的super可以作为函数,也可以作为特殊的对象(无法直接输出,但是可以设置属性)。
}
}
继承
见上面对“类与对象”内继承的实现,核心在于prototype 原型继承模式 。
抽象
抽象是允许模拟工作问题中通用部分的一种机制。这可以通过继承(具体化)或组合来实现。 JavaScript通过继承实现具体化,通过让类的实例是其他对象的属性值来实现组合。
JavaScript Function 类继承自Object类(这是典型的具体化) 。Function.prototype的属性是一个Object实例(这是典型的组合)。
多态
其实多态并没有太多好说的,核心在于“原型链”:如果你在子类声明了同名的属性和方法,那么就不会在原型链上向上查找。