blog icon indicating copy to clipboard operation
blog copied to clipboard

JavaScript 创建对象-从es5到es6

Open ly2011 opened this issue 7 years ago • 0 comments

JavaScript 创建对象-从es5到es6

转自: https://fed.renren.com/2017/08/07/js-oop-es52es6/

本文主要讲述了使用JavaScript创建对象的几种方式,分别是传统的Object构造函数、对象字面量、工厂模式、构造函数模式、原型模式、组合模式,以及es的class定义类。然后从babel的角度探究es5与es6创建对象的区别。

1. 创建对象的几种方式

1.1 Object 构造函数和对象字面量

通过调用 Object 构造函数new一个Object对象,然后再给这个对象的每一个属性和方法进行赋值,代码如下所示:

var person = new Object()
person.age = 16
person.name = 'Dom'
person.code = function() {
  console.log('hello world!')
}

对象字面量是通过在一个大括号里面使用键值对的方式表示每一个属性和方法,每一个键值对之间使用逗号隔开,代码如下所示:

var person = {
  age: 16,
  name: 'Dom',
  code: function() {
    console.log('hello world!')
  }
}

虽然对象字面量简单直观,但是上面两种方法都存在一个共同的问题:当需要创建很多很多个Person对象的时候,只能一个一个去创建,每一个对象的方法和属性都需要单独写,这使得代码没有丝毫复用性可言,违背了对象封装的特性。于是乎,工厂模式就随之出现了。

1.2 工厂模式

工厂模式通过将对象的创建封装到一个方法中,再通过在调用该方法时传入参数而实现对象的实例化,解决了以上的产生大量重复代码的问题,代码如下所示:

function createPerson(age, name) {
  var obj = new Object()
  obj.age = age
  obj.name = name
  obj.code = function() {
    console.log('hello world!')
  }
  return obj
}
var person1 = createPerson(11, '小白')
var person2 = createPerson(12, '小黑')

但是工厂模式也存在一个不足,就是通过该方法创建的对象的构造函数全都是Object,没有辨识度。没有办法通过构造函数辨别一个对象到底是Person还是Dog,亦或是Cat。于是乎,为了解决这个问题,就引入了构造函数模式。

1.3 构造函数模式

构造函数模式就是通过定义一个function函数,然后通过 this 给对象的属性和方法进行赋值。当我们实例化对象时,只需在该函数前面加一个 new 关键字就可以了。

function Person(age, name) {
  this.age = age
  this.name = name
  this.code = function() {
    console.log('hello world!')
  }
}
var person1 = new Person(11, '小白')
var person2 = new Person(12, '小黑')

构造函数模式解决了工厂模式中的对象识别问题,通过:

console.log(person1 instanceof Person) // true

可以看出person1 能成功被识别为一个Person对象。 但是构造函数模式也同样存在一个缺点,就是构造函数里的属性和方法在每个对象上都要实例化一遍,包括对象公用的属性和方法,这样就造成了代码的复用性差的问题。所以大多数人会考虑将构造函数模式和原型模式结合起来使用。

1.4原型模式

原型模式是通过将所有的属性和方法都定义在其prototype属性上,达到这些属性和方法能被所有的实例所共享的目的。代码示例如下:

function Person(age, name) {
  Person.prototype.age = age
  Person.prototype.name = name
  Person.prototype.code = function() {
    console.log('hello world!')
  }
}
var person1 = new Person()
var person2 = new Person()

当然,这种方法在项目开发中是没有人会使用的,因为当一个对象上的属性改变时,所有对象上的属性也会随之改变,这是非常不切实际的。至于原型模式的原理,会在下一篇的原型链中解释。在这里提及原型模式是为了介绍以下的构造函数+原型组合模式:

1.5构造函数+原型组合模式

组合模式是将构造函数和原型模式结合在一起,继承了他们的优点的同时又避免了各自的缺点。它将具有各自特点的属性和方法定义在构造函数中,将实例间共享的属性和方法定义在prototype上,成为了在es6出现之前使用最普遍的一种创建对象模式。

function Person(age, name) {
  this.age = age
  this.name = name
  this.cry = function() {
    console.log(name + ' is crying!!!')
  }
}
Person.prototype = {
  constructor: Person,
  sayName: function() {
    console.log(this.name)
  }
}
var person1 = new Person(11, '小白')
var person2 = new Person(12, '小黑')

1.6 class定义类

当然,前面讲的都是浮云,现在大家都用class定义类啦,class的出现就是为了定义类能更加简单。回到上面的Person构造函数上,我们现在将其改造成使用class定义的方式:

class Person {
  constructor(age, name) {
    this.age = age
    this.name = name
    this.cry = function() {
      console.log(name + ' is crying!!!')
    }
    sayName() {
      console.log(this.name)
    }
  }
}
var person1 = new Person(11, '小白')
var person2 = new Person(12, '小黑')

使用class定义类跟上面的构造函数+原型组合模式有一些相似之处,但又有所区别。 class定义的类上有个constructor方法,这就是构造方法,该方法会返回一个实例对象,this代表的就是实例对象,这跟上边的构造函数模式很类似。 此外, class上的方法都是定义在prototype上的,这又跟原型模式有一些相似之处,这个class的sayName等价于:

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

虽然class定义的类跟es5中的构造函数+原型组合模式很相似,但是他们还是存在不少区别的,下面对比如下:

2. es5与es6定义对象的区别

    1. class 的构造函数必须使用 new 进行调用,普通构造函数不用 new 也可以执行。
    1. class不存在变量提升,es5中的function存在变量提升。
    1. class内部定义的方法不可以枚举,es5在prototype上定义的方法可以枚举。

为什么会存在以上这些区别呢?下面使用babel将es6转化为es5看看他的实现过程就知道了。

3. es6中class转化为es5

下面将讲述使用babel将以上的class定义Person类转换成使用es5实现:

es6代码:

class Person{
  constructor(age, name) {
    this.age = age;
    this.name = name;
    this.cry = function() {
      console.log(name + 'is crying!!!');
    }
  }
  sayName() {
    console.log(this.name);
  }
}
var person1 = new Person(11, '小白');
var person2 = new Person(12, '小黑');

使用babel转化成的es5后的代码:

'use strict'; // es6中class使用的是严格模式

// 处理class中的方法
var _createClass = function () {
  function defineProperties (target, props) {
    for (var i = 0; i < props.length; i++) {
      var descriptor = props[i];
      // 默认不可枚举
      descriptor.enumerable = descriptor.enumerable || false;
      descriptor.configurable = true;
      if ("value" in descriptor) descriptor.writable = true;
      Object.defineProperty(target, descriptor.key, descriptor);
    }
  }
  return function (Constructor, protoProps, staticProps) {
    if (protoProps) defineProperties(Constructor.prototype, protoProps);
    if (staticProps) defineProperties(Constructor, staticProps);
    return Constructor;
  };
}();

// 对构造函数进行判定
function _classCallCheck (instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

// class Person转换为 es5的function
var Person = function () {
  function Person (age, name) {
    // 调用了_classCallCheck检查Person是否为构造函数
    _classCallCheck(this, Person);

    this.age = age;
    this.name = name;
    this.cry = function () {
      console.log(name + 'is crying!!! T^T');
    };
  }

  // 调用_createClass处理定义在class中的方法。
  _createClass(Person, [{
    key: 'sayName',
    value: function sayName () {
      console.log(this.name);
    }
  }]);

  return Person;
}();

var person1 = new Person(11, '小白');
var person2 = new Person(12, '小黑');

这里我将转换后的代码格式化并加上了一些注释。 从以上代码可以看出,class主要是通过两个函数实现:_createClass和_classCallCheck。 所以为什么会存在上述区别呢:

    1. class构造函数必须使用new进行调用,普通构造函数不用new也可执行。

class中的constructor会自动转化为function构造函数,然后在function中通过_classCallCheck的检查该function是否是一个Constructor。因为有_classCallCheck检查必须是 instanceof Constructor,所以class必须使用new进行调用。

    1. class不存在变量提升,es5中的function存在变量提升。

class转变成了函数表达式进行声明,因为是函数表达式声明的,所以class不存在变量提升。

    1. class内部定义的方法不可枚举,es5在prototype上定义的方法可以枚举。

class中定义的方法会传入_createClass中,然后Object.defineProperty将其定义在Constructor.prototype上。所以class中的方法都是定义在Constructor.prototype上的。由于defineProperties中的 descriptor.enumerable = descriptor.enumerable || false; 将属性的 enumerable 默认为 false, 所以class中定义的方法不可枚举。

ly2011 avatar Nov 30 '18 13:11 ly2011