前言:本系列是阅读《JavaScript 设计模式》(张容铭),Refactoring Guru 两者的集成笔记,有兴趣可自行查阅。
PS:前者出版年份为 2015,可能有些设计模式已经没那么有价值,后者(网站的政治倾向广告可以忽略)主要内容是面对对象编程语言如 Java,但是都可以参考其设计思想。
面对对象
创建一个类
面对对象编程即是将需求抽象为一个对象,分析其特征(属性)与动作(方法)。
javascript 是一种基于原型(prototype)的语言,每创建一个对象时,都会为其创建一个原型对象,用于指向其继承的属性、方法。
原型对象中会像函数中创建this
一样创建一个constructors
属性,用于指向包含该原型对象的函数或对象。
通过prototype
继承的属性、方法并不属于对象自身,需要通过prototype
一级一级查找,因此通过类创建(实例化)对象时,由this
指定的属性、方法会得到创建,而通过prototype
继承的属性、方法是每个对象通过 prototype 访问到,并不会再次创建。
let Book = function (name, price) {
this.name = name;
this.price = price;
};
Book.prototype.display = function () {
console.log(`<${this.name}>的价格是${this.price}`);
};
let book = new Book('山海经注解', 10); // 使用new关键字实例化对象时,this指向当前对象
book.display(); // <山海经注解>的价格是10
属性和方法的封装
在函数内部使用
this
创建的属性、方法,在类创建对象时,每个对象都拥有一份并可通过外部访问,可看做对象的公共属性、公共方法。
通过 javascript 函数级作用域这一特性,可以实现类的私有属性、私有方法的创建。
通过this
创建的方法,不仅能访这些对象的公用属性和公用方法,还能访问类(创建时)或对象自身私有属性和私有方法,可以看做特权(private)方法。
let Book = function (name, price) {
// 私有变量
let number = 1;
let that = this;
// 特权方法
this.getNum = function () {
return number;
};
// 构造器(注意:构造函数内this指向window对象,需要处理)
// 1.使用that变量
function setName() {
that.name = name;
}
// 2.使用箭头函数
setPrice = () => {
this.price = price;
};
setName(name);
setPrice(price);
};
闭包实现静态私有变量
闭包即是有权访问另一个函数作用域中变量的函数(通常在被访问函数中被调用,通过函数作用域链向上访问)。
// 闭包内部实现一个完整类然后返回
let Book = (function () {
// 静态私有变量
let number = 1;
function _book(newName, newPrice) {
this.name = newName;
this.price = newPrice;
this.getNum = function () {
return number;
};
}
_book.prototype.display = function () {
console.log(`<${this.name}>的价格是${this.price},剩下${number}本`);
};
return _book;
})();
let book = new Book('山海经注解', 10);
book.display(); // <山海经注解>的价格是10,剩下1本
创建对象的安全模式
有时候可能会忘记使用
new
关键字,直接指向 Book 类可能导致副作用,通过检查实现安全模式。
let Book = function (name, price) {
// instanceof 用于判断是否当前对象的原型对象
if (this instanceof Book) {
this.name = name;
this.price = price;
} else {
console.log('Does not use keyword new');
return new Book(name, price);
}
};
let book = new Book('山海经注解', 10);
console.log(`<${book.name}>的价格是${book.price}`); // <山海经注解>的价格是10
继承
类式继承
新建子类与父类,子类的
prototype
指向父类对象,通过子类原型访问到父类原型属性和方法。
缺点是若某个子类从父类继承的公共属性是引用类型,更改后会影响到其他子类。
// 声明父类
function fatherBook() {
// 使用与子类不同的变量,防止被覆盖
this.title = 'father';
this.sort = ['all'];
}
// 添加父类公共方法
fatherBook.prototype.getFatherName = function () {
return this.title;
};
// 声明子类
function sonBook(name) {
this.name = name;
}
// 继承父类
sonBook.prototype = new fatherBook();
// 添加子类公共方法
sonBook.prototype.getSonName = function () {
return this.name;
};
let foodBook = new sonBook('food');
console.log(`sonBook is ${foodBook.getSonName()},fatherBook is ${foodBook.getFatherName()}`); // sonBook is food,fatherBook is father
foodBook.sort.push('apple');
console.log(`food's sort is ${foodBook.sort}`); // food's sort is all,apple
let utilBook = new sonBook('util');
utilBook.sort.push('wrench');
console.log(`util's sort is ${utilBook.sort}`); // util's sort is all,apple,wrench
构造函数继承
利用
call()
更改函数上下文环境,父类中由this
绑定属性、方法,子类自然就继承了父类的公共属性、公共方法,并不涉及原型prototype
。
缺点是由于没有涉及原型,父类原型方法想被子类继承就必须放在构造函数中,这样创建的实例都会拥有一份而无法共用,违背了DRY
(Don't repeat yourself)原则。
// 声明父类构造函数
function fatherBook(name) {
this.name = name;
this.sort = ['all'];
}
// 声明子类构造函数
function sonBook(name) {
fatherBook.call(this, name);
}
let foodBook = new sonBook('food');
foodBook.sort.push('apple');
console.log(`food's sort is ${foodBook.sort}`); // food's sort is all,apple
let utilBook = new sonBook('util');
utilBook.sort.push('wrench');
console.log(`util's sort is ${utilBook.sort}`); // util's sort is all,wrench
组合继承
结合了类式继承和构造函数继承的优点。
// 声明子类
function fatherBook(name) {
// 使用与子类不同的变量,防止被覆盖
this.name = name;
this.sort = ['all'];
}
// 添加父类公共方法
fatherBook.prototype.getName = function () {
return this.name;
};
// 声明子类
function sonBook(name) {
fatherBook.call(this, name);
}
// 继承父类
sonBook.prototype = new fatherBook();
let foodBook = new sonBook('food'),
utilBook = new sonBook('util');
foodBook.sort.push('apple');
console.log(`${foodBook.getName()} sort is ${foodBook.sort}`); // food sort is all,apple
utilBook.sort.push('wrench');
console.log(`${utilBook.getName()} sort is ${utilBook.sort}`); // // util's sort is all,wrench
原型式继承
原型式继承是对类式继承的封装,目的在于返回新的实例化对象。
借助prototype
根据已有对象创建一个新对象,同时不必创建新的自定义对象。
随着这种思想的深入,后续出现了Object.create()
。
function inheritObject(obj) {
// 声明过渡对象
function Fn() {}
// 过渡对象继承父对象
Fn.prototype = obj;
// 返回过渡对象实例
return new Fn();
}
let fatherBook = {
name: 'father',
sort: ['all'],
};
let foodBook = inheritObject(fatherBook),
utilBook = inheritObject(fatherBook);
foodBook.sort.push('apple');
console.log(`foodBook sort is ${foodBook.sort}`); // foodBook sort is all,apple
utilBook.sort.push('wrench');
console.log(`utilBook sort is ${utilBook.sort}`); // utilBook sort is all,apple,wrench
寄生式继承
原型式继承的二次封装,封装过程中对继承的对象进行了扩展。
function createBook(obj) {
let o = inheritObject(obj);
o.getName = function () {
return this.name;
};
return o;
}
组合寄生式继承
解决组合式继承缺点——子类原型为父类实例,这里处理是类的原型而非对象。
需要注意的是,如果子类添加原型方法,必须使用点语法,否则会覆盖从父类原型继承的对象。
function inheritPrototype(subClass, superClass) {
// 复制一份父类原型的副本
let p = inheritObject(superClass.prototype);
// 设置子类的原型
subClass.prototype = p;
// 修正因为重写子类原型,subClass.constructor属性被修改
p.constructor = subClass;
}
// 声明子类
function fatherBook(name) {
// 使用与子类不同的变量,防止被覆盖
this.name = name;
this.sort = ['all'];
}
// 添加父类公共方法
fatherBook.prototype.getName = function () {
return this.name;
};
// 声明子类
function sonBook(name) {
fatherBook.call(this, name);
}
// 继承父类
inheritPrototype(sonBook, fatherBook);
let foodBook = new sonBook('food'),
utilBook = new sonBook('util');
foodBook.sort.push('apple');
console.log(`${foodBook.getName()} sort is ${foodBook.sort}`); // food sort is all,apple
utilBook.sort.push('wrench');
console.log(`${utilBook.getName()} sort is ${utilBook.sort}`); // util sort is all,wrench
多继承
javascript 依赖原型链实现继承,理论上是无法实现多继承,但是可以使用一些技巧,通过继承多个对象的属性实现类似的多继承。
// 单继承,实际上是一个浅拷贝过程
function extend(target, source) {
/*
* 遍历源对象属性
* for...in是为遍历对象属性而构建的,
* 可以任意顺序迭代一个对象除Symbol以外的可枚举属性,
* 包括继承的可枚举属性
*/
for (const i in source) {
target[i] = source[i];
}
return target;
}
// 多继承,传入多个参数,第一位目标对象,其他为源对象
function mix() {
let i = 1,
len = arguments.length,
target = arguments[0],
arg;
for (; i < len; i++) {
// 缓存当前对象
arg = arguments[i];
for (const property in arg) {
target[property] = arg[property];
}
}
return target;
}
// 也可将min方法直接绑到原生对象Object上使用
Object.prototype.mix = function () {
let i = 0,
len = arguments.length,
arg;
for (; i < len; i++) {
// 缓存当前对象
arg = arguments[i];
for (const property in arg) {
this[property] = arg[property];
}
}
};
let book1 = { name: 'book' },
book2 = { price: 10 },
otherBook = { sort: ['other'] };
otherBook.mix(book1, book2);
console.log(otherBook); // {"sort": ["other"],"name": "book","price": 10}
多种调用方式——多态
在面对对象编程思想中,多态是指同一个行为具有多个不同表现形式或形态的能力,例如同是吃饭,猫吃老鼠、牛吃草,即是同一个方法多种调用方式,在 javascript 中可通过对传入参数加以判断实现多态。
function add() {
function zero() {
return 0;
}
function one(num) {
return num;
}
function two(num1, num2) {
return num1 + num2;
}
this.add = function () {
let 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]);
default:
return new Error('only access two number');
}
};
}
let A = new add();
console.log(A.add(1, 2)); // 3
console.log(A.add(1, 2, 3)); // Error: only access two number
console.log(A.add()); // 0