1

七种创建模式表格对比:循序渐进解决问题

  • 当然不能忘记es6的class
创建模式 解决的问题 未解决的问题 是否能使用instanceof检测类型
工厂模式 解决了重复调用函数的问题 不能具体识别对象的类型(都是从Object来的) 可以
构造函数模式 解决了识别对象的问题 每个方法都要在实例上重新创建一遍,即每个Person实例都包含一个不同的Function实例,浪费空间 可以
原型模式 解决了不能共享方法,浪费内现存的问题 1. 不能为构造函数初始化参数 2. prototype里包含引用类型值的属性,修改时会影响到原型 可以
组合使用原型模式和构造模式 解决了共享内存的问题,传参的问题,比较好的 ----- 可以
动态原型模式 不把原型和构造函数分开写,把所有信息都封装在构造函数中 不能使用对象字面量重写原型 可以
寄生构造函数模式 同工厂函数一样 不可以
稳妥构造函数模式 适合安全执行环境,比如不能用this和new的 不可以

一、定义方法

// 1. new的方式创建 
var obj = new Object();
// 2. 对象字面量创建
var obj = {};
// 3. Object.create方法创建
var obj = Object.create(o);

二、追求更好的创建方法

0、class

class Person {
  constructor(name, age){
    this.name = name;
    this.age = age;
    this.friends = ['a', 'b'];
  }
  getName() {
    console.log(this.name);
  }
}

var p = new Person ('tom', 24);
p.getName();

1、工厂模式-----自己理解就是专门抽取一个函数(模子)来创建同类的对象

var createPerson = function (name, age) {
  // 声明一个中间对象,该对象就是工厂对象的模子
  var o = new Object();

  // 添加需要的属性和方法
  o.name = name;
  o.age = age;
  o.getName = function () {
    return this.name;
  }

  return o;
}

var personTom = createPerson('tom', 27);
var personJack = createPerson('jack', 18);

console.log(personTom instanceof Object); // true
console.log(personJack);

2、构造函数模式-----构造函数可以用来创建特定类型的对象。

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

  this.getName = function () {
    return this.name;
  }
}

var pa1 = new Person('tom', 27);
console.log(pa1); // Person{.....}
console.log(pa1 instanceof Person); // true
console.log(pa1 instanceof Object); // true

// 发现的相等关系
console.log(p1.constructor === Person);
console.log(p1.__proto__.constructor === Person);
console.log(p1.__proto__.constructor === p1.constructor);

// 不能共享实例的方法的问题
console.log(pa1.getName === pa2.getName); // false
  • 通过new,改变this的指向。
function demo() {
  console.log(this);
}

demo(); // Window
new demo(); // demo{}

new的实现:

// 
// 将该中间对象的原型指向构造函数的原型;
// 将构造函数的this,指向该中间对象
// 返回该中间对象,即返回实例对象

function New(func) {
  // step1:声明一个新的中间对象,该对象为最终返回的实例
  var res = {};

  if (func.prototype !== null) {
    // step2:将该中间对象的__proto__指向构造函数的原型对象,获取构造函数上的属性和方法
    res.__proto__ = func.prototype;
  }

  // step3:ret为执行构造函数的结果,通过apply,将构造函数的作用域赋给新对象,即将this指向了这个新对象
  var ret = func.apply(res, Array.prototype.slice.call(arguments, 1));
  
  // step4: 返回新的对象--若我们在构造函数中,明确指定了返回对象,则返回ret
  if ((typeof ret === 'object' || typeof ret === 'function') && ret !== null) {
    return ret;
  }

  // step4: 返回新的对象--若我们在构造函数中没有明确指定返回对象时,那么默认返回res。因为ret为undefined
  return res;
}

var p1 = New(Person, 'tom', 21);
console.log(p1.getName());
console.log(p1 instanceof Person);
console.log(p1 instanceof Object);

3、原型模式-----根据需求,选择性的将一些属性和方法通过prototype,挂载在原型对象上。

  • 几乎每个函数都有一个prototype,几乎每个对象都有__proto__。
  • 当访问实例对象中的属性或者方法时,会优先访问实例自身的。
  • 可以通过 in 来判断,一个对象上是否拥有某个属性或方法。(不论实例自身存在还是原型上存在)
console.log('name' in p1);

in最常用的场景之一就是判断页面是否在移动端打开

isMobile = 'ontouchstart' in document;

还有其他相关判断方法:

// 1、hasOwnProperty: 只有属性存在于实例上才返回true
function Person () {}
Person.prototype.name = '12';
Person.prototype.sayName = function () {
  console.log(this.name);
};

var p1=new Person();
var p2=new Person();
console.log(p1.hasOwnProperty('name')); // false
console.log('name' in p1); // true
console.log(p1.name); // '12'

p1.name = 'rrrr'; // 直接给实例添加name属性,实例上的属性会覆盖(屏蔽)原型上的属性
console.log(p1.hasOwnProperty('name')); // true
console.log('name' in p1); // true
console.log(p1.name); // 'rrrr'

console.log(p2.hasOwnProperty('name')); // false  上面给p1添加的name不影响p2
console.log('name' in p2); // true
console.log(p2.name); // '12'

delete p1.name; // 删除p1上添加的name
console.log(p1.hasOwnProperty('name')); // false
console.log('name' in p1); // true
console.log(p1.name); // '12'  原型上的

// 2、hasOwnProperty和in结合,可判断属性是否在原型上的
function hasPrototypeProperty(object, name) {
  return !object.hasOwnProperty(name) && (name in object);
}

// 3、for-in循环,返回实例和原型上的可枚举属性。(新添加的覆盖了原型上本不可枚举的属性也会被循环出来)
for(let item in p1){
  console.log(item, p1[item]);
}

// 4、Object.keys获取所有实例上的可枚举的属性,返回key的数组
//   p1.propertyIsEnumerable('name'); // 返回boolean,检测只在实例上的属性是否可枚举

// 5、Object.getOwnPropertyNames获取所有实例上的属性(包括可枚举和不可枚举),返回key的数组

// 6、4,5结合实现获取实例上的不可枚举属性
var target = p1;
var enum_and_nonenum = Object.getOwnPropertyNames(target);
var enum_only = Object.keys(target);
var nonenum_only = enum_and_nonenum.filter((item) => {
  return !enum_only.includes(item);
});
console.log(nonenum_only);
  • 原型的动态性
    (1)对原型对象所做的修改能够立即反映在实例上。跟实例的创建和原型的修改先后无关。
    (2)实例中的指针指向原型,不是指向构造函数。重写原型对象会切断现有原型和之前一期经存在的实例之间的联系。
function Person () {
}

Person.prototype.age = 17;

// 也可以重写原型对象,所以要把constructor: Person加上。(通过这种方法重设constructor会使其[[Enumerable]]变成true。默认情况下 constructor的[[Enumerable]]是false)
Person.prototype = {
  constructor: Person, // constructor是创建函数的时候,自动获取到的属性
  name: 'tom',
  friends: ['a', 'b'],
  sayName() {
    console.log(this.name);
  }
};

let p1 = new Person();
console.log(p1.friends); // ['a', 'b']
let p2 = new Person();
console.log(p2.friends); // ['a', 'b']

p1.friends.push('c');
console.log(p1.friends); // ['a', 'b', 'c']
console.log(p2.friends); // ['a', 'b', 'c']
  • 原生对象也可以在构造函数上定义方法,比如
Array.prototype.startWith = function () {}

但是不推荐,防止不同地方的命名冲突。

4、组合使用构造函数模式和原型模式:使用最广泛,认同度最高,是定义引用类型的默认模式。

  • 通过原型模式定义方法和共享属性,既能保留实例的属性副本,又能节省内存。
  • 通过构造函数模式传递初始化参数。
function Person(name, age) {
  this.name = name;
  this.age = age;
  this.friends = ['a', 'b'];
}

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

var p1 = new Person('tom', 27);
var p2 = new Person('jack', 18);

p1.friends.push('c');

console.log(p1.friends);  // ["a", "b", "c"]
console.log(p2.friends); //  ["a", "b"]
console.log(p2.getName === p2.getName); // true

5、动态原型模式:在构造函数中初始化原型(仅在必要的情况下)

  • 使用动态原型模式不能使用对象字面量重写原型
function Person(name, age) {
  this.name = name;
  this.age = age;
  this.friends = ['a', 'b'];

  // 只会在初次调用构造函数时执行
  if (typeof this.getName !== 'function') {
    Person.prototype.getName = function () {
      console.log(this.name);
    }
  }
}

let p1 = new Person('tom', 27);
p1.getName();

6、寄生构造函数模式:和工厂模式一样,但是使用new调用。(请移步采用这种模式)

  • 返回的对象和构造函数或者构造函数的原型属性之间没有关系。所以不能用instanceof来确定类型。
function SpecialArray () {
  let values = new Array();

  values.push.apply(values, arguments);

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

  return values;
}

var colors = new SpecialArray('red', 'green', 'blue');
console.log(colors.toPipedString()); // red|green|blue

7、稳妥构造函数模式

  • 同寄生构造函数相比,新创建对象的实例方法不引用this;不适用new操作符调用构造函数。
function Person(name, age) {
  var o = new Object();
  o.name= name;
  o.age = age;

  o.getName = function () {
    console.log(name);
  }

  return o;
}

var p = Person ('tom', 24);
p.getName();

p保存的是一个稳妥对象。除了调用getName方法,没有别的办法可以访问数据成员。


雨花石
410 声望19 粉丝

人生没有彩排,每天都是直播