运用 JS 中的各种模式创建自定义类型

本文详细介绍了7种JavaScript创建自定义类型的模式,包括工厂模式、构造函数模式等,并对比了各自的优缺点。最后,解析了ES6中class语法的本质。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

虽然在 ES6 中,已经出了 class 的语法,貌似好像不用了解 ES5 中的这些老东西了,但是越深入学习,你会发现理解这些模式的重要性。

在本文中,我会描述 7 种常用的创建自定义类型的模式:工厂模式、构造函数模式、原型模式、组合使用构造函数模式、动态原型模式、寄生构造函数模式、稳妥构造函数模式。分别给出他们的示例代码,并分析他们的利弊,方便读者选择具体的方式来构建自己的自定义类型。

最后,我会指出 ES6 中的 class 语法,本质上其实还是利用了组合使用构造函数模式进行创建自定义类型。

1. 工厂模式

废话不多说,先上工厂模式的实例代码:

function createPerson(name, age, job){
    var o = new Object();           // 创建对象
    o.name = name;                  // 赋予对象细节
    o.age = age;                    // 赋予对象细节
    o.job = job;                    // 赋予对象细节
    o.sayName = function(){         // 赋予对象细节
        alert(this.name);
    };
    return o;                       // 返回该对象
}

var person1 = createPerson("Nicholas", 29, "Software Engineer");
var person2 = createPerson("Greg", 27, "Doctor");

优点:解决了创建多个相似对象的问题;
缺点:没有解决对象识别的问题(即不知道这个对象是什么类型),对于对象的方法没有做到复用。

2. 构造函数模式

function Person(name, age, job){
    this.name = name;       // 对象的所有细节全部挂载在 this 对象下面
    this.age = age;
    this.job = job;
    this.sayName = function(){
        alert(this.name);
    }; 
}

var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

说到构造函数模式就不得不提到 new 操作符了。我们来看看 new 这个操作符到底做了什么:

  1. 创建一个对象;
  2. 将构造函数内的 this 指向这个新创建的对象,同时将该函数的 prototype 的引用挂载在新对象的原型下;
  3. 执行函数内的细节,也就是将属性和方法挂载在新对象下;
  4. 隐式的返回新创建的对象。

优点:解决了对象识别的问题;
缺点:对于自定义类型的方法每次都要新创建一个方法函数实例,没有做到函数复用。如果把所有方法函数写到父级作用域中,是做到了函数复用,但同时方法函数只能在父级作用域的某个类型中进行调用,这对于父级作用域有点名不副实,同时对于自定义引用类型没有封装性可言。

3. 原型模式

function Person(){
}

Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
    alert(this.name);
};

var person1 = new Person();
person1.sayName();      //"Nicholas"

var person2 = new Person();
person2.sayName();      //"Nicholas"

alert(person1.sayName == person2.sayName);      //true

理解要点:

  1. 无论什么时候,只要创建了一个新函数,就会根据一组特定规则为该函数创建一个 prototype 属性,这个属性指向函数的原型对象。
  2. 在默认情况下,所有原型对象都会自动获得一个 constructor 属性,这个属性包含一个指向 prototype 属性所在函数的指针。至于原型中的其他方法则都是从 Object 继承而来。
  3. 当调用构造函数创建了一个新实例后,该实例的内部将包含一个指针 [[prototype]](内部属性) ,指向构造函数的原型对象
  4. 当调用构造函数创建一个新实例后,该实例的实例环境,即构造函数,会针对原型对象上的非引用类型的原型属性,在构造函数中自动构建相应的实例环境属性。也就是说,之后根据构造函数创建的实例,它的实例属性中的非引用类型属性,都仍是根据构造函数中的实例环境属性创建的。

但是为减少不必要的输入,也为了从视觉上更好地封装原型的功能,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象。如下所示:

function Person(){
}

Person.prototype = {
    name : "Nicholas",
    age : 29,
    job: "Software Engineer",
    sayName : function () {
        alert(this.name);
    }
};

但是这种写法,其本质上完全重写了默认的 prototype 对象,因此 constrctor 属性也就变成了新对象的 constructor 属性(指向 Object 构造函数),不在指向 Person 函数。尽管此时,instanceOf 操作符还能返回正确的结果。

如果 constructor 属性真的很重要,可以像下面这样特意将它设置回适当的值:

function Person(){
}

Person.prototype = {
    constructor : Person,
    name : "Nicholas", 
    age : 29,
    job: "Software Engineer",
    sayName : function () {
        alert(this.name);
    }
};

注意,以这种方式重设 constructor 属性会导致他的 [[Enumerable]] 特性被设置为 true 。默认情况下,原生的 constructor 属性是不可枚举的,因此,如果你使用兼容 ECMAScript 5JavaScript 引擎,你可以试试 Object.defineProperty() 方法:

function Person(){
}

Person.prototype = {
    name : "Nicholas",
    age : 29,
    job : "Software Engineer",
    sayName : function () {
        alert(this.name);
    }
};

//重设构造函数,只适用于 ECMAScript 5 兼容的浏览器
Object.defineProperty( Person.prototype, "constructor", {
    enumerable: false,
    value: Person
});

注意,重写原型对象会切断新原型与已经存在的对象实例之间的联系;它们引用的仍然是最初的原型。

优点:对自定义类型的方法解决了函数复用的问题。
缺点:

  1. 不能为构造函数传递初始化参数
  2. 原型模式中实现了对于包含引用类型值的属性的共享,这就意味着一个实例中修改了该引用类型值,所有实例的该属性都会被修改!!!

4. 组合使用构造函数模式和原型模式

在组合使用构造函数模式和原型模式中,构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性,而且还支持向构造函数传递参数。如以下示例代码所示:

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["Shelby", "Court"];
}

Person.prototype = {
    sayName : function(){
        alert(this.name);
    }
}

Object.defineProperty( Person.prototype, "constructor", {
    enumerable: false,
    value: Person
);

var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

person1.friends.push("Van");
alert(person1.friends);    //"Shelby,Count,Van"
alert(person2.friends);    //"Shelby,Count"
alert(person1.friends === person2.friends);     //false
alert(person1.sayName === person2.sayName);     //true

优点:能为构造函数传递初始化参数;该复用复用,不该复用的没复用。
缺点:封装性不好,构造函数和原型分别独立于父级作用域进行申明。

5. 动态原型模式(推荐)

该模式把所有信息都封装在构造函数中,通过构造函数来实现初始化原型 (仅在必要的情况下),又保持了同时使用构造函数和原型的优点。请看以下示例代码:

function Person(name, age, job){
    //属性
    this.name = name; 
    this.age = age; 
    this.job = job;

    //方法
    if (typeof this.sayAge != "function"){      // 此处应该永远去判断新添加的属性和方法
        Person.prototype.sayName = function(){
            alert(this.name);
        }; 
        Person.prototype.sayAge = function(){
            alert(this.age);
        };
    }
}

var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();

if 语句检查的可以是初始化之后应该存在的任何属性或方法——不必用一大堆 if 语句检查每个属性和每个方法;只要检查其中一个即可

注意,使用动态原型模式时,不能使用对象字面量重写原型。前面已经解释过了,如果已经创建的实例的情况下重写原型,那么就会切断新原型与现有实例之间的联系。

优点:封装性非常好;还可使用 instanceOf 操作符确定它的类型。
缺点:

6. 寄生构造函数模式

除了使用 new 操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实是一模一样的。请看以下代码:

function Person(name, age, job){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function(){
        alert(this.name);
    };
    return o; 
}

var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();       //"Nicholas"

在使用 new 操作符下,构造函数在不返回值的情况下,默认会返回新对象实例。而通过在构造函数的末尾添加一个 return 语句,可以重写调用构造函数时返回的值。

缺点:没有解决对象识别的问题(即不知道这个对象是什么类型),不能依赖 instanceOf 操作符来确定对象类型;对于对象的方法没有做到复用。

7. 稳妥构造函数模式

先来了解下稳妥对象:指的是没有公共属性,而且其方法也不引用 this 的对象。稳妥对象最适合在一些安全的环境中 (这些环境中会禁止使用 this 和 new),或者再防止数据被其他应用程序 (如 Mashup 程序) 改动时使用。稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建对象的实例方法不引用 this;二是不使用 new 操作符调用构造函数。以下为示例代码:

function Person(name, age, job){
    var o = new Object();       //创建要返回的对象

    //可以在这里定义私有变量和函数

    o.sayName = function(){     //添加方法
        alert(name);
    };

    return o;       //返回对象
}

var friend = Person("Nicholas", 29, "Software Engineer");
friend.sayName();  //"Nicholas"

其原理就是利用闭包,保有对私有变量和私有方法的引用

优点:不可能有别的方法访问到传入到构造函数中的原始数据。
缺点:没有解决对象识别的问题(即不知道这个对象是什么类型),不能依赖 instanceOf 操作符来确定对象类型;对于对象的方法没有做到复用。

8. ES6 中的 class

咱们这块以 class 实例来展开讲述:

class Parent {
    name = "qck";
    sex = "male";
    //实例变量
    sayHello(name){
        console.log('qck said Hello!',name);
    }
    constructor(location){
      this.location = location;
    }
}

我们来看看这段代码通过 babel 编译后的 _createClass 函数:

var _createClass = function () { 
    function defineProperties(target, props) { 
        for (var i = 0; i < props.length; i++) {
            var descriptor = props[i];

            // 对属性进行数据特性设置
            descriptor.enumerable = descriptor.enumerable || false; // enumerable设置
            descriptor.configurable = true;                         // configurable设置
            if ("value" in descriptor) descriptor.writable = true;  // 如果有value,那么可写
            Object.defineProperty(target, descriptor.key, descriptor);  // 调用defineProperty() 进行属性设置
        } 
    } 
    return function (Constructor, protoProps, staticProps) { 
        // 设置到第一个 Constructor 的 prototype 中
        if (protoProps) defineProperties(Constructor.prototype, protoProps);

        // 设置 Constructor 的 static 类型属性
        if (staticProps) defineProperties(Constructor, staticProps); 

        return Constructor;
    }; 
}();

首先该方法是一个自执行函数,接收的一参是构造函数本身,二参是为构造函数的原型对象需要添加的方法或者属性,三参是需要为构造函数添加的静态属性对象。从这个函数就可以看出 class 在创建自定义类型时,用了原型模式。

我们看看编译后的结果是如何调用 _createClass 的:

var Parent = function () {      // 这里是自执行函数
    _createClass(Parent, [{     // Parent的实例方法,通过修改Parent.prototype来完成
        key: "sayHello",
        value: function sayHello(name) {
            console.log('qck say Hello!', name);
        }
    }]);

    function Parent(location) {     //在Parent构造函数中添加实例属性
        _classCallCheck(this, Parent);
        this.name = "qck";
        this.sex = "male";
        this.location = location;
    }

    return Parent;
}();

function _classCallCheck(instance, Constructor) {
    if (!(instance instanceof Constructor)) {
        throw new TypeError("Cannot call a class as a function"); 
    } 
}

这里调用 _createClass 的地方就证实了我们刚才的想法——确实应用了原型模式:我们的 class 上的方法,其实是通过修改该类 (实际上是函数)prototype 来完成的。

而通过返回的构造函数,我们可以发现:实例属性还是通过构造函数方式来添加的。

最后,我们来看看 _classCallCheck 方法,它其实是一层校验,保证了我们的实例对象是特定的类型。

所以,综上所述,ES6 中的 class 只是个语法糖,它本质上还是用组合使用构造函数模式创建自定义类型的,这也就是为什么我们要学上面那些知识的初衷。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值