面向对象编程(Object-oriented programming,OOP)是一种程序设计范型。它将对象作为程序的基本单元,将程序和数据封装其中,以提高程序的重用性、灵活性和扩展性。
面向对象特点:
- 封装
- 继承
- 多态
Javascript 是面向对象语言呢,还是基于对象语言呢?
MDN 给出的面向对象编程基本概念是这样的:
大家可以看看该博主总结的:JavaScript面向对象 - 简书
在这里,咱们不用纠结这个问题,不管是基于对象和面向对象,在编码过程中都应该有面向对象的意识。小编会使用 ES5 和 ES6 的语法来体现 JavaScript 的面向对象。
封装
ES5 语法
第一种方式:创建对象
// 图书类
var Book = function (title, time, type) {
this.title = title;
this.time = time;
this.type = type;
}
测试代码
// 实例化书籍
var book = Book("Javascript面向对象", "2014", "javascript");
var book1 = new Book("CSS新世界", "2015", "CSS");
console.log(book); // undefined
console.log(book1.title); // CSS新世界
console.log(window.title); // Javascript面向对象
为什么book为undefined,而且book的属性怎么会添加到window上面去了。因为咱们必须得用 new 关键字来实例化。可以使用安全模式来避免这种问题。
第二种方式:创建对象的安全模式
var Book = function (title, time, type) {
// 判断执行过程中this是否是当前这个对象(如果是说明是用new创建的)
if (this instanceof Book) {
this.title = title;
this.time = time;
this.type = type;
} else {
return new Book(title, time, type);
}
}
测试用例
var book = Book('Javascript', '2014', 'js');
console.log(book); // Book
console.log(book.title); // Javascript
console.log(book.time); // 2014
console.log(book.type); // js
console.log(window.title); // undefined
console.log(window.time); // undefined
console.log(window.type); // undefined
第三种方式:闭包实现
// 利用闭包实现
var Book = (function () {
// 静态私有变量
var bookNum = 0;
// 静态私有方法
function checkBook(name) {}
// 创建类
function _book(newId, newName, newPrice) {
// 私有变量
var name, price;
// 私有方法
function checkID($id) {}
// 特权方法
this.getName = function () {};
this.getPrice = function () {};
this.setName = function () {};
this.setPrice = function () {};
// 公有属性
this.id = newId;
// 公有方法
this.copy = function () {};
bookNum++;
if (bookNum > 100) {
throw new Error("我们仅出版100本书");
}
// 构造器
this.setName(name);
this.setPrice(price);
}
// 构建原型
_book.prototype = {
// 静态公有属性
isJsBook: false,
// 静态公有方法
display: function () {},
};
// 返回类
return _book;
})();
ES6 语法
class Book {
// 构造函数
constructor(title, time, type) {
this.title = title;
this.time = time;
this.type = type;
}
}
继承
单继承
ES5 语法
第一种方式:子类的原型对象——类式继承
// 父类
function superclass() {
this.books = ["Javascript", "html", "css"];
}
// 子类
function subclass() { }
subclass.prototype = new superclass();
var instancel = new subclass();
var instance2 = new subclass();
console.log(instance2.books); // ["Javascript","html","css"]
instancel.book.push("设计模式");
console.log(instance2.books); // ["Javascript","html","css","设计模式"]
缺点:
- 由于子类通过其原型prototype对父类实例化,继承了父类。所以说父类中的共有属性要是引用类型,就会在子类中被所有实例共用,因此一个子类的实例更改子类原型从父类构造函数中继承来的共有属性就会直接影响到其他子类。
- 由于子类实现的继承是靠其原型prototype对弗雷德实例化实现的,因此在创建父类的时候,是无法向父类传递参数的,因此在实例化父类的时候也无法对父类构造函数内的属性进行初始化。
第二种方式:创建即继承——构造函数继承
// 构造函数式继承
// 声明父类
function superclass(id) {
// 引用类型共有属性
this.books = ["Javascript", "html", "css"];
// 值类型共有属性
this.id = id;
}
// 父类声明原型方法
superclass.prototype.showBooks = function () {
console.log(this.books);
}
// 声明子类
function subclass(id) {
// 继承父类
superclass.call(this, id);
}
// 创建第一个子类的实例
var instance1 = new subclass(10);
// 创建第二个子类的实例
var instance2 = new subclass(11);
instance1.booksrouter.push("设计模式");
console.log(instance1.books); // ["Javascript","html","css","设计模式"]
console.log(instance1.id); // 10
console.log(instance2.books); // ["Javascript","html","css"]
console.log(instance2.id); // 11
instance1.showBooks(); // TypeError
注意:SuperClass.call(this, id);这条语句是构造函数式继承的精华,由于call这个方法可以更改函数的作用环境,因此在子类中,对superClass调用这个方法就是将子类中的变量在父类中执行一遍,由于父类中是给this绑定属性的,因此子类自然也就继承了父类的共有属性。由于这种类型的继承没有涉及原型 prototype,所以父类的原型方法自然不会被子类继承,而如果要想被子类继承就必须要放在构造函数中,这样创建出来的每个实例都会单独拥有一份而不能共用,这样就违背了代码复用的原则。为了综合这两种模式的优点,后来有了组合式继承。
第三种方式:组合继承——将优点为我所用
// 组合式继承
// 声明父类
function superclass(name) {
// 值类型共有属性
this.name = name;
// 引用类型共有属性
this.books = ["html", "css", "Javascript"];
// 父类原型共有方法
superclass.prototype.getName = function () {
console.log(this.name);
};
}
// 声明子类
function subclass(name, title) {
// 构造函数式继承父类 name 属性
superclass.call(this, name);
// 子类中新增共有属性
this.title = title;
}
// 类式继承 子类原型继承父类
subclass.prototype = new superclass();
// 子类原型方法
subclass.prototype.getTime = function () {
console.log(this.time);
};
测试用例
var instance1 = new subclass("js book", 2014);
instance1.books.push("设计模式");
console.log(instance1.books); // ["html", "css", "Javascript", "设计模式"]
instance1.getName(); // js book
instance1.getTime();// 2014
var instance2 = new subclass("css book", 2013);
console.log(instance2.books); // ["html", "css", "Javascript"]
instance2.getName(); // css book
instance2.getTime(); // 2013
第四种方式:原型式继承——洁净的继承者
// 原型是继承
function inheritoobject(o) {
// 声明一个过渡对象函数
function F() { };
// 过渡对象的原型继承父对象
F.prototype = o;
// 返回过渡对象的一个实例,该实例的原型继承了父对象
return new F();
}
测试用例
var book = {
name: "js book",
alikeBook: ["css book", "html book"]
};
var newBook = inheritoobject(book);
newBook.name = "ajax book";
newBook.alikeBook.push("xml book");
var otherBook = inheritobject(book);
otherBook.name = "flash book";
otherBook.alikeBook.push("as book");
console.log(newBook.name);//ajax book
console.log(newBook.alikeBook); //["css book", "html book", "xml book","as book"]
console.log(otherBook.name); //flash book
console.log(otherBook.alikeBook); //["css book", "html book", "xml book","as book"]
console.log(book.name); //js book
console.log(book.alikeBook); //["css book", "html book", "xml book","as book"]
第五种方式:寄生式继承——如虎添翼
// 寄生式继承
// 声明基对象
var Book = {
name: "js book",
alikeBook: ["css book", "html book"]
};
function createBook(obj) {
// 通过原型继承方式创建新对象
var o = new inheritobject(obj);
// 拓展新对象
o.getName = function () {
console.log(name);
};
// 返回拓展后的新对象
return o;
}
第六种方式:寄生组合式继承——终极继承者
/**
* 寄生式继承 继承原型
* 传递参数 subclass 子类
* 传递参数 superclass 父类
**/
function inheritPrototype(subclass, superclass) {
// 复制—份父类的原型副本保存在变量中
var p = inheritobject(superclass.prototype);
// 修正因为重写子类原型导致子类的constructor属性被修改
p.constructor = subclass;
// 设置子类的原型
subclass.prototype = p;
}
// 定义父类
function superclass(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
// 定义父类原型方法
superclass.prototype.getName = function () {
console.log(this.name);
};
// 定义子类
function subclass(name, time) {
// 构造函数式继承
superclass.call(this, name);
// 子类新增属性
this.time = time;
}
// 寄生式继承父类原型
inheritPrototype(subclass, superclass);
// 子类新增原型方法
subclass.prototype.getTime = function () {
console.log(this.time);
}
测试用例
// 创建两个测试方法
var instance1 = new subclass("js book", 2014);
var instance2 = new subclass("css book", 2013);
instance1.colors.push("black");
console.log(instance1.colors); //["red", "blue", "green", "black"]
console.log(instance2.colors); //["red", "blue", "green"]
instance2.getName(); //css book
instance2.getTime(); //2013
ES6 语法
// 父类
class superclass {
constructor(title, time) {
this.title = title;
this.time = time;
}
}
// 子类
class subclass extends superclass {
constructor(title, time, type) {
super(title, time);
this.type = type;
}
}
多继承
在 JavaScript 中继承是依赖于原型 prototype 链实现的,只有一条原型链,所以理论上是不能继承多个父类的。然而 JavaScript 是灵活的,通过一些技巧方法你却可以继承多个对象的属性来实现类似的多继承。
var extend = function (target, source) {
// 遍历源对象中的属性
for (var property in source) {
// 将源对象中的属性复制到目标对象中
target[property] = source[property];
}
// 返回目标对象
return target;
};
测试用例
var book = {
name: 'Javascript设计模式',
alike: ['css', 'html', 'Javascript']
}
var anotherBook = {
color: 'blue'
}
extend(anotherBook, book);
console.log(anotherBook.name);// Javascript设计模式
console.log(anotherBook.alike);// ["css", "html", "Javascript"]
anotherBook.alike.push('ajax');
anotherBook.name = '设计模式';
console.log(anotherBook.name);// 设计模式
console.log(anotherBook.alike);// ["css", "html", "Javascript", "ajax"]
console.log(book.name);// Javascript设计模式
console.log(book.alike); // ["css", "html", "Javascript", "ajax"]
我们的这个extend方法是一个浅复制过程,他只能复制值类型的属性,对于引用类型的属性它无能为力。而在jquery等一些框架中实现了深复制,就是将源对象中的引用类型的属性再执行一遍extend方法而实现的。
// 多继承 属性复制
var mix = function () {
var i = 1, // 从第二个参数起为被继承的对象
len = arguments.length,// 获取参数长度
target = arguments[0], // 第—个对象为目标对象
arg; // 缓存参数对象
// 遍历被继承的对象
for (; i < len; i++) {
// 缓存当前对象
arg = arguments[i];
// 遍历被继承对象中的属性
for (var property in arg) {
// 将被继承对象中的属性复制到目标对象中
target[property] = arg[property];
}
}
// 返回目标对象
return target;
};
mix方法的作用就是将传入的多个对象的属性复制到源对象中,这样即可实现对多个对象的属性的继承。”
“这是实现方式真不错,可是使用的时候需要传入目标对象(第一个参数——需要继承的对象)。”
“当然你也可以将它绑定到原生对象Object上,这样所有的对象就可以拥有这个方法了。
object.prototype.mix = function () {
var i = 0, // 从第—个参数起为被继承的对象
len = arguments.length, // 获取参数长度
arg;// 缓存参数对象
// 遍历被继承的对象
for (; i < len; i++) {
// 缓存当前对象
arg = arguments[i];
// 遍历被继承对象中的属性
for (var property in arg) {
// 将被继承对象中的属性复制到目标对象中
this[property] = arg[property];
}
}
}
测试用例
otherBook.mix(book1, book2);
console.log(otherBook);// object {color:"blue", name:"Javascript设计模式", mix:function, about:"—本Javascript书"}
多态
多态,就是同一个方法多种调用方式。在 JavaScript 中也是可以实现的,只不过要对传入的参数做判断以实现多种调用方式,如我们定义一个add方法,如果不传参数则返回10,如果传一个参数则返回10+参数,如果传两个参数则返回两个参数相加的结果。
//多态
function add() {
// 获取参数
var arg = arguments,
// 获取参数长度
len = arg.length;
switch (len) {
// 如果设有参数
case 0:
return 10;
// 如果只有—个参数
case 1:
return 10 + arg[0];
// 如果有两个参数
case 2:
return arg[0] + arg[1];
}
}
// 测试用例
console.log(add()); // 10
console.log(add(5)); // 15
console.log(add(6, 7));// 13
当然我们还可以让其转化成更易懂的类形式:
function Add() {
// 无参数算法
function zero() {
return 10;
}
// —个参数算法
function one(num) {
return 10 + num;
}
// 两个参数算法
function two(num1, num2) {
return num1 + num2;
}
// 相加共有方法
this.add = function () {
var arg = arguments,
// 获取参数长度
len = arg.length;
switch (len) {
// 如果设有参数
case 0:
return zero();
// 如果只有—个参数
case 1:
return one(arg[0]);
// 如果有两个参数
case 2:
return two(arg[0], arg[1]);
}
}
}
// 实例化类
var A = new Add();
//测试
console.log(A.add()); // 10
console.log(A.add(5));// 15
console.log(A.add(6, 7));// 13
总结
封装与继承是面向对象中的两个主要特性,继承即是对原有对象的封装,从中创建私有属性、私有方法、特权方法、共有属性、共有方法等,对于每种属性与每种方法特点是不一样的,有的不论对类如何实例化,它只创建一次,那么这类属性或者方法我们称之为静态的。有的只被类所拥有,那么这类属性和方法又是静态类方法与静态类属性。当然可被继承的方法与属性无外乎两类,一类在构造函数中,这类属性与方法在对象实例化时被复制一遍。另一类在类的原型对象中,这类属性与方法在对象实例化时被所有实例化对象所共用。
提到类的实例化我们就引出了继承,当然如果实例化的是对象那么则为对象继承,如果实例化的是类(当然类也是一种对象,只不过是用来创建对象的),那么就是一种类的继承。对于类的继承我们根据继承的方式又分为很多种,通过原型链继承的方式我们称之为类式继承,通过构造函数继承的方式我们称之为构造函数式继承,那么将这两种方式组合起来的继承方式我们称之为组合继承,由于类式继承过程中会实例化父类,这样如果父类构造函数极其复杂,那么这种方式对构造函数的开销是不值得的,此时有了一种新的继承方式,通过在一个函数内的过渡对象实现继承并返回新对象的方式我们称之为寄生式继承,此时我们在结合构造函数时继承,这样再融合构造函数继承中的优点并去除其缺点,得到的继承方式我们称之为寄生组合式继承。当然有时候子类对父类实现继承可以通过拷贝方法与属性的方式来实现,这就有了多继承,即将多个父类(对象)的属性与方法拷贝给子类实现继承。
对于面向对象中的多态,在JavaScript中实现起来就容易得多了,通过对传递的参数判断来决定执行逻辑,即可实现一种多态处理机制。
参考:
- MDN (Mozilla Developer Network)
- JavaScript 设计模式
大家也可以看看其他博主的讲解: