在 JavaScript 中,建立对象的方式有不少种,最经常使用的通常是经过字面量的方式,而要建立实例对象则通常经过建立一个构造函数,经过 new 关键字来构造。javascript
虽然 Object 函数和字面量均可以建立对象,但同时也会有一个问题:使用一个接口建立多个对象时,会出现大量重复代码。下面来介绍一些建立对象的变体。java
function createPerson(name, age) {
var obj = new Object();
obj.name = name;
obj.age = age;
obj.sayName = function () {
console.log(this.name);
};
return obj;
}
var person = createPerson('mike', 18);
复制代码
工厂模式解决了建立多个类似对象的问题,但缺点是没法识别对象原型。bash
这里打印 person 对象,能够看到有 2 个属性和 1 个方法,原型对象是 Obejct,constructor 属性(指向构造函数的指针)指向 Object 对象。babel
function Person(name, age) {
this.name = name;
this.age = age;
this.sayName = function () {
console.log(this.name);
}
}
var person = new Person('mike', 18);
var person2 = new Person('alice', 20);
// 至关于如下操做
var obj = new Object();
obj.__proto__ = Person.prototype;
Person.call(obj, 'mike', 18);
复制代码
构造函数模式是比较常见的一种方式,经过大写函数名的第一个字母来用以区分普通函数。函数
构造函数与工厂模式还有如下的不一样:性能
此时建立 person 实例须要经过 new 关键字,经过 new 关键字调用构造函数的过程其实经历了如下四个步骤:ui
构造函数解决了工厂模式不能识别实例类型的问题,可是也有一个缺点:在这个例子里它会屡次建立了相同函数 sayName。this
咱们建立每个函数都有一个 prototype(原型)属性,指向一个对象。这个对象的用途是包含全部特定类型(例子是 Person)的全部实例共享的属性(name age)和方法(sayName)。spa
function Person() { }
Person.prototype = {
constructor: Person, // 不指定 constructor 会使 constructor 指向断裂,致使对象类型没法正确识别。
name: 'mike',
age: 19,
hobby: ['football', 'singing'],
sayName: function () {
console.log(this.name);
}
}
var person1 = new Person();
var person2 = new Person();
person1.hobby.push('dancing'); // person2.hobby: ['football', 'singing','dancing']
复制代码
constructor 指向未断裂的状况:指向了 Personprototype
constructor 指向断裂的状况:失去了 constructor,默认指向了 Object
原型链示意图:
下图可见经过原型模式解决了构造函数模式屡次建立了 sayName 方法的问题,但聪明的电视机前的你确定发现了定义的原型属性会被全部的实例共享。
当咱们操做了 person1 的 hobby 对象的时候,person2 的也同时被修改了,这是咱们不肯看到的。
function Person(name, age) {
this.name = name;
this.age = age;
this.hobby = ['football', 'singing']
}
Person.prototype = {
constructor: Person, // 不指定 constructor 会使 constructor 指向断裂,致使对象类型没法正确识别。
sayName: function () {
console.log(this.name);
}
}
var person1 = new Person('mike', 18);
person1.hobby.push('dancing');
var person2 = new Person('alice', 19);
复制代码
经过以上的几种方式的分析,咱们差很少也能获得比较好的一种模式了,那就是组合模式。
在构造函数中添加实例属性,在构造函数的原型链上添加实例方法,这样既解决了实例共享,又解决了屡次建立相同函数的问题,是目前使用比较普遍的模式。
ES6 里咱们能够经过 class 关键字来定义一个类,class 其实是一个语法糖,虽然绝大部分的功能能够经过 ES5 实现,可是 class 的写法让对象变的更加清晰,更接近面向对象的语法。 经过 class 来改写组合模式:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
this.hobby = ['football', 'singing'];
}
sayName() {
console.log(this.name);
}
}
var person1 = new Person('mike', 18);
person1.hobby.push('dancing');
var person2 = new Person('alice', 19);
复制代码
由此对比可见,和 ES5 的结果只有在 __proto__ 对象里的 constructor 显示的是 class,其他的部分都是一致。 经过 babel 编译成 ES5,咱们进行一下对比。
'use strict';
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");
}
}
var Person = function () {
function Person(name, age) {
_classCallCheck(this, Person);
this.name = name;
this.age = age;
this.hobby = ['football', 'singing'];
}
// 挂载 sayName 方法
_createClass(Person, [{
key: 'sayName',
value: function sayName() {
console.log(this.name);
}
}]);
return Person;
}();
var person1 = new Person('mike', 18);
person1.hobby.push('dancing');
var person2 = new Person('alice', 19);
复制代码
抛开对属性的一些配置上的操做,与 ES5 咱们所用的组合模式并没有不一样。
首先咱们经过组合模式建立一个 Animal 父类对象
// 定义一个动物类
function Animal(name) {
// 属性
this.name = name || 'Animal';
// 实例方法
this.sleep = function () {
return this.name + ' 正在睡觉!';
}
}
// 原型方法
Animal.prototype.eat = function (food) {
return this.name + ' 正在吃: ' + food;
};
复制代码
核心: 将父类的实例做为子类的原型(注意不能使用字面量方式定义原型方法,会重写原型链)
function Cat() {}
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';
// Test Code
var cat = new Cat();
console.log(cat.name); // cat
console.log(cat.eat('fish')); // cat 正在吃:fish
console.log(cat.sleep()); // cat 正在睡觉!
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // true
复制代码
特色:
缺点:
推荐指数:★★(三、4两大体命缺陷)
核心:使用父类的构造函数来加强子类实例,等因而复制父类的实例属性给子类(没用到原型)
function Cat(name){
Animal.call(this);
this.name = name || 'Tom';
}
// Test Code
var cat = new Cat();
console.log(cat.name); // Tom
console.log(cat.sleep()); // Tom 正在睡觉
// console.log(cat.eat('fish')); // 会报错,原型在这里不可用
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true
复制代码
特色:
缺点:
推荐指数:★★(缺点3)
核心:为父类实例添加新特性,做为子类实例返回
function Cat(name){
var instance = new Animal();
instance.name = name || 'Tom';
return instance;
}
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // false
复制代码
特色:不限制调用方式,不论是new 子类()仍是子类(),返回的对象具备相同的效果
缺点:
推荐指数:★★
function Cat(name){
var animal = new Animal();
for(var p in animal){
Cat.prototype[p] = animal[p];
}
Cat.prototype.name = name || 'Tom';
}
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true
复制代码
特色:支持多继承
缺点:
推荐指数:★(缺点1)
核心:经过调用父类构造,继承父类的属性并保留传参的优势,而后经过将父类实例做为子类原型,实现函数复用
function Cat(name){
Animal.call(this);
this.name = name || 'Tom';
}
Cat.prototype = new Animal();
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // true
复制代码
特色:
缺点: 调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)
推荐指数:★★★★(仅仅多消耗了一点内存)
核心:经过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点
function Cat(name){
Animal.call(this);
this.name = name || 'Tom';
}
(function(){
// 建立一个没有实例方法的类
var Super = function(){};
Super.prototype = Animal.prototype;
//将实例做为子类的原型
Cat.prototype = new Super();
})();
// 等价于下面这种状况
// function inheritPrototype(sub, sup) {
// var Fn= function() {}
// Fn.prototype = sup.prototype;
// sub.prototype = new Fn();
// }
// inheritPrototype(Cat, Animal);
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); //true
复制代码
特色:堪称完美
缺点:实现较为复杂
推荐指数:★★★★
首先仍是建立一个 Animal 类
class Animal {
constructor(name) {
this.name = name || 'Animal';
this.sleep = function () {
return this.name + ' 正在睡觉!';
}
}
eat(food) {
return this.name + ' 正在吃: ' + food;
};
}
复制代码
而后经过 extends 关键字来继承 Animal
class Cat extends Animal {
constructor(name, age) {
super(name);
this.age = age; // 新增的子类属性
}
eat(food) {
const result = super.eat(food); // 经过 super 调用父类方法
return this.age + ' 岁的 ' + result;
}
}
const cat = new Cat('miao', 3);
复制代码
总的来讲,ES6 的 class 语法糖更清晰和优雅地实现了建立对象和对象继承。 可是咱们要想更好的理解 class,那么关于 ES5 的对象、对象继承以及原型链等知识也是要掌握的很牢固。