zoukankan      html  css  js  c++  java
  • JS创建对象的几种方式

    在 JavaScript 中虽然 Object 构造函数或对象字面量都可以用来创建单个对象,但是这些方法都有一个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码。为了解决这些问题,人们提出了很多对象创建的解决办法。

    在了解这一章的内容之前,最好先回顾一下js的原型继承:

    js原型继承与原型链(一)

    js原型继承与原型链(二)

    js原型继承与原型链(三)

    工厂模式

    工厂模式是软件工程领域一种广为人知的设计模式,这种模式抽离了创建对象的具体过程。考虑到在 ECMAScript 中无法创建类,开发人员发明以一种函数,用函数来封装以特定接口创建对象的细节。如下面的例子所示

    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("james",9,"student");
    
    var person2 = createPerson("kobe",9,"student");

    优点:解决了创建多个相似对象时,代码的复用问题

    缺点:使用工厂模式创建的对象,没有解决对象识别的问题(就是怎样知道一个对象的类型是什么)

    构造函数模式

     构造函数模式在前面的原型继承中都有涉及,这里就不多做展开了。

    function createPerson(name, age, job){
        this.name = name;
        this.age = age;
        this.job = job;
        this.sayName = function(){
            alert(this.name);
        };
        return o;
    }
    
    var person1 = new createPerson("james",9,"student");
    
    var person2 = new createPerson("kobe",9,"student");

    优点:解决了工厂模式中对象类型无法识别的问题,并且创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型。

    缺点:我们知道 ECMAScript 中的函数是对象,在使用构造函数创建对象时,每个方法都会在实例对象中重新创建一遍。拿上面的例子举例,这意味着每创建一个对象,我们就会创建一个 sayName 函数的实例,但它们其实做的都是同样的工作,因此这样便会造成内存的浪费。

    原型模式

    我们知道,我们创建的每一个函数都有一个 prototype 属性,这个属性指向函数的原型对象,这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。我们通过使用原型对象可以让所有的对象实例共享它所包含的属性和方法,因此这样也解决了代码的复用问题。

    function Person(){
    
    }
    
    Person.prototype.name = "james";
    Person.prototype.age = 9;
    Person.prototype.job = "student";
    Person.prototype.sayName = function(){
        alert(this.name);
    }
    
    var person1 = new Person();
    person1.sayName(); // "james"
    
    var person2 = new Person();
    person2.sayName(); // "james"
    
    
    console.log(person1.sayName === person2.sayName)

    优点:解决了构造函数模式中多次创建相同函数对象的问题,所有的实例可以共享同一组属性和函数。

    缺点:

    • 首先第一个问题是原型模式省略了构造函数模式传递初始化参数的过程,所有的实例在默认情况下都会取得默认的属性值,会在一定程度上造成不方便。
    • 因为所有的实例都是共享一组属性,对于包含基本值的属性来说没有问题,但是对于包含引用类型的值来说(例如数组对象),所有的实例都是对同一个引用类型进行操作,那么属性的操作就不是独立的,最后导致读写的混乱。我们创建的实例一般都是要有属于自己的全部属性的,因此单独使用原型模式的情况是很少存在的。

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

    创建自定义类型的最常见方式,就是组合使用构造函数模式和原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。使用这种模式的好处就是,每个实例都会拥有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。而且这中混成模式还支持向构造函数传递参数,可以说是及两种模式之长。

    function Person(name, age, job){
        this.name = name;
        this.age = age;
        this.job = job;
    }
    
    Person.prototype = {
        constructor: Person,
        sayName: function(){
            alert(this.name);
        }
    }
    
    var person1 = new createPerson("james",9,"student");
    
    var person2 = new createPerson("kobe",9,"student");
    
    console.log(person1.name); // "james"
    console.log(person2.name); // "kobe"
    console.log(person1.sayName === person2.sayName); // true

    优点:采用了构造函数模式和原型模式的优点,这种混成模式是目前使用最广泛,认同度最高的一种创建自定类型的方法。

    缺点:由于使用了两种模式,因此对于代码的封装性来说不是很好。

    动态原型模式

    由于上面混成模式存在封装性的问题,动态原型模式是解决这个问题的一个方案。这个方法把所有信息都封装到了构造函数中,而在构造函数中通过判断只初始化一次原型。

    function Person(name, age, job){
        this.name = name;
        this.age = age;
        this.job = job;
    
        if(typeof this.sayName !== "function" ){
    
            Person.prototype.sayName: function(){
                alert(this.name);
            } 
        } 
    }
    
    var person1 = new createPerson("james",9,"student");
    
    person1.sayName(); // "james"

    注意在 if 语句中检查的可以是初始化后应该存在的任何属性或方法,不必要检查每一个方法和属性,只需要检查一个就行。

    优点:解决了混成模式中封装性的问题

    寄生构造函数模式

    如果在前面几种模式不适用的情况下,可以使用寄生构造函数模式。这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后返回新创建的对象。

    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 person1 = new Person("james",9,"student");
    那么两者有什么功能上的差别呢?事实上,两者本质上的差别仅在于new操作符(因为函数取什么名字无关紧要),工厂模式创建对象时将createPerson看作是普通的函数,而寄生构造函数模式创建对象时将Person看作是构造函数,不过这对于创建出的对象来说,没有任何差别

    注意在构造函数不返回值的情况下,默认会返回新创建的对象,而通过在构造函数的末尾添加一个 return 语句,可以重写调用构造函数时返回的值。

    构造函数和普通函数的区别在于:当使用new+构造函数创建对象时,如果构造函数内部没有return语句,那么默认情况下构造函数将返回一个该类型的实例(如果以上面的例子为参考,person1和person2为Person类型的对象实例,可以使用person1 instanceof Person检验),但如果构造函数内部通过return语句返回了一个其它类型的对象实例,那么这种默认的设置将被打破,构造函数最终返回的实例类型将以return语句中对象实例的类型为准。

    基于这个规则,在Person()构造函数中,由于最后通过return语句返回了一个Object类型的对象实例,所以通过该构造函数创建的对象实际上是Object类型而不是Person类型;这样一来就与createPerson()函数返回的对象类型相同,因此可以说工厂模式和寄生构造函数模式在功能上是等价的

    如果非要说两者的不同,并且要从其中选择一个作为创建对象的方法的话,我个人更偏向于寄生构造函数模式一些。这是因为new Person()(寄生构造函数模式)更能让我感觉到自己正在创建一个对象,而不是在调用一个函数(工厂模式)。

    优点:我对这个模式的理解是,它主要是基于一个已有的类型,在实例化时对实例化的对象进行扩展。这样既不用修改原来的构造函数,也达到了扩展对象的目的。

    缺点:和工厂模式一样的问题,不能依赖 instanceof 操作符来确定对象的类型。

    稳妥构造函数模式

    Douglas Crockford 发明了 JavaScript 中的稳妥对象这个概念。所谓稳妥对象,指的就是,没有公共属性,而且其方法也不使用 this 的对象。稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用 this 和 new),或者在防止数据被其他应用程序改动时使用。

    稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建的对象的实例方法不引用 this ;二是不使用 new 操作符调用构造函数。因此我们可以将前面的例子改写如下:

    function Person(para_name, para_age, para_job) {
        //创建要返回的对象
        var o = {};
    
        //在这里定义私有属性和方法
        var name = para_name;
        var age = para_age;
        var job = para_job;
    
        var sayAge = function() {
            alert(age);
        };
    
        //在这里定义公共方法
        o.sayName = function() {
            alert(name);
        };
    
        //返回对象
        return o;
    }
    
    var person1 = Person("Nicholas", 29, "Software Engineer");    //创建对象实例
    person1.sayName();    //Nicholas
    person1.name;         //undefined
    person1.sayAge();     // 报错

    稳妥构造函数模式与前面介绍的两种设计模式具有相似的地方,都是在函数内部定义好对象之后返回该对象来创建实例。然而稳妥构造函数模式的独特之处在于具有以下特点:

    • 没有通过对象定义公共属性
    • 在公共方法中不使用this引用对象自身
    • 不使用new操作符调用构造函数

    这种设计模式最适合在一些安全的环境中使用(这些环境中会禁止使用this和new);为了较好地理解这种设计模式,我们可以采取类比的方法——这种构造对象的方式就如同C++/Java语言中通过访问控制符private定义出包含私有成员的类的方式一样(将上例按C++中类的方式来定义):

    class Person {
    //定义私有成员变量和函数
    private: 
        string name;
        int age;
        string job;
        int sayAge() {return age;}
    //定义构造函数和公共方法(函数)
    public:
        string sayName() {return name;}    //公共方法
        Person(string p_name, int p_age, string p_job):name(p_name),age(p_age),job(p_job) {}  //构造函数
    }
    
    //创建对象实例
    Person person1("Nicholas", 29, "Software Engineer");
    person1.sayName();    //Nicholas
    person1.name;         //报错(无法访问)
    person1.sayAge();     //报错(无法访问)

    可见,利用C++定义出了一个Person类,其中的nameagejob以及sayAge()是私有成员,无法通过类似person1.name的方式直接访问,这是一种类的保护机制;而定义为publicsayName()函数则可以直接访问。

    JS中的稳妥构造函数模式正是为了实现这样的数据保护机制。它巧妙地利用了函数的作用域实现了对象属性的私有化:在函数中定义的变量是局部变量,按道理本应该在函数执行完毕退出后进行销毁或清理,但由于通过对象的公共方法对该局部变量保持着引用,所以该变量即便是在构造函数退出之后也依然保持有效(闭包)。

    这样一来,创建出的对象既能通过公共方法提供的访问接口对私有属性进行访问(引用的是构造函数的局部变量),也能保证无法通过对象自身对其直接访问(person1.name无法访问到对应数据,因为name是构造函数的局部变量而不是对象的属性),从而保证了对象属性的访问安全。




    参考资料:
    https://www.jianshu.com/p/0e400b9de976
    http://cavszhouyou.top/JavaScript%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%E4%B9%8B%E5%AF%B9%E8%B1%A1%E5%88%9B%E5%BB%BA.html
  • 相关阅读:
    用tensorflow构建神经网络学习简单函数
    Entity Framework 6.0 Tutorials(1):Introduction
    Entity Framework Tutorial Basics(43):Download Sample Project
    Entity Framework Tutorial Basics(42):Colored Entity
    Entity Framework Tutorial Basics(41):Multiple Diagrams
    Entity Framework Tutorial Basics(40):Validate Entity
    Entity Framework Tutorial Basics(39):Raw SQL Query
    Entity Framework Tutorial Basics(38):Explicit Loading
    Entity Framework Tutorial Basics(37):Lazy Loading
    Entity Framework Tutorial Basics(36):Eager Loading
  • 原文地址:https://www.cnblogs.com/fmyao/p/12586434.html
Copyright © 2011-2022 走看看