【For Interview】es5方式创建类

118 阅读5分钟

前言:基本上是面试必考题!
es6的class如何用es5实现,其实是整理红宝书的东西。

工厂模式

封装了创建对象的细节

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

var person1 = createPerson('Tom', 23, 'doctor');
var person2 = createPerson('John', 21, 'engineer');

缺点:没有解决对象识别的问题,即怎样知道一个对象的类型。我个人理解是无法判断某个实例是不是某个类的实例!

构造函数模式

特点:

  • 没有显式创建对象
  • 直接将属性和方法赋值给this对象
  • 没有return
function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = function() {
    alert(this.name);
  }
}

/*
 ** 又是常考的知识点:new 做了什么
 ** 1. 创建新对象
 ** 2. 将构造函数的作用域赋给新对象(this就指向了新对象)
 ** 3. 执行构造函数中的代码(为这个新对象添加属性)
 ** 4. 返回对象
 */
var person1 = new Person('Tom', 23, 'doctor');
var person2 = new Person('John', 21, 'engineer');

可以得知,两个对象又一个构造函数属性,都指向Person

person1.constructor == Person // true
person2.constructor == Person // true
/*
 * 实际上 实例的构造函数是下面
 ƒ Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = function() {
    alert(this.name);
  }
}
**/

person1 instanceof Person; // true
person2 instanceof Person; // true
person1 instanceof Object; // true
person2 instanceof Object; // true

缺点:
每个方法都要在实例重新创建一遍,其实没有必要,方法作用一样但是要创建好多次,没啥必要。

person1.sayName == person2.sayName // false

当然,可能说

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

function sayName() {
  alert(this.name);
}

这样所有实例就都用了全局的方法,但是这个全局方法却只能给Person用,这特么还叫全局?要是有更多的方法呢?那岂不是要维护更多的这种只给某个class用的全局了呢?

等等等等,新模式出现

原型模式

function Person() {
}

Person.prototype.name = "Tom";
Person.prototype.age = 29;
Person.prototype.job = 'Engineer';
Person.prototype.sayName = function() {
  alert(this.name);
}

这样一遍一遍的写prototype很麻烦,所以进化一下

function Person() {
}

Person.prototype = {
  name: 'Tom',
  age: 29,
  job: 'engineer',
  sayName: function() {
    alert(this.name);
  }
}

这样写有个 问题
constructor属性不再指向 Person了。 正常情况每创建一个函数,会同时创建它的prototype对象,这个对象会自动获得constructor属性,而这种声明式完全重写了默认的prototype对象,因此constructor属性变成了新对象的constructor的属性(指向Object构造函数),不再指向Person函数,尽管instanceof操作符还能返回正常值,但是通过constructor已经无法确定对象的类型了

var friend = new Person();
friend instanceof Object; // true
friend instanceof Person; // true
friend.constructor == Person; // false
friend.constructor == Object; // true

如果 真的需要constructor值,那就加上就好

function Person() {
}

Person.prototype = {
  constructor: Person,
  name: 'Tom',
  age: 29,
  job: 'engineer',
  sayName: function() {
    alert(this.name);
  }
}

又有缺点contructor本来是不可枚举的,这样一来就变得可以枚举了。
再改,用上defineProperty

Object.defineProperty(Person.prototype, 'constructor', {
  enumerable: false,
  value: Person,
})

除此之外,原型模式还有其他缺点

  1. 在初始化的时候,所有实例都会获得相同的属性值
  2. 如果属性是引用类型,那就问题很大了
function Person() {
}

Person.prototype = {
  constructor: Person,
  name: 'Tom',
  age: 29,
  job: 'engineer',
  friends: ['jack', 'John'],
  sayName: function() {
    alert(this.name);
  }
}

var person1 = new Person();
var person2 = new Person();

person1.friends.push('Julia');

alert(person1.friends);  // ['jack', 'John', 'Julia']
alert(person2.friends); // ['jack', 'John', 'Julia']
alert(person1.friends === person2.friends) // true

构造函数模式和原型模式的结合

属性用构造函数模式
方法用原型模式

function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  this.friends = ['jack', 'julia'];
}

Person.prototype = {
  constructor: Person,
  sayName: function() {
    alert(this.name);
  }
}

var person1 = new Person('Tom', 11, 'doctor');
var person2 = new Person('Marry', 43, 'teacher');

person1.friends.push('mack');

alert(person1.friends); // ['jack', 'julia', 'mack']
alert(person2.friends); // ['jack', 'julia']
alert(person1.friends === person2.friends); // false
alert(person1.sayName === person2.sayName); // true

这是个最广泛的创建'class'的方法,但是我写一个类要分开两段写,很不舒服,那能不能写到一个里面? 当然可以!下面这种动态原型模式

动态原型模式

function Person(name, age, job) {
  // 属性
  this.name = name;
  this.age = age;
  this.job = job;
  // 方法
  if (typeof this.sayName != 'function') {
    Person.prototype.sayName = function() {
      alert(this.name);
    }
  }
}

这个方法在构造函数运行的时候会执行一次,而且仅执行一次,后续就再也不执行了,但是生成的实例都会动态获得这个方法!

寄生构造函数模式

function Person(name, age, job) {
  var o = new Object();
  o.name = name;
  o.age = age;
  o.job = job;
  o.sayName = function() {
    alert(this.name);
  }
  return o;
}
var friends = new Person('Nick', 29, 'worker');
friends.sayName(); // 'Nick'

乍一看,这特么就是工厂模式吧?
其实不是,寄生构造模式是需要用new去调用的,而工厂模式是直接一个函数。
可能会问,这特么有啥用?
个人看了书上的实例,可能最大的作用在于,我可复写并且扩展原来的基本类?

function specialArray() {
  // 创建数组
  var avlues = new Array();
  
  // 添加值
  values.push.apply(values, arguments);
  
  // 添加方法
  values.toPipedString = function() {
    return this.joiin('|');
  }
  
  // 返回数组
  return values;
}

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

缺点:
也是不能用instanceof确定对象类型

稳妥构造函数模式

大佬提出的稳妥对象概念:

  1. 没有公共属性
  2. 其方法也不引用this 稳妥构造函数与寄生构造类似,但是有不同:
  3. 新创建对象的实例方法不引用this
  4. 不使用new操作符调用构造函数

这特么除了函数名不同,那跟工厂模式有啥区别呢? 个人感觉一模一样, 实际上观察name,发现name是直接用的传入的, 这样,变量person中保存的是一个稳妥对象,而除了调用sayName()方法外,没有别的方式可以访问其数据成员

function Person(name, age, job) {
  var o = new Object();
  
  // 这里定义私有变量和函数
  
  // 添加方法
  o.sayName = function() {
    alert(name);
  }
  return o;
}

var friends = Person('Tom', 29, 'engineer');
friends.sayName(); // 'Tom'

不用new去创建实例,内部也不使用this,说这么做安全
缺点: 也是不能用instanceof确定对象类型