面向对象的程序设计之JS创建对象的9种模式及其优缺点


目录

  • 1.new Object ()
  • 2.字面式创建对象
  • 3.工厂模式
  • 4.构造函数模式
    • 4.1.将构造函数当作函数
    • 4.2.构造函数的问题
  • 5.原型模式
    • 5.1.理解原型对象
    • 5.2.原型与in操作符
    • 5.3.更简单的原型语法
    • 5.4.原型的动态性
    • 5.5.原生对象的原型
    • 5.6.原型对象的问题
  • 6.组合使用构造函数模式和原型模式
  • 7.动态原型模式
  • 8.寄生构造函数模式
  • 9.稳妥构造函数模式

1.new Object ()
var person = new Object(); person.name = "Echo"; person.age = 18; person.say = function(){ console.log(this.name); }

2.字面式创建对象
var person ={ name: "Echo", age: 18, say: function(){ console.log(this.name); } };

3.工厂模式 虽然Object构造函数或对象字面量都可以用来创建单个对象,但这些方式有个明显的缺点:使用一个接口创建很多对象,会产生大量的重复代码。为解决这个问题,开始使用工厂模式的一种变体。
这种模式抽象了创建具体对象的过程,用函数来封装以特定接口创建对象的细节。
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 = createPerson("Echo",18,"Software Engineer"); var person2 = createPerson("yya",17,"Doctor");

createPerson()函数能够根据接受的参数来构建一个包含所有必要信息的Person对象。可以无数次地调用这个函数,而每次它都会返回一个包含三个属性一个方法的对象。工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题,即怎样知道一个对象的类型。
4.构造函数模式 构造函数可用来创建特定类型的对象。像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("Nicho1as",18,"Software Engineer") ; var person2 = new Person("yya",17,"Doctor");

Person()函数取代了createPerson()函数。Person()中的代码除了与createPerson()中相同的部分外,还存在以下不同之处:
  • 没有显式的创建对象;
  • 直接将属性和方法赋给了this对象;
  • 没有return语句;
  • 数首字母大写;
按照惯例,构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头。主要是为了区别于其他函数,因为构造函数本身也是函数,只不过可以用来创建对象而已。
经典:要创建Person的新实例,必须使用new操作符。以这种方式调用构造函数实际上会经历以下4个步骤:
  1. 创建一个新对象;
  2. 将构造函数的作用域赋给新对象(因此this就指向了这个新对象);
  3. 执行构造函数中的代码(为这个新对象添加属性);
  4. 返回新对象.
person1和person2是Person的一个不同的实例。都存在一个constructor(构造函数)属性,该属性指向 Person。对象的constructor属性最初是用来标识对象类型的。这个例子中创建的所有对象既是Object的实例,同时也是Person的实例,通过instanceof操作符可以得到验证。
person1.constructor == Person; //true person2.constructor == Person; //trueperson1 instanceof Object; //true person1 instanceof Person; //true person2 instanceof Object; //true person2 instanceof Person; //true

创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型,而这正是构造函数模式胜过工厂模式的地方。
4.1.将构造函数当作函数
构造函数与其他函数的唯一区别,就在于调用它们的方式不同。不过,构造函数毕竟也是函数,不存在定义构造函数的特殊语法。任何函数,只要通过new操作符来调用那它就可以作为构造任何雨数。如果不通过new操作符来调用,那它跟普通函数也不会有什么两样。
Person()函数可以通过下列任何一种方式来调用:
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; this.sayName = function(){ console.log(this.name); }; }; // 1.作为构造函数使用 var person = new Person("Echo",18,"Software Engineer"); person.sayName(); // Echo// 2.作为普通函数调用 Person("yya",17,"Doctor"); // 添加到window, window.sayName(); // yya// 3.在另一个对象的作用域中调用 var o = new Object(); Person.call(o,"Kristen",25,"Nurse"); o.sayName(); // Kristen

  1. 作为构造函数调用:使用new操作符来创建一个新对象。
  2. 作为普通函数调用:不使用new操作符,属性和方法都被添加给window对象上。全局作用域中调用一个函数时,this对象总是指向window对象。
  3. 在另一个对象的作用域中调用:可以使用call()或者apply()在某个特殊对象的作用域中调用Person()函数。因此调用后o就拥有了所有属性和方法。
4.2.构造函数的问题
使用构造函数的主要问题:就是每个方法都要在每个实例上重新创建一遍。因此每个Person实例都包含一个不同的Function实例,不同实例上的同名函数是不相等的,以这种方式创建函数,会导致不同的作用域链和标识符解析。
然而,创建两个完成同样任务的Function实例的确没有必要;可像下面这样,通过把函数定义转移到构造函数外部来解决这个问题。
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; this.say = sayName; }; function sayName(){ console.log(this.name); } var personl = new Person("Echo", 18,"Software Engineer"); var person2 =new Person("yya",17,"Doctor"); personl.sayName(); // Echo person2.sayName()// yya

将sayName()函数的定义到了构造函数外部。而在构造函数内部,将say属性设置成等于全局的sayName函数。这样一来,由于say包含的是一个指向函数的指针,因此person1和person2对象就共享了在全局作用域中定义的同一个sayName()函数。
这样做确实解决了两个函数做同一件事的问题,可是新问题又来了:在全局作用域中定义的函数实际上只需要被某个对象调用,这让全局作用域有点名不副实。而更让人无法接受的是:如果对象需要定义很多方法,那么就要定义很多个全局函数,此时这个自定义的引用类型就丝毫没有封装性可言。好在,这些问题可以通过使用原型模式来解决。
5.原型模式 创建的每个函数都有一个prototype原型属性,是一个指针指向一个对象,而这个对象的用途是包含特定类型的所有实例共享的属性和方法。那么prototype就是通过调用构造函数而创建的那个对象实例原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。
function Person(){}; Person.prototype.name = "Echo"; Person.prototype.age = 18; Person.prototype.job = "Software Engineer"; Person.prototype.sayName = function(){ console.log(this.name); }; var person1 = new Person(); var person2 = new Person(); person1.sayName(); // Echo person2.sayName(); // Echo console.log(person1.sayName == person2.sayName); // true

将所有属性和方法直接添加到了 Person的prototype上,构造函数变成了空函数。即便如此,也仍然可以通过调用构造函数来创建新对象,且新对象会具有相同的性和方法。这些属性和方法是由所有实例共享的。
5.1.理解原型对象
只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性。这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor构造函数属性,这个属性是一个指向prototype属性所在函数的指针。
创建了自定义的构造函数之后,其原型对象默认只会取得constructor属性; 至于其他方法,则都是从object 继承而来的。当调用构造函数创建一个新实例后,该实例的内部将包含一个内部属性指针,指向构造数的原型对象。ECMA-262第5版中管这个指针叫[[Prototype]]。虽然在脚本中没有标准的方式访问[[Prototype]],但Firefox、Safari 和Chrome 在每个对象上都支持一个属性\_\_proto\_\_这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。
Person.prototype指向了原型对象,Person.prototype.constructor又指回了 Person。
原型对象中除了包含 constructor属性之外,还包含后来添加的其他属性。Person的每个实例都包含一个内部属性,该属性指向了Person.prototype。
虽然所有实现中都无法直接访问[[Prototype]],但可以通过isPrototypeOf()方法来确定对象之间是否存在原型关系。从本质上讲,如果对象实例的__proto__指向调用isPrototypeof()方法的对象的原型对象,那么就返回true。
console.log(Person.prototype.isPrototypeOf(person1)); // true console.log(Person.prototype.isPrototypeof(person2)); // true

原型Person对象的isPrototypeOf()方法测试了 person1和person2。因为它们内部都有一个指向 Person.prototype 的指针,因此都返回了true
ES5增加了一个新方法,叫Object.getPrototypeof(),在所有支持的实现中。这个方法返回[[Prototype]]的值。 返回的对象实际就是这个对象的原型。
console.log(Object.getPrototypeOf(person1) == Person.prototype); // true console.log(Object.getPrototypeOf(person1).name); // "Echo

每当代码读取某个对象的某个属性时,都会执行一次搜索,搜索首先从对象实例本身开始。如果在实例中找到了该属性,则返回该属性的值;若未找到。则继续搜索指针指向的原型对象,在原型对象中查找该属性。若在原型对象中找到该属性,则返回该属性的值。而这正是多个对象实例共享原型所保存的属性和方法的基本原理。
虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。在实例中添加了一个属性,而该属性与实例原型中的属性同名,那就在实例中创建该属性,该属性将会屏蔽原型中的那个同名属性。
function Person(){}Person.prototype.name = "Echo" ; var person1 = new Person(); var person2 = new Person(); person1.name = "yya"; console.log(person1.name); //"yya" --来自实例 console.log(person2.name); //"Echo" --来自原型

添加这个属性只会屏蔽我们访问原型中的那个属性,但不会修改那个属性。使用 delete 操作符可以删除实例属性,从而能够重新访问原型中的属性。
function Person(){}Person.prototype.name = "Echo" ; var person1 = new Person(); var person2 = new Person(); person1.name = "Greg"; console.log(person1.name); // "Greg" --来自实例 console.log(person2.name); // "Echo" --来自原型 delete person1.name; console.log(person1.name); // "Echo" --来自原型

使用hasOwnProperty()方法可以检测一个属性是存在于实例中,还是原型中。只有给定属性存在于对象实例中时,才会返回true。
function Person(){}Person.prototype.name = "Echo" ; var person1 = new Person(); var person2 = new Person(); console.log(person1.hasOwnProperty("name")); // falseperson1.name = "yya"; console.log(person1.name); // "yya" - 来自实例 console.log(person1.hasOwnProperty("name")); // trueconsole.log(person2.name); // "Echo" - 来自原型 console.log(person2.hasOwnProperty("name")); // falsedelete person1.name; console.log(person1.name); // "Echo" -来自原型 console.log(person1.hasOwnProperty("name")); // false

ES5的Object.getOwnPropertyDescriptor()方法只能用于实例属性,要取得原型属性的描述符,必须直接在原型对象上调用Object.getOwnPropertyDescriptor()方法。
function Person(){}Person.prototype.name = "Echo" ; var person1 = new Person(); var person2 = new Person(); person1.name = "yya"; console.log(Object.getOwnPropertyDescriptor(person1, 'name')); console.log(Object.getOwnPropertyDescriptor(Person.prototype, 'name'));

5.2.原型与in操作符
有两种方式使用in操作符:单独使用在for-in循环中使用
单独使用时in操作符:对象能够访问指定属性时返回true,无论该属性存在于实例中还是原型中。
function Person(){} Person.prototype.name = "Echo"; var person1 = new Person(); console.log(person1.hasOwnProperty("name")); // false console.log("name" in person1); // trueconsole.log(person1.name); // Echo-来自原型 person1.name = "yya"; console.log(person1.name); // yya --来自实例 console.log(person1.hasOwnProperty("name")); // true console.log("name" in person1); // true

hasOwnProperty()方法和in操作符配合使用。只要in操作符返回true,而hasOwnProperty()返回 false,就可以确定属性是原型中的属性。函数的返回值最终返回true说明该属性一定存在于原型上,而返回false的情况比较复杂,并不能直接判断该属性到底是存在于实例上还是原型上,也有可能判断的属性根本就不存在。
function Person(){} Person.prototype.name = "Echo"; Person.prototype.age = 18; var person1 = new Person(); person1.name = "yya"; person1.job = "Software Engineer"; function hasPrototypeProperty(obj,name){ console.log(!obj.hasOwnProperty(name),(name in obj)) return !obj.hasOwnProperty(name) && (name in obj); }; hasPrototypeProperty(person1,'name')// !true短路 = false(存在于实例,且存在于原型,短路) hasPrototypeProperty(person1,'job')// !true短路 = false(存在于实例,不存在于原型,短路) hasPrototypeProperty(person1,'age')// !false && true = true (不存在于实例,仅存在于原型) hasPrototypeProperty(person1,'1212')// !false && false = false (不存在于实例,不存在于原型)

for-in 循环:,返回的是所有能够通过对象访问的、可枚举的属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性。覆盖了原型中不可枚举属性的实例属性也会在for-in 循环中返回。
function Person(){} Person.prototype.age = 18; var person = new Person(); Object.defineProperty(person, "name", { writable: false,// 表示能改修改属性的值 configurable: true,// 表示是否可以从对象中删除属性 enumerable:false,// 表示能否通过for in 循环返回属性 value: "Echo"// 属性值 }); person.toString = function(){} for (var key in person) { console.log(key); // age toString } for (var key in person) { if (Object.hasOwnProperty.call(person, key)) { console.log(key); // toString } }

要想取得对象上所有可枚举的实例属性,可以使用ES5的Object.keys()方法。该方法接收一个对象作为参数,返回一个包含所有可校举属性的字符串数组。
function Person(){} Person.prototype.name = "Echo"; Person.prototype.age = 18; Person.prototype.job = "Software Engineer"; Person.prototype.sayName = function(){ console.log(this.name); } var keys = Object.keys(Person.prototype); console.log(keys) //['name', 'age', 'job', 'sayName']var p1 = new Person(); p1.name="Rob"; p1.age = 32; var p1keys = Object.keys(p1); console.log(p1keys); // ['name', 'age']

如果想要得到所有实例属性,无论是否可枚举,可以使用Object.getOwnPropertyNames()方法。
var keys = Object.getOwnPropertyNames(Person.prototype); console.log(keys); // ['constructor', 'name', 'age', 'job', 'sayName']

结果中包含了不可枚举的constructor属性。Object.keys()和Object.getOwnPropertyNames()方法都可以用来替代for-in循环。
5.3.更简单的原型语法
前面例子中每添加一个属性和方法就要敲一遍Person.prototype。为减不必要的输入,也为了从视觉上更好地封装原型的功能,更常见的做法是用一个包含所有属性和方法对象字面量来重写整个原型对象,
function Person(){ this.sayName = function(){ console.log('Echo'); } }var p = new Person() Person.prototype = { name: "Echo", age: 18, job: "Software Engineer", sayName: function(){ console.log(this.name); } }; p.sayName()

本质上完全重写了默认的prototype对象,此时constructor属性也就变成了新对象的constructor属性,指向Object构造函数,不再指向Person函数。 如果constructor的值很重要,可以手动设置值。
function Person(){}Person.prototype = { constructor: Person, name: "Echo" , age: 18, job: "Software Engineer" , sayName: function(){ console.log(this.name); } }

但是值得注意的是:以这种方式重设constructor属性会导致它的[[Enumerable]]特性被设置为true。默认情况下,原生的 constructor 属性是不可枚举的,可以试试Object.defineProperty()。
Object.defineProperty(Person.prototype,"constructor",{ Enumerable: false, value:Person });

5.4.原型的动态性
由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反应出来。即使是先创建了实例后修改原型也照样如此。
var p1 = new Person(); Person.prototype.sayHi = function(){ console.log("Hi"); // Hi };

先创建了一个实例,然后在Person. prototype中添加了一个方法sayHi。即使实例是在添加新方法之前创建的,仍然可以访问这个新方法。其原因可以归结为实例与原型之间的松散连接关系。因为实例与原型之间的连接只不过是一个指针,而非一个副本,可以随时为原型添加属性和方法。
如果是重写整个原型对象,把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。因为调用构造函数时会为实例添加一个指向最初原型的 [[Prototype]]指针,实例中的指针仅指向原型,而不指向构造函数。
function Person(){} var p1 = new Person(); Person.prototype = { constructor: Person, name:"Echo", age: 18, job:"Software Engineer", sayName: function (){ console.log(this.name); } }; p1.sayName(); // TypeError: p1.sayName is not a function

5.5.原生对象的原型
原型模式的重要性不仅体现在创建自定义类型方面,所有原生的引用类型,都是采用这种模式创建的。所有原生引用类型都在其构造函数的原型上定义了方法。
console.log(typeof Array.prototype.sort); // function console.log(typeof String.prototype.substring); // function

通过原生对象的原型,不仅可以取得所有默认方法的引用,而且也可以定义新方法。可以像修改自定义对象的原型一样修改原生对象的原型,因此可以随时添加方法。尽管可以这样做,但不推荐在产品化的程序中修改原生对象的原型。如果因某个实现中缺少某个方法。就在原生对象的原型中添加这个方法,那么当在另一个支持该方法的实现中运行代码时,就可能会导致命名冲突。而且,这样做也可能会意外地重写原生方法。
5.6.原型对象的问题
它省略了为构造函传递初始化参数,结果所有实例在默认情况下都将获得相同的属性值。虽然会带来一些不方便,但还不是原型的最大问题,原型模式的最大问题是由其共享的本性所导致的。
原型中所有属性是被所有实例共享的,这种共享对于函数非常合适。对于基本值的属性也不会有太大的问题,然而,对于引用类型的属性来说,问题就比较突出了。
function Person(){}Person.prototype = { constructor: Person, name :"Echo", age : 18, job :"Software Engineer", friends : ["Shelby", "Court"], sayName : function (){ console.log(this.name); } }; var person1 = new Person(); var person2 = new Person(); person1.friends.push("Van"); console.log(person1.friends); // ['Shelby', 'Court', 'Van'] console.log(person2.friends); // ['Shelby', 'Court', 'Van'] console.log(person1.friends === person2.friends); // true

friends的属性包含一个字符串数组。修改了person1.friends引用的数组,也会影响person2.friends,因为friends数组存在于Person.prototype而非person1中,所以二者的friends属性指向同一个数组,实例一般都是要有仅属于自己的属性的。而这个问题正是我们很少看到有人单独使用原型模式的原因所在。
6.组合使用构造函数模式和原型模式 创建自定义类型的最常见方式。就是组合使用构造函数模式与原型模式。其中构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。这样一来,每个实例都会有自己的一份实例属性的副本但同时又共享着对方法的引引用。最大限度地节省了内存。还支持向构造函数传递数。
是目前在ECMAScript中使用最广泛、认同度最高的一种创建自定义类型的方法。可以说,这是用来定义引用类型的一种默认模式。
function Person(name, age, job){ this.name = name; this.age = age; this.job= job; this.friends = ["Shelby","Court"]; } Person.prototype = { constructor : Person, sayName : function(){ console.log(this.name) } }var person1 = new Person("Echo",18,"Software Engineer"); var person2 = new Person("yya",17,"Doctor"); person1.friends.push("Van"); console.log(person1.friends); // ['Shelby', 'Court', 'Van'] console.log(person2.friends); // ["Shelby","Court"] console.log(person1.friends == person2.friends); // false console.log(person1.sayName == person2.sayName); // true

7.动态原型模式 动态原模式把所有信息都封装在了构造函数中,通过构造函数初始化原型,保持了同时使用构造函数和原型的优点。其主要原理是可以通过查找某个应该存在的方法是否有效,来决定是否需要初始化原型。
function Person(name, age,job){ //属性 this.name =name; this.age = age; this.job = job; //方法 if (typeof this.sayName != "function"){ Person.prototype.sayName = function(){ console.log(this.name); } } }var p1 = new Person("Echo", 18, "Software Engineer"); p1.sayName();

在sayName()方法不存在的情况下,将它添加到原型中。只会在初次调用构造函数时才会执行。原型完成初始化后对原型所做的修改,能够立即在所有实例中得到反映。因此,这种方法可以说非常完美。其中,if语句检查的可以是初始化之后应该存在的任何属性或方法--不必用一大堆 if 检查每个属性和方法,只要检查其中一个即可。
使用动态原型模式时,不能使用对象字面量重写原型。因为如果在已经创建了实例的情况下重写原型,那么就会切断现有实例与新原型之间的联系。
8.寄生构造函数模式 寄生构造函数模式:除了使用new操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实模一样的。构造函数在不返回值的情况下,默认会返回新对象实例。而通过在构造函数的末尾添加return语句,可以重写调用构造函数时返回的值。
function Person(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 p1 = new Person("Echo", 18,"Software Engineer"); // 寄生构造函数模式 var p2 = new Person("yya",17,"Doctor"); // 工厂模式 p1.sayName(); // Echo p2.sayName(); // yya

返回的对象与构造函数或者与构造函数的原型属性之间没有关系,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。因此不能依赖instanceof 操作符来确定对象类型。
9.稳妥构造函数模式 说到稳妥构造函数模式,首先要介绍一下稳妥对象这个概念,指的是没有公共属性,而且其方法也不引用this的对象。最适合在一些安全的环境中(这些环境中会禁止使用this和new)或者在防止数据被其他应用程序改动时使用。稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:
  1. 新创建对象的实例方法不引用this;
  2. 不使用new操作符调用构造函数。按照稳妥构造函数的要求,重写一下Person构造函数。
function Person(name, age, job){ var o = new Object(); // 可以在这里定义私有变量和函数// 添加方法 o.sayName = function(){ console.log(name); } return o; }var p1 = new Person("Echo", 18,"Software Engineer"); p1.sayName()// Echo

在以这种模式创建的对象中,除了使用sayName()方法之外,没有其他办法访向name的值。变量p1中保存的是一个稳妥对象,而除了调用sayName()方法外,没有别的方式可以访问其数据成员。即使有其他代码会给这个对象添加方法或数据成员,但也不可能有别的办法访问传入到构造函数中的原始数据,这种安全性,使得它非常适合在某些安全执行环境下使用。
【面向对象的程序设计之JS创建对象的9种模式及其优缺点】同寄生构造函数模式,返回的对象与构造函数或者与构造函数的原型属性之间也没有关系,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。因此同样不能依赖instanceof 操作符来确定对象类型。

    推荐阅读