ECMAScript is an object-oriented programming language for performing computations and manipulating computational objects within a host environment.
ECMAScript 是一个面向对象的编程语言,用于对宿主环境中的对象进行计算与交互。
——ECMAScript 2015 Language Specification #4 Overview
注:JavaScript 是 ECMAScript 的俗名。
Java 是一门高度面向对象的编程语言,JavaScript 也是面向对象的编程语言。
C++ 是一门多编程范式的编程语言,文本考虑它的面向对象编程范式。
本文介绍一个与C++,Java的面向对象编程范式接近的JavaScript子集。
本文假设读者熟悉C++ OOP或Java,初学JavaScript。
综述
在 C++/Java 中,均使用class
关键字来修饰。
在 JavaScript 中,类的本质是一个函数,是基于原型继承的,这在本质上与C++/Java 有所不同。
Instead objects may be created in various ways including via a literal notation or via constructors which create objects and then execute code that initializes all or part of them by assigning initial values to their properties. Each constructor is a function that has a property named “prototype” that is used to implement prototype-based inheritance and shared properties.
相反(注:与C++, Java 相反),对象可能被多种方式创建,包括通过字面量或者通过构造器先创建对象然后执行代码赋初始值给那些对象的属性以此来初始化对象。每个构造器都是一个函数,具有一个属性名为”prototype”(原型)用于实现基于原型的继承与共享属性。
ECMAScript 2015 Language Specification #4.2.1 Objects
类的声明
// ES6
class A {}
// Equivalent in ES5 and below
function A(){};
ES6 开始启用 class 修饰符来声明类,并强制使用 new 来实例对象(同Java)。
C++ class要用 ;
结尾,Java 与 JavaScript 均没有这个要求。
构造函数与实例化
在 C++, Java 中,使用与类同名并且不声明返回类型的成员函数来定义构造函数。
在 JavaScript 中,使用名为constructor
的成员函数来声明构造函数。
// ES6
class A {
constructor(x) {
this.x = x;
}
}
// ES5 and below
function A(x) {
this.x = x;
};
无须在class中提前声明成员,可以直接初始化赋值,这是动态语言的特点。
实例化:与C++, Java 一样,使用new
来创建类的对象。
// C++
A obj;
A obj();
A *obj = new A();
// Java
A obj = new A();
// JavaScript
obj = new A();
// ...
注意:在JavaScript中,如果忘记了在实例化的时候加 new
,效果就完全不一样了,这点需要注意。
成员(属性与方法)
实例能直接访问成员,必须先有实例对象才能访问其成员。
成员变量也叫属性,是实例能直接访问的变量。
成员函数也叫方法,是实例能直接调用的函数,并且函数中的this指针指向调用方法的实例。
举个简单的例子:
// C++
class A {
public:
int a;
void setA(int a) {
this -> a = a;
}
int getA() {
return this -> a;
}
};
用JS来实现就是:
// ES6
class A {
setA(a) { this.a = a; }
getA(a) { return this.a; }
}
// ES5 and below
function A {};
A.prototype.setA = function(a) { this.a = a; };
A.prototype.getA = function() { return this.a; };
类成员(静态成员)
类成员也叫静态成员,可以在没有实例的情况下访问,类成员不属于任何一个实例,也无法直接由实例访问。
在 C++/Java 中,静态成员使用 static 来修饰。
在JavaScript中,也可以用static修饰方法。
// ES6
class A {
static createA() { return new A(); }
}
// ES5 and below
function A(){};
A.createA = function(){ return new A(); };
关于用 static 修饰静态成员变量,则是ES7的语法”static property initialization”了,目前处于 ES7 Stage0 状态,基本上就是下一代ECMAScript就能用了。
// ES7
class A {
static count = ;
}
// ES6
class A {}
A.count = ;
// ES5 and below
function A(){};
A.count = ;
继承
继承分为单继承与多继承。
C++直接支持了单继承与多继承,而Java则采用了“单继承+接口”的方式。
JavaScript是通过原型继承的,目前依然是只有一个指针,只能支持单继承。不排除以后一个原型指针变成一个指针列表,从而支持多继承的可能,但这个可能性很小,因为多继承很难用。
// ES6
class A extends B {
constructor(x) {
super();
this.x = x;
}
}
默认情况下,类会直接继承 Object 类。
在ES5及以下版本,实现类的继承是不容易的。
类继承的要点有三:
1. 静态成员的继承
2. 方法的继承
3. 属性的继承
假设A要继承B。
首先要把B的构造函数包含在A的构造器里来继承B的构造器:
function A() {
B.apply(this, arguments); // super
// do other things
this.x = ;
}
然后要把B的所有属性(静态成员)拷贝给A:
for(var i in B) A[i] = B[i];
最后要把B的原型的所有属性(方法)拷贝给A的原型:
for(var i in B.prototype) A.prototype[i] = B.prototype[i];
// or create a new object by B's prototype
A.prototype = Object.create(B.prototype);
这样才能算是完成了类的继承,现在,ES6只需要简单地:
// ES6
class A extends B {
constructor() {
super(arguments);
// ...
}
}
就可以实现完美的单继承了。
注意: ES5 的模拟继承并不完美。
假设完成模拟继承之后,如果父类的一个静态成员变量是个数字,当父类的静态成员变量发生变化,子类的对应变量并不能随之变化。
在ES6中有一个__proto__
属性:
function A(){
B.apply(this, arguments); // 继承属性
// ...
}
A.__proto__ = B; // 继承静态成员
A.prototype.__proto__ = B.prototype; // 继承方法
模拟属性多继承
只要在构造器内应用多个类的构造器,就能实现属性的多继承。
function A(){
B.apply(this, arguments);
C.apply(this, arguments);
// ...
}
注意:不同类中的同名属性有一个后来居上的覆盖原则。
没有办法将B, C 的方法或者静态成员都绑定到A上,因为原型指针只有一个。
但是可以通过类似模拟单继承的方式将那些都拷贝到A上对应的位置。
那样的效果也不错,因为在通常情况下,是不会对基类进行修改的。
延伸:JavaScript 需要接口或者多继承吗?
Does JavaScript have the interface type (such as Java’s ‘interface’)? Answer of cHao
方法重载
在JavaScript中,函数也是属性,名字是唯一的,不能重复的,因此同名方法是不能共存的。
但是它们可以合并到一个函数里面去:
function add(a, b) {
if(typeof a === 'number' && typeof b === 'number') {
return a + b;
} else if(b === undefined) {
return a;
} else {
console.log(arguments);
throw new TypeError('no match for add and the arguments behind');
}
}
方法重写
直接覆盖即可
class A {
say() { console.log('A'); }
}
class B extends A {
say() { console.log('B'); }
}
访问控制
在C++/Java 中都有 public, private, protected 关键字来标记访问权限。
在JavaScript中却缺少这样的机制来控制权限。
用闭包来使得变量私有是很容易的,但使得对象的属性私有甚至受保护就很难了。
- 了解纯JavaScript做法,移步JavaScript 实现私有与受保护成员
- TypeScript 是 JavaScript 的超集,类型支持很强,移步 TypeScript Classes
然而个人觉得,在实际开发中并不需要这么强的访问控制。
说起来我也不是一个热衷面向对象的程序员。