JavaScript创建对象的各种方式

《JavaScript高级程序设计》一共提到了7种创建对象的方式:

  • 工厂模式
  • 构造函数模式
  • 原型模式
  • 构造函数和原型组合模式
  • 动态原型模式
  • 寄生构造模式
  • 稳妥构造模式

工厂模式

// 工厂模式
      function Person(){
            var o = new Object(); 
            o.name = 'xiaopao';
            o.say = function(){
                alert(this.name)
            };
            return o;
        }

        var person1 = Person();
        console.log(person1);
        console.log(person1 instanceof Person); // false  无法得知实例的类别
        console.log(person1 instanceof Object); // true
        console.log(person1.__proto__ === Object.prototype); // true

优点: 完成了返回一个对象的要求。
缺点:

  1. Person ---> Person.prototype ; person1 = o ; o.proto--->Object.prototype
    无法通过constructor识别对象,以为都是来自Object , 无法得知来自Person
  2. 每次通过Person创建对象的时候,所有的say方法都是一样的,却存储了多次,浪费内存资源。

构造函数模式

// 构造函数模式
function Person(name){
            this.name = name;
            this.say = function(){
                console.log(this.name)
            }
        }

        var person1 = new Person('xiaopao');
        console.log(person1);
        person1.say(); // xiaopao
        console.log(person1 instanceof Person); // true  可以的是实例的类别
        console.log(person1.__proto__ === Person.prototype); // true
        
        Person('xiaopao');
        console.log(name); // 添加了一个全局name属性

优点:
1.可以通过constructor或者instanceof可以识别实例的类型
2.可以通过new 关键字来创建对象实例,更像OO语言中创建对象实例
缺点:
1.多个实例的say方法都是实现一样的效果,但却存储了很多次(存放地址不同)

构造函数模式优化

// 构造函数模式优化
        function Person(name){
            this.name = name;
            this.say = getName;
        }

        function getName(){ // 这样添加了全局方法,这就没所谓的封装性了
            console.log(this.name);
        }

        var person1 = new Person('xiaopao');
        console.log(person1);
        person1.say(); // xiaopao

优点: 解决了每个方法都要被重新创建的问题
缺点:给全局添加方法,无封装性可言。。

原型模式

// 原型模式
function Person(){};

        Person.prototype.name = 'xiaopao';
        Person.prototype.say = function(){
            console.log(this.name);
        }

        var person1 = new Person();
        console.log(person1);
        console.log(person1.name); //xiaopao
        person1.say(); // xiaopao

优点:方法不会重新创建
缺点:
1.所有属性和方法都共享
2.不能初始化参数(不能传参)

原型模式优化

// 原型模式优化
        function Person(){};

        Person.prototype = {
            name: 'xiaopao',
            say: function(){
                console.log(this.name);
            }
        }

        var person1 = new Person();
        console.log(person1.name); // xiaopao
        person1.say(); // xiaopao
        console.log(Person.prototype.constructor); // ƒ Object() { [native code] }   

优点:封装性好一点
缺点:重写了原型,丢失了constructor属性

原型模式优化

// 原型模式优化
        // function Person(){};

        // Person.prototype = {
        //     constructor: Person,
        //     name: 'xiaopao',
        //     say: function(){
        //         console.log(this.name);
        //     }
        // }

        // var person1 = new Person();
        // console.log(person1.name); // xiaopao
        // person1.say(); // xiaopao
        // console.log(Person.prototype.constructor); // Person(){}

优点:实例可以通过constructor属性找到所属构造函数
缺点: 原型模式该有的缺点还是有
原型模式缺点

// 原型模式缺点  属性是引用类型时
        function Person(){};

        Person.prototype.name = 'xiaopao';
        Person.prototype.say = function(){
            console.log(this.name);
        }
        Person.prototype.friends = ['ailse']; // friends属性存储的是内存地址,指向的是同一个数组(引用类型)对象, 当其中一个实例修改数组的值,其他实例访问该属性时值都已改变

        var person1 = new Person();
        var person2 = new Person();
        
        console.log(person1.friends,person2.friends); // ["ailse"] ["ailse"]
        person1.friends.push('lulu');
        console.log(person1.friends,person2.friends); // ["ailse", "lulu"]  ["ailse", "lulu"] 

组合模式
构造函数与原型模式组合

// 组合模式
        function Person(name){
            this.name = name;
            this.friends = ['ailse'];
        }

        Person.prototype = {
            say: function(){
                console.log(this.name);
            }
        }

        var person1 = new Person('xiaopao');
        var person2 = new Person('jianjian');
        console.log(person1.name); // xiaopao
        console.log(person2.name); // jianjian
        person1.say(); // xiaopao
        person2.say(); // jianjian
        person1.friends.push('lulu');
        console.log(person1.friends); // ["ailse", "lulu"]
        console.log(person2.friends); // ["ailse"]

优点:
1.解决了原型模式对于引用对象的缺点
2.解决了原型模式没有办法传递参数的缺点
3.解决了构造函数模式不能共享方法的缺点
缺点:如果构造函数和原型对象放在一起,封装性强再强一点

(先new一个对象,再对原型对象通过对面字面量进行赋值的情况)
直接通过对象字面量给 Person.prototype进行赋值的时候会导致constructor改变,所以需要手动的设置
直接通过对象字面量给Person.prototype进行赋值,会无法作用在之前创建的对象实例上。

function Person(name){
            this.name = name;
            this.friends = ['ailse'];
        }


        var person1 = new Person('xiaopao'); // 报错 Uncaught TypeError: person1.say is not a function
        // Person.prototype.say = function(){
        //     console.log(this.name);
        // } 
        
        Person.prototype.getName = function(){
            console.log(this.name);
        } 

        Person.prototype = { // 用对象字面量的形式必须在new实例之前定义,
            constructor: Person, 
            say: function(){
                console.log(this.name);
            }
        }

        var person2 = new Person('jianjian');
        person1.getName(); // xiaopao
        // person1.say(); //  报错
        
        person2.say(); // jianjian

这是因为对象实例和对象原型直接是通过一个指针链接的,这个指针是一个内部属性[[Prototype]], 可以通过proto访问。我们通过对象字面量修改Person.prototype指向的地址,然而对象实例的proto,并没有跟着一起更新,所以这就导致,实例还是访问着原来的Person.prototype, 所以建议不要通过这种方式去改变 Person.prototype属性

动态原型模式
JavaScript除了function Person, 还有一个Person.prototype,被定义成了两部分。所以,JavaScript对于封装性还是不够完美,而动态原型模式正是致力于要解决这个问题,它把所有的信息都封装在了构造函数中,通过在构造函数中初始化原型,既很好的体现了封装性,又保持了组合使用构造函数和原型模式的特点,一举两得,非常完美。

// 动态原型模式  把原型对象属性也放在构造函数当中(封装性强一点),第一次new实例进行原型对象赋值, 之后不重复赋值, 但是不能通过对象字面量进行赋值
        function Person(name, age, job){
            this.name = name;
            this.age = age;
            this.job = job;
            console.log(typeof this.sayName);
            if(typeof this.sayName != 'function'){  
                Person.prototype.sayName = function(){
                    console.log(this.name);
                };
        
                Person.prototype.sayJob = function(){
                    console.log(this.job);
                };
            }
        }
        
        var p1 = new Person('张三', 18, 'JavaScript');//sayName不存在,添加到原型
        var p2 = new Person('李四', 20, 'Java');//sayName已经存在,不会再向原型添加
        
        p1.sayName();//张三
        p2.sayName();//李四

为什么动态原型模式不能通过对象字面量赋值原型

JavaScript创建对象的各种方式_第1张图片
image.png

JavaScript创建对象的各种方式_第2张图片
image.png

第一次创建实例对象时,先new, 然后执行构造函数,重写原型,那么此时实例的 proto指向的还是原来的原型,不是重写后的原型。 第二次创建实例,因为新原型已经创建好了,所以实例的 proto指向的就是重写的这个原型。使用给原型添加属性的方式操作的一直是同一个原型,所以也就不存在先后的问题。

优点:封装性更优秀
小缺点: 不能使用对象字面量的形式初始化原型

// 动态原型模式优化
        function Person(name) {
            this.name = name;
            if (typeof this.getName != "function") {
                Person.prototype = {
                    constructor: Person,
                    getName: function () {
                        console.log(this.name);
                    }
                }
        
                return new Person(name);
            }
        }
        
        var person1 = new Person('kevin');
        var person2 = new Person('daisy');
        
        person1.getName(); // kevin
        person2.getName();  // daisy

寄生构造函数模式

// 寄生构造函数模式
        function Person(name){
            var o = new Object();
            o.name = name;
            o.say = function(){
                console.log(this.name);
            }
            return o;
        }

        var p1 = new Person('xiaopao');
        console.log(p1);
        console.log(p1 instanceof Person); // false
        console.log(p1 instanceof Object); // true

优点: 和工厂模式基本一样,除了多了个new操作符
缺点: 和工厂模式一样,不能区分实例的类别
这种方法可以在特殊情况下使用。 比如我们想创建一个具有额外方法的特殊数组,但是又不想直接修改Array构造函数,可以这样写:

function SpecialArray() {
    var values = new Array();

    for (var i = 0, len = arguments.length; i < len; i++) {
        values.push(arguments[i]);
    }

    values.toPipedString = function () {
        return this.join("|");
    };
    return values;
}

var colors = new SpecialArray('red', 'blue', 'green');
var colors2 = SpecialArray('red2', 'blue2', 'green2');


console.log(colors);
console.log(colors.toPipedString()); // red|blue|green

console.log(colors2);
console.log(colors2.toPipedString()); // red2|blue2|green2

其实所谓的寄生构造函数模式就是比工厂模式在创建对象的时候,多使用了一个new, 实例上两者的结果是一样的。

但是作者可能是希望能想使用普通Array一样使用SpecialArray, 虽然把SpecialArray当成函数也一样能用,但是这并不是作者的本意,也变得不优雅。

在可以使用其他模式的情况下,不要使用这种模式

稳妥构造函数模式

// 稳妥构造函数模式
        function Person(name){
            var o = new Object();
            o.name = name;
            o.say = function(){
                console.log(name);
            }
            return o;
        }
        var p1 = Person('xiaopao');
        console.log(p1.name); // xiaopao
        p1.say(); // xiaopao
        p1.name = 'lulu';
        p1.say();  // xiaopao
        console.log(p1.name) // lulu

优点:安全, name好像成了私有变量,只能通过say方法去访问
缺点: 不能区分实例的类别

所谓稳妥对象,指的是没有公共属性,而且其方法也不引用this对象。
与计生构造函数模式有两点不同:

  1. 新创建的实例方法不引用this
  2. 不使用new操作符调用构造函数
    稳妥对象最适合在一些安全的环境中。

https://juejin.im/post/5bdf1cf06fb9a049cd53a3a1

你可能感兴趣的:(JavaScript创建对象的各种方式)