JavaScript是不是一门严格意义上的面向对象的语言,它并没有提供类的方法。它是使用 原型继承 而不是类继承达到面向对象的效果。(在 ES2015/ES6 中引入了class关键字,但只是语法糖,JavaScript 仍然是基于原型的)。
什么是面向对象
面向对象语言的三大特性:
- 封装(Encapsulation): 把相关的信息(无论数据或方法)存储在对象中的能力
- 继承(Inheritance): 由另一个类(或多个类)得来类的属性和方法的能力
- 多态(Polymorphism): 编写能以多种方法运行的函数或方法的能力
其他的一些内容:
- 类(Class): 定义了一件事物的抽象特点, 用来构造对象
- 对象(Object): 类的实例化
- 属性(Property): 对象具有的数据
- 方法(Method): 也成消息,用于对象之间传递数据
封装(encapsulation )
对于ES5来说,没有class的概念,并且由于js的函数级作用域(在函数内部的变量在函数外访问不到),所以我们就可以模拟 class 的概念,在es5中,类其实就是保存了一个函数的变量,这个函数有自己的属性和方法。如果我们要把”属性”(property)和”方法”(method),封装成一个对象,甚至要从原型对象生成一个实例对象,我们就需要对对象进行封装。
封装:把客观事物封装成抽象的类,隐藏属性和方法的实现细节,仅对外公开接口。
创建对象的方式
以下封装的实例对象,均以
Person
为例,包含name
这个基本属性和say
这个方法
工厂模式
function Person() {
var o = new Object();
o.name = 'hanmeimei';
o.say = function() {
console.log(this.name);
}
return o;
}
var person = Person(); // 和 寄生构造函数模式 相比,没有new
缺点:
- 对象识别问题:无法识别对象,以为都是来自Object,无法得知来自Person
- 内存浪费问题:每次通过Person创建对象的时候,都会返回相同属性和方法的对象。
构造函数模式
- 构造函数名首字母大写
- 内部使用this:通过this定义的属性和方法,实例化对象的时候都会重新复制一份
- 使用 new 生成实例 (注意:构造函数模式隐试的在最后返回
return this
所以在缺少new
的情况下,会将属性和方法添加给 全局对象,浏览器端就会添加给window对象。)
function Person() {
this.name = 'hanmeimei';
this.say = function() {
console.log(this.name)
}
// return this /**隐式返回**/
}
var person = new Person();
优点:
- 通过
constructor
或者instanceof
可以识别对象实例的类别person instanceof Object // true person instanceof Person // true person.constructor === Person // true
- 可以通过new 关键字来创建对象实例,更像OO语言中创建对象实例
缺点:
- 构造函数创建对象,每个方法都要在每个实例上重新创建一次
new的实质:
- 创建一个新对象(实例)
- 将构造函数的作用域赋给新对象(也就是重设了this的指向,this就指向了这个新对象)
- 执行构造函数中的代码(为这个新对象添加属性)
- 返回新对象
原型模式
原型模式把那些不变的属性和方法,直接定义在prototype对象上。
function Person() {}
Person.prototype.name = 'hanmeimei';
Person.prototype.say = function() {
console.log(this.name);
}
var person = new Person();
优点:
- 属性和方法共享:所有的实例对象共享它所包含的属性和方法,不必在构造函数中定义对象实例信息。
- 动态添加属性和方法:可以动态的添加原型对象的方法和属性,并直接反映在对象实例上。
Person.prototype.friends = ['lilei'] console.log(person.friends) // ['lilei']
缺点:
- 引用类型值会出现问题
// 因为js对引用类型的赋值都是将地址存储在变量中,所以person1和person2的friends属性指向的是同一块存储区域。 var person1 = new Person(); var person2 = new Person(); person1.friends.push('xiaoming'); console.log(person2.friends) //['lilei', 'xiaoming']
- 向上查找:第一次调用say方法或者name属性的时候会搜索两次,第一次是在实例上寻找say方法,没有找到就去原型对象(Person.prototype)上找say方法,找到后就会在实力上添加这些方法or属性。
- 所有的方法都是共享的,没有办法创建实例自己的属性和方法,也没有办法像构造函数那样传递参数。
构造函数和原型组合模式
这是使用最为广泛、认同度最高的一种创建自定义类型的方法。它可以解决上面那些模式的缺点
function Person(name) {
this.name = name
}
Person.prototype.say = function() {
console.log(this.name)
}
var person = new Person('hanmeimei')
person.say() //hanmeimei
优点:
- 解决了原型模式对于引用对象的缺点:每个实例都会有自己的一份实例属性副本
- 解决了原型模式没有办法传递参数的缺点
- 解决了构造函数模式不能共享方法的缺点:共享着对方法的引用
动态原型模式
动态原型模式将所有信息都封装在了构造函数中,初始化的时候,通过检测某个应该存在的方法时候有效,来决定是否需要初始化原型。
function Person(name) {
this.name = name
// 只有在sayName方法不存在的时候,才会将它添加到原型中。这段代码只会初次调用构造函数的时候才会执行。
if(typeof this.say !== 'function') {
Person.prototype.say = function() {
console.log(this.name)
}
}
}
优点:
- 可以在初次调用构造函数的时候就完成原型对象的修改
- 修改能体现在所有的实例中
寄生构造函数模式
和工厂模式基本一样,除了多了个new操作符
function Person(name) {
var o = new Object()
o.name = name
o.say = function() {
alert(this.name)
}
return o
}
var peron1 = new Person('hanmeimei')
稳妥构造模式
稳妥对象指的是没有公共属性,而且其方法也不引用this。
稳妥对象最适合在一些安全环境中(这些环境会禁止使用this和new),或防止数据被其他应用程序改动时使用。
function Person(name) {
var o = new Object()
o.say = function() {
alert(name)
}
}
var person1 = new Person('hanmeimei');
person1.name // undefined
person1.say() //hanmeimei
优点:
- 安全,name 好像成为了私有变量,只能通过say方法去访问
缺点:
- 不能区分实例的类别
参考
继承(Inheritance)
继承:子类可以使用父类的所有功能,并且对这些功能进行扩展。继承的过程,就是从一般到特殊的过程。
继承方式
以下代码 以
SuperClass
为父类,SuperClass
为子类
原型链继承
核心:将父类的实例作为子类的原型
核心实现代码:SubClass.prototype = new SuperClass();
// 声明父类
var SuperClass = function () {
this.name = 'hanmeimei';
this.say = function() {
console.log(this.name);
}
};
//声明子类
var SubClass = function () {};
SubClass.prototype = new SuperClass()
var sub = new SubClass()
优点:父类方法可以复用
缺点:
- 父类的引用属性会被所有子类实例共享
- 子类构建实例时不能向父类传递参数
构造函数继承
核心:将父对象的构造函数绑定在子对象上,直接改变this的指向。(这是所有继承中唯一一个不涉及到prototype的继承。)
核心代码实现:SuperType.bind(this);
(也可以使用call
、apply
)
// 声明父类
var SuperClass = function () {
this.name = 'hanmeimei';
this.say = function() {
console.log(this.name);
}
};
//声明子类
var SubClass = function () {
SuperClass.call(this, arguments );
};
var sub = new SubClass()
优点:(和原型链继承完全反过来)
- 父类的引用属性不会被共享
- 子类构建实例时可以向父类传递参数
缺点:父类的方法不能复用,子类实例的方法每次都是单独创建的。
组合式继承
核心:原型式继承和构造函数继承的组合,兼具了二者的优点。
组合式继承就是汲取 类式继承 和 构造函数继承 的优点,即避免了内存浪费,又使得每个实例化的子类互不影响。
// 声明父类
var SuperClass = function () {
this.name = 'hanmeimei';
};
SuperClass.prototype.say = function() {
console.log(this.name);
}
//声明子类
var SubClass = function () {
SuperClass.call(this, arguments );
};
SubClass.prototype = new SuperClass()
var sub = new SubClass()
原型式继承
核心:原型式继承的object方法本质上是对参数对象的一个浅复制。
function object(o){
function F(){}
F.prototype = o;
return new F();
}
var SuperClass = {
name: 'hanmeimei',
say: function(){
console.log(this.name)
}
}
var subClass = object(SuperClass);
优点:父类方法可以复用
缺点:
- 父类的 引用属性 会被所有子类实例共享 (写实例)
- 子类构建实例时不能向父类 传递参数
寄生式继承
核心:使用原型式继承获得一个目标对象的浅复制,然后增强这个浅复制的能力。
这种继承方式是把原型式+工厂模式结合起来,目的是为了封装创建的过程。
寄生组合继承
先给父类的原型创建一个副本,然后修改子类constructor属性,最后在设置子类的原型就可以了
ES6 Class extends
核心: ES6继承的结果和 寄生组合继承 相似,本质上,ES6继承是一种语法糖。但是,寄生组合继承是先创建子类实例this对象,然后再对其增强;而ES6先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。
class SuperClass {}
class SubClass extends SuperClass {
constructor() {
super();
}
}
ES6实现继承的具体原理:
参考
多态(Polymorphism)
所谓多态,就是指一个引用类型在不同情况下的多种状态。
多态的最根本好处在于,你不必再向对象询问“你是什么类型”而后根据得到的答案调用对象的某个行为——你只管调用该行为就是了,其他的一切多态机制都会为你安排妥当。
普通写法:
var googleMap = {
show: function(){
console.log( '开始渲染谷歌地图' );
}
};
var baiduMap = {
show: function(){
console.log( '开始渲染百度地图' );
}
};
// 函数以字符串形式的传参形式
// 如果判断条件一旦增多,就会变全是if-else语句
var renderMap = function( type ){
if ( type === 'google' ){
googleMap.show();
}else if ( type === 'baidu' ){
baiduMap.show();
}
};
renderMap( 'google' ); // 输出:开始渲染谷歌地图
renderMap( 'baidu' ); // 输出:开始渲染百度地图
改进:
var googleMap = {
page: 10,
show: function(){
console.log( '开始渲染谷歌地图' );
}
};
var baiduMap = {
page: 1,
show: function(){
console.log( '开始渲染百度地图' );
}
};
// 将render传入的参数换成相应的对象
var renderMap = function( map ){
if ( map.show instanceof Function ){
map.show();
console.log(mape.page);
}
};
renderMap( googleMap ); // 输出:开始渲染谷歌地图 10
renderMap( baiduMap ); // 输出:开始渲染百度地图 1