JavaScript 设计模式(序)— 面对对象编程

45 阅读3分钟

前言:本系列是阅读《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