js中创建对象方式----原型模式
更新:HHH   时间:2023-1-7


一、什么是原型模式

在js中,创建对象的方式有工厂模式和构造函数模式等; 而构造函数模式最大的问题在于:构造函数中的每个方法都需要在实例对象中重新创建一遍,不能复用,所以为了解决这一个问题,就需要使用原型模式来创建对象。
原型模式是把所有实例共享的方法和属性放在一个叫做prototype(原型)的属性中 ,在创建一个函数时都会有个prototype属性, 这个属性是一个指针,指向一个对象,是通过调用构造函数而创建的那个对象实例的原型对象

 // 构造函数
    function Person() {};

    // 原型属性prototype
    Person.prototype.name = '张三';
    Person.prototype.sayName = function() {
        console.log(this.name);
    };

    let person1 = new Person();
    person1.sayName(); //张三
    let person2 = new Person();
    person2.sayName(); // 张三

    console.log(person1.sayName == person2.sayName); //true
  1. 理解原型对象

    无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象,在默认的情况下,所有的原型对象都自动获得一个constructor(构造函数)属性,这是一个指针,指向prototype属性所在的函数。创建了自定义的构造函数之后,其原型对象默认只会取得constructor属性;其他的方法则是从Object继承来的。
    当调用构造函数创建一个新实例对象后,该实例的内部将包含一个指针[[Prototype]],指向构造函数的原型对象。这个连接存在于实例和构造函数的原型对象之间,而不是存在实例和构造函数之间。
    每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始。如果在实例中找到了就返回该属性的值,没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性,如果在原型对象中找到了这个属性,就返回该属性的值。
    虽然可以通过实例访问保存在原型中的值,但不能通过实例对象重写原型中的值,如果在实例中添加一个在原型中的同名属性,该属性会自动屏蔽原型中的属性,但是不会修改原型中的属性,只会阻止访问原型中的属性,通过delete操作符则可以完全删除实例属性,使得可以重新访问原型中的属性。

  2. 原型与in操作符

    hasOwnProperty()方法可以检测一个属性是否存在于实例对象中,
    // 构造函数
    function Person() {
    this.age = 16;
    };
    Person.prototype.name = "张三";
    let person1 = new Person();
    console.log(person1.hasOwnProperty('name')); // false
    console.log(person1.hasOwnProperty('age')); // true

    in操作符的使用可以分为两类,单独使用和在for-in循环使用,在单独使用时,in操作符会在通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中。
    // 构造函数
    function Person() {}
    Person.prototype.name = 'zhang';
    let person1 = new Person();
    console.log('name' in person1); // true
    person1.age = 14;
    console.log('age' in person1); // true
    同时使用hasOwnProperty()方法和in操作符,可以确定该属性时在原型上还是在存在于对象中。
    // 构造函数
    function Person() {}
    function hasPrototypeProperty(object, name) {
    return !object.hasOwnProperty(name) && (name in object);
    }
    Person.prototype.name = "张三";
    let person = new Person();
    console.log(hasPrototypeProperty(person, 'name')); // true
    console.log(hasPrototypeProperty(person, 'age')); // false
    使用for-in循环时,返回的是所有能够通过对象访问的、可枚举的属性,其中即包含存在于实例中的属性,也包含与存在原型中的属性。
    let o = {
    name: 'san',
    age: 14,
    };
    for(let key in o) {
    console.log(key);
    }
    要取得对象上所有可枚举的实例属性,可以使用Object.keys()方法,接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。
    如果想得到所有实例属性。无论是否可枚举,都可以使用Object.getOwnPropertyNames()方法。

  3. 更简单的原型语法

    为了减少不必要的输入和从视觉上更好的封装原型的功能,常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象。
    // 构造函数
    function Person() {};
    Person.prototype = {
    sayHi: function() {
    console.log(hi);
    },
    name: '张三',
    };
    通过这个方式会导致原型对象中的constructor属性不在指向Person了。如果constructor的值真的很重要,可以像下面这样特意将它设置回适当的值。
    // 构造函数
    function Person() {};
    Person.prototype = {
    constructor: Person,
    sayHi: function() {
    console.log(hi);
    },
    name: '张三',
    };
    但是通过这种方式会导致对象的[[Enumerable]]特性被设置为ture,默认情况下,constructor属性时不可枚举的,可以通过Object.defineProperty()解决这个问题。
    // 构造函数
    function Person() {};
    Person.prototype = {
    sayHi: function() {
    console.log(hi);
    },
    name: '张三',
    };
    Object.defineProperty(Person.prototype, "constructor", {
    enumerable: false,
    value: Person
    }

  4. 原型的动态性

    当对原型对象所做的任何修改都能够立即从实例上反应出来。
    function Person() {};
    var friend = new Person();
    Person.prototype.sayHi = function() {
    console.log('hi');
    };
    friend.sayHi(); // hi

    但是如果是重写整个原型对象,那么情况就不一样了。调用构造函数时会为实例添加一个指向最初原型的[[prototype]]指针,而把原型修改为另外一个对象 就相当于切断了构造函数与最初原型之间的联系。 实例中的指针仅指向原型,而不是指向构造函数。
    // 构造函数
    function Person() {};
    var friend = new Person();
    Person.prototype = {
    constructor: Person,
    sayHi: function() {
    console.log(hi);
    }
    };
    friend.sayHi(); // Uncaught TypeError: friend.sayHi is not a function

    创建了一个Person的实例,然后又重写了其原型对象。但是在使用sayHi()时发生了错误,这个时候实例所指向的原型对象是一个新的对象。重写原型对象切断了现有原型与之前已经存在的对象实例直接的联系。所以报错了。

  5. 原生对象的原型

    原型模式的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型,都采用这种模式,所有的原生引用类型(Object、Array、String)等,都在其构造函数的原型上定义了方法。可以像修改自定义对象的原型一样修改原生对象的原型。

二、原型模式的缺点

对于包含引用类型值的属性来说,所有实例在默认的情况下都会取得相同的属性值。
// 构造函数
function Person() {};
// 原型属性prototype
Person.prototype = {
constructor: Person,
friends: ['张三', '李四'],
}
let person1 = new Person();
let person2 = new Person();
person1.friends.push('王五');
console.log(person1.friends); // ["张三", "李四", "王五"]
console.log(person2.friends); // ["张三", "李四", "王五"]

由于friends存在于Person的原型对象中,所以person1对friends的修改也会通过person2反应出来,但是实例对象一般都是要有属于自己的全部属性,正因为如此,很少有人单独使用原型模式来创建对象。

返回web开发教程...