JavaScript--创建对象的方法总结

JavaScript常被描述为一种基于原型的语言–每个对象拥有一个原型对象,对象以其原型为模板,从原型集成方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层,以此类推。这种关系常被称为原型链,它解释了为何一个对象会拥有定义在其他对象中的属性和方法,

准确的说,这些属性和方法是定义在Object的构造函数上的prototype属性上。传统的OOP中,首先定义“类”,此后创建对象实例时,类中定义的所有属性和方法都被复制到实例中。在JavaScript中并不如此复制–而是在对象实例和它的构造器之间建立一个链接(它是_proto_ 属性,从构造函数的prototype属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法。

创建对象有多种方式,一种是工厂模式:

function createPerson (name, age, job) {
      var o = new Object();
      o.name = name;
      o.age = age;
      o.job = job;
      o.sayName = function () {
        console.log(this.name);
      };
      return o;
    }

    var person1 = new createPerson('Lele', '12', 'study');
    var person2 = new createPerson('Niuniu', '11', 'study');

    person1.sayName()//Lele
    person2.sayName()//Niuniu

函数createPerson()能够根据接受的参数来构建一个包含所有必要信息的Person对象,可以无数次的调用这个函数,每次它都会返回一个包含三个属性一个方法的对象,但是却没有解决对象识别的问题(即怎样知道一个对象的类型),随着JavaScript的发展,又一个新模式出现了。
构造函数模式

ECMAScript中的构造函数可以用来创建特定类型的对象。像Object和Array这样的原生构造函数,在运行时会自动出现在执行环境中。此外,也可以创建自定义的构造函数,从而自定义对象类型的属性和方法,可以使用构造函数模式将前面的例子重写如下:

 function Person (name, age, job) {
      this.name = name;
      this.age = age;
      this.job = job;
      this.sayName = function () {
        console.log(this.name);
      };
    }

    var person1 = new Person('Lele', '12', 'study');
    var person2 = new Person('Niuniu', '11', 'study');

    person1.sayName()
    person2.sayName()

在这个例子中,Person()函数取代了createPerson()函数,两个函数存在以下不同:

  • 没有显示的创建对象
  • 直接将属性和方法赋给了this
  • 没有return语句

另外,两个函数的命名方式也稍有不同,在构造函数中,函数名字第一个字母大写。按照惯例,构造函数始终都应该以第一个字母大写,而非构造函数则应以第一个小写字母开头。

使用构造函数创建新实例,必须使用new操作符,以这种方法调用构造函数实际上会经历以下四个步骤:

  1. 创建一个新对象
  2. 将构造函数的作用域赋给新对象(因此this就指向了这个新对象)
  3. 执行构造函数里面的代码(为这个新对象添加属性)
  4. 返回新对象

在前面例子的最后,person1和person2分别保存了Person的一个不同的实例,这两个对象都有一个constructor(构造函数)属性,该属性指向Person,如下所示:

	console.log(person1.constructor == Person) //true
    console.log(person2.constructor == Person) //true

对象的constructor属性最初是用来标识对象类型的。但是提到检测类型,还是instanceof更可靠一些,我们在这个例子中创建的所有对象既是Object对象,同时也是Person的实例,这一点可以通过instance操作符得到验证:

    console.log(person1 instanceof Object) //true
    console.log(person1 instanceof Person) //true
    console.log(person2 instanceof Object) //true
    console.log(person1 instanceof Person) //true

创建自定义构造函数意味着将来可以将它的实例标识为一种特定的类型;而这正是构造函数模式胜过工厂模式的地方。在这个例子中,person1和person2之所以同时是Object的实例,是因为所有对象都继承自Object。

构造函数

构造函数也是函数,构造函数和普通函数惟一的区别就是调用方式的不同,也就是只要是通过关键字new调用的函数,就是构造函数,例如前面定义的Person()函数可以通过以下任何一个方法调用。

方法一: 当做构造函数使用

 var person1 = new Person('Lele', '12', 'study');
 person1.sayName() //Lele

方法二: 作为普通函数调用

	Person('Niuniu', '11', 'study'); //添加到window
    window.sayName(); //Niuniu

    var o = new Object()
    Person.call(o, 'Huahua', 8, 'study');
    o.sayName(); //Huahua

方法三:在另一个对象的作用域中调用

    var o = new Object();
    Person.call(o, "Kristen", 25, "Nurse");
    o.sayName(); //"Kristen"

构造函数的问题

构造函数模式虽然好用,但是并非没有缺点。使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。在前面的例子中,person1和person2都有名为sayName()的方法,但那两个方法不是同一个Function的实例。不要忘了–ECMAscript中的函数也是对象,因此每定义一个函数,也就是实例化了一个对象,从逻辑角度讲,此时的构造函数也可以这样定义:

 function Person(name, age, job) {
       this.name = name;
       this.age = age;
       this.job = job;
       this.sayName = new Function("console.log(this.name)")
   }

   var person1 = new Person('Mary', '12', 'student');
   person1.sayName() // Mary

从这个角度来看构造函数,更容易明白每个Person实例都包含一个不同的Function实例(以显示name属性) 的本质。说明白些,以这种方式创建函数,会导致不同的作用域和标识符解析,但创建Function新实例的机制仍然是相同的。因此,不同实例上的同名函数是不相等的,所以以下代码可以证明这一点。

   var person1 = new Person('Mary', '12', 'student');
   var person2 = new Person('Alice', 16, 'student');
   console.log(person1.sayName == person2.sayName) // false

然而,创建两个完成同样任务的Function实例确实是没有必要;况且有this对象在,根本不用再执行代码前就把函数绑定到特定对象上面。因此大可以像下面这样,通过把函数定义转移到构造函数外部来解决问题。

   function Person(name, age, job) {
       this.name = name;
       this.age = age;
       this.job = job;
       this.sayName = sayName;
   }

   function sayName() {
       console.log(this.name)
   }

   var person1 = new Person('Mary', '12', 'student');
   var person2 = new Person('Alice', 16, 'student');
   console.log(person1.sayName == person2.sayName); //true

这样person1和person2对象就共享了在全局作用域中定义的同一个sayName()函数。这样做确实解决了两个函数做同一件事的问题,可是新问题又来了,在全局作用域中定义的函数实际只能被某个对象调用,这让全局作用域有点名不副实。而更让人无法接受的是:如果对象需要定义多个方法,那么就要定义很多个全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了。好在,这些问题可以通过使用原型模式来解决。

原型模式

我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法,所有我们添加到原型对象上的方法和属性就会被所有的实例共享(),示例代码如下:

  function Person() {

  }
  Person.prototype.name = "Nichola";
  Person.prototype.age = 14;
  Person.prototype.job = "student";
  Person.prototype.sayName = function () {
      console.log(this.name)
  }
  var person1 = new Person();
  console.log("Person1: ");
  person1.sayName(); //Nichola
  var person2 = new Person();
  console.log("Person2: ");
  person2.sayName(); //Nichola

我们可以看到,原型继承实现了属性和方法的共用,但是完全的共用又导致创建的所有实例都是一样的,这样就失去了创建实例的意义,那么有没有这么一种方法,既能实现共用属性方法的共享,又能保留自身的私有属性呢?答案是,有的,就是组合继承。

组合模式

造函数可以用来创建特定类型的对象,就像我们前面讲到的,构造函数也是函数,只是通过new调用,就成了构造函数,所以普通函数有的东西,构造函数也是有的,比如原型对象。所以我们可以把私有的属性放在构造函数里面,把共有方法放在原型对象里面,用构造函数模式和原型模式组合的方式创建实例,示例代码如下:

 function Person(name, age, job) {
      this.name = name;
      this.age = age;
      this.job = job;
  }

  Person.prototype.sayName = function () {
      console.log(this.name)
  }
  var person1 = new Person('Mary', 14, 'student');
  person1.sayName(); //Mary
  
  var person2 = new Person('Alice', 14, 'student');
  person2.sayName(); //Alice

  console.log(person1.sayName == person2.sayName) //true

原型不太懂得同学可以参考一下另一篇文章:原型详解

写的不好的地方还请海涵,如果哪里不明白欢迎评论区指出或者私信我,谢谢

猜你喜欢

转载自blog.csdn.net/DayDreamWMM/article/details/82593877