构造函数和原型

66 阅读8分钟

一 创建对象

image.png

1.1工厂模式

工厂模式是一种众所周知的设计模式,广泛应用于软件工程领域,用于抽象创建特定对象的过程。

     function createPerson(name, age, job) {
            let o = new Object();
            o.name = name;
            o.age = age;
            o.job = job;
            o.sayName = function() {
                console.log(this.name);
            };
            return o;
            }
    let person1 = createPerson("Nicholas", 29, "Software Engineer");
    let person2 = createPerson("Greg", 27, "Doctor");
    

这里,函数createPerson()接收3个参数,根据这几个参数构建了一个包含Person信息的对象。可以用不同的参数多次调用这个函数,每次都会返回包含3个属性和1个方法的对象。这种工厂模式虽然可以解决创建多个类似对象的问题,但没有解决对象标识问题(即新创建的对象是什么类型)。

1.2 构造函数

image.png

    function Person(name, age, job){
            this.name = name;
            this.age = age;
            this.job = job;
            this.sayName = function() {
                console.log(this.name);
            };
    }
    let person1 = new Person("Nicholas", 29, "Software Engineer");
    let person2 = new Person("Greg", 27, "Doctor");
    person1.sayName();   // Nicholas
    person2.sayName();   // Greg
手写new
         function Person( name, age){
            this.name=name;
            this.age=age
            }
        
         function  Mynew( func,...args){
            // var obj=Object.create(func.prototype)
            // 创建一个空对象,并且指定原型为func.prototype
            var obj={}
            obj.__proto__=func.prototype
            func.call(obj,...args)
            // new构造函数时要执行函数,同时指定this
            return obj;
            // 最后return这个对象
             
         }

          
        let  p= Mynew( Person, 'zs',25)
         console.log(p);

1.4 构造函数的问题

构造函数虽然有用,但也不是没有问题。构造函数的主要问题在于,其定义的方法会在每个实例上都创建一遍

console.log(person1.sayName == person2.sayName); // false

要解决这个问题,可以把函数定义转移到构造函数外部

    function Person(name, age, job){
      this.name = name;
      this.age = age;
      this.job = job;
      this.sayName = sayName;
    }
    function sayName() {
      console.log(this.name);
    }
    let person1 = new Person("Nicholas", 29, "Software Engineer");
    let person2 = new Person("Greg", 27, "Doctor");
    person1.sayName();   // Nicholas
    person2.sayName();   // Greg

因为这一次sayName属性中包含的只是一个指向外部函数的指针,所以person1和person2共享了定义在全局作用域上的sayName()函数。这样虽然解决了相同逻辑的函数重复定义的问题,但全局作用域也因此被搞乱了,因为那个函数实际上只能在一个对象上调用。如果这个对象需要多个方法,那么就要在全局作用域中定义多个函数

二:原型模式

image.png

image.png

image.png

2.1 对象原型

image.png

2.2 constructor

image.png

image.png

2.3 访问原型的方法

  • 对象.proto 不建议使用
  • 构造函数.prototype
  • ECMAScript的Object类型有一个方法叫Object.getPrototypeOf(),返回参数的内部特性[[Prototype]]的值。(原型)

2.4 Object类型还有一个setPrototypeOf()方法,可以向实例的私有特性[[Prototype]]写入一个新值

let biped = {
      numLegs: 2
    };
    let person = {
      name: 'Matt'
    };
    Object.setPrototypeOf(person, biped);
    console.log(person.name);                                      // Matt
    console.log(person.numLegs);                                  // 2
    console.log(Object.getPrototypeOf(person) === biped);   // true

警告 Object.setPrototypeOf()可能会严重影响代码性能 为避免使用Object.setPrototypeOf()可能造成的性能下降,可以通过Object.create()来创建一个新对象,同时为其指定原型:

   let biped = {
      numLegs: 2
    };
    let person = Object.create(biped);
    person.name = 'Matt';
    console.log(person.name);                                      // Matt
    console.log(person.numLegs);                                  // 2
    console.log(Object.getPrototypeOf(person) === biped);   // true

2.5 属性查找原则

在通过对象访问属性时,会按照这个属性的名称开始搜索。搜索开始于对象实例本身。如果在这个实例上发现了给定的名称,则返回该名称对应的值。如果没有找到这个属性,则搜索会沿着指针进入原型对象,然后在原型对象上找到属性后,再返回对应的值

 function Person() {}
    Person.prototype.name = "Nicholas";
    Person.prototype.age = 29;
    Person.prototype.job = "Software Engineer";
    Person.prototype.sayName = function() {
      console.log(this.name);
    };
    let person1 = new Person();
    let person2 = new Person();
    person1.name = "Greg";
    console.log(person1.name);   // "Greg",来自实例
    console.log(person2.name);   // "Nicholas",来自原型

当console.log()访问person1.name时,会先在实例上搜索个属性。因为这个属性在实例上存在,所以就不会再搜索原型对象了。而在访问person2.name时,并没有在实例上找到这个属性,所以会继续搜索原型对象并使用定义在原型上的属性

hasOwnProperty()方法用于确定某个属性是在实例上还是在原型对象上。这个方法是继承自Object的,会在属性存在于调用它的对象实例上时返回true

 function Person() {}
    Person.prototype.name = "Nicholas";
    Person.prototype.age = 29;
    Person.prototype.job = "Software Engineer";
    Person.prototype.sayName = function() {
      console.log(this.name);
    };
    let person1 = new Person();
    let person2 = new Person();
    console.log(person1.hasOwnProperty("name")); // false
    person1.name = "Greg";
    console.log(person1.name); // "Greg",来自实例
    console.log(person1.hasOwnProperty("name")); // true
    console.log(person2.name); // "Nicholas",来自原型
    console.log(person2.hasOwnProperty("name")); // false
    delete person1.name;
    console.log(person1.name); // "Nicholas",来自原型
    console.log(person1.hasOwnProperty("name")); // false

2.6 其它原型语法

在前面的例子中,每次定义一个属性或方法都会把Person.prototype重写一遍。为了减少代码冗余,也为了从视觉上更好地封装原型功能,直接通过一个包含所有属性和方法的对象字面量来重写原型成为了一种常见的做法,如下面的例子所示

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

在这个例子中,Person.prototype被设置为等于一个通过对象字面量创建的新对象。最终结果是一样的,只有一个问题:这样重写之后,Person.prototype的constructor属性就不指向Person了。在创建函数时,也会创建它的prototype对象,同时会自动给这个原型的constructor属性赋值。而上面的写法完全重写了默认的prototype对象,因此其constructor属性也指向了完全不同的新对象(Object构造函数),不再指向原来的构造函数。 虽然instanceof操作符还能可靠地返回值,但我们不能再依靠constructor属性来识别类型了,如下面的例子所示:

    let friend = new Person();
    console.log(friend instanceof Object);        // true
    console.log(friend instanceof Person);        // true
    console.log(friend.constructor == Person);   // false
    console.log(friend.constructor == Object);   // true
    这里,instanceof仍然对ObjectPerson都返回true。但constructor属性现在等于Object而不是Person

如果constructor的值很重要,则可以像下面这样在重写原型对象时专门设置一下它的值:

    function Person() {
    }
    Person.prototype = {
      constructor: Person,
      name: "Nicholas",
      age: 29,
      job: "Software Engineer",
      sayName() {
        console.log(this.name);
      }
    };
    这次的代码中特意包含了constructor属性,并将它设置为Person,保证了这个属性仍然包含恰当的值。

但要注意,以这种方式恢复constructor属性会创建一个[[Enumerable]]为true的属性。而原生constructor属性默认是不可枚举的

因此,如果你使用的是兼容ECMAScript的JavaScript引擎,那可能会改为使用Object.defineProperty()方法来定义constructor属性:

       function Person() {}
    Person.prototype = {
      name: "Nicholas",
      age: 29,
      job: "Software Engineer",
      sayName() {
        console.log(this.name);
      }
    };
    // 恢复constructor属性
    Object.defineProperty(Person.prototype, "constructor", {
      enumerable: false,
      value: Person
    });

2.7 原型的动态性

因为从原型上搜索值的过程是动态的,所以即使实例在修改原型之前已经存在,任何时候对原型对象所做的修改也会在实例上反映出来。下面是一个例子

 let friend = new Person();
    Person.prototype.sayHi = function() {
      console.log("hi");
    };
    friend.sayHi();    // "hi",没问题!

以上代码先创建一个Person实例并保存在friend中。然后一条语句在Person.prototype上添加了一个名为sayHi()的方法。虽然friend实例是在添加方法之前创建的,但它仍然可以访问这个方法。之所以会这样,主要原因是实例与原型之间松散的联系。在调用friend.sayHi()时,首先会从这个实例中搜索名为sayHi的属性。在没有找到的情况下,运行时会继续搜索原型对象。因为实例和原型之间的链接就是简单的指针,而不是保存的副本,所以会在原型上找到sayHi属性并返回这个属性保存的函数

虽然随时能给原型添加属性和方法,并能够立即反映在所有对象实例上,但这跟重写整个原型是两回事 重写整个原型会切断最初原型与构造函数的联系,但实例引用的仍然是最初的原型。记住,实例只有指向原型的指针,没有指向构造函数的指针。来看下面的例子

    function Person() {}
    let friend = new Person();
    Person.prototype = {
      constructor: Person,
      name: "Nicholas",
      age: 29,
      job: "Software Engineer",
      sayName() {
        console.log(this.name);
      }
    };
    friend.sayName();   // 错误
    在这个例子中,Person的新实例是在重写原型对象之前创建的。在调用friend.sayName()的时候,会导致错误

2.8 原生对象原型

原型模式之所以重要,不仅体现在自定义类型上,而且还因为它也是实现所有原生引用类型的模式。所有原生引用类型的构造函数(包括Object、Array、String等)都在原型上定义了实例方法

比如,数组实例的sort()方法就是Array.prototype上定义的,而字符串包装对象的substring()方法也是在String.prototype上定义的,如下所示:

console.log(typeof Array.prototype.sort); // "function" 
console.log(typeof String.prototype.substring); // "function"

通过原生对象的原型可以取得所有默认方法的引用,也可以给原生类型的实例定义新的方法。可以像修改自定义对象原型一样修改原生对象原型,因此随时可以添加方法。比如,下面的代码就给String原始值包装类型的实例添加了一个startsWith()方法

    String.prototype.startsWith = function (text) {
      return this.indexOf(text) === 0;
    };
    let msg = "Hello world! ";
    console.log(msg.startsWith("Hello"));   // true

注意 尽管可以这么做,但并不推荐在产品环境中修改原生对象原型。这样做很可能造成误会,而且可能引发命名冲突(比如一个名称在某个浏览器实现中不存在,在另一个实现中却存在)。另外还有可能意外重写原生的方法。推荐的做法是创建一个自定义的类,继承原生类型。