对象、原型、继承(一)

182 阅读6分钟

一般情况下,我们可以用字面量或者Object构造函数的方式来创建一个对象,但是这个方法有一个缺点,当使用同一个接口创建对象时,容易产生大量重复的代码。

工厂模式

由于在JS中无法创建类(ES6之前),可以使用工厂模式,用函数来封装以特定接口创建对象的过程。

function createPerson(name, age) {
  let o = new Object();
  o.name = name;
  o.age = age;
  o.say = function(){
    console.log('hello ' + o.name)
  };
  return o
}
p = createPerson('kuma', 20)
console.log(p.name) // 'kuma'

构造函数

在JS中,也可以用自定义构造函数的方式创建对象,重写上个例子:

//构造函数函数名首字母应当大写,用以区别一般函数
function Person(name, age, sex) {
  this.name = name;
  this.age = age;
  this.say = function (){
    console.log('hello ' + this.name)
  }
}
const p = new Person('kuma', 19)
console.log(p.name) // 'kuma'

相对比,用构造函数的方式,没有显性创建对象,将属性和方法直接赋值给this,没有return语句。

这里面使用了new操作符,它会经历以下步骤:

  1. 创建一个新对象;
  2. 将构造函数的的作用域赋值给新对象(this指向新对象);
  3. 执行构造函数的代码,为新对象添加属性;
  4. 返回新对象。

新建的p对象都有一个constructor(构造函数)属性,指向Person。

p.constructor === Person // true

p既是Person的实例,也是Object的实例,因为所有对象均继承自Object。

构造函数需要通过new调用,如果直接调用,那么和普通函数没有区别。

Person('kuma', 19)
console.log(window.name) // kuma
// 在全局作用域下调用函数,这里this在浏览器环境下指向了window

对象的constructor属性最初是用来标识对象类型的,而自定义构造函数意味着可以将它的实例标识为一种特定的类型,这是胜过工厂模式的地方。

p instanceof Person // true

构造函数的方法虽然好,但是有一个缺点,就是每个方法都要在实例上重新创建一遍(例子里是say方法)。如果将say方法移到外面,那么这个方法就变成全局函数,如果有很多个方法,那么要定义很多个全局函数,而且毫无封装可言。

原型模式

我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象, 而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。如果按照字面意思来理解,那 么prototype 就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以 让所有对象实例共享它所包含的属性和方法。

function Person(){}

Person.prototype.name = 'kuma';
Person.prototype.age = '20';
Person.prototype.say = function(){ 
  console.log('hello '+ this.name);
};

const p1 = new Person()
const p2 = new Person()
console.log(p1.name) // 'kuma'
console.log(p1.say === p2.say) // true

与构造函数模式不同的是,新对象的这些属性和方法是由所有实例共享的。换句话说, person1 和person2 访问的都是同一组属性和同一个sayName()函数。

原型对象

无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype 属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor (构造函数)属性,这个属性包含一个指向prototype 属性所在函数的指针。

就拿前面的例子来说,Person.prototype.constructor 指向Person。而通过这个构造函数,我们还可继续为原型对象添加其他属性和方法。

当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部 属性),指向构造函数的原型对象。这个指针在部分浏览器是一个属性:__proto__

ePXJxS.png
Person 的每个实例——person1 和person2 都包含一个内部属性,该属性仅仅指向了Person.prototype;换句话说,它们 与构造函数没有直接的关系。

更简单的原型语法

上面的方法每次都需要使用Person.prototype,更方便的做法是使用对象字面的方式重写Person.prototype。

function Person(){}

Person.prototype = {
  constructor : Person,
  name : "kuma",
  age : 20,
  sayName : function () {
    console.log('hello ' + this.name);
  }
};

以上代码特意包含了一个constructor 属性,并将它的值设置为Person,从而确保了通过该属 性能够访问到适当的值。

注意,以这种方式重设constructor 属性会导致它的[[Enumerable]]特性被设置为true。默认 情况下,原生的constructor 属性是不可枚举的,因此如果你使用兼容ECMAScript 5的JavaScript 引 擎,可以试一试Object.defineProperty()。

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

原生对象的原型

所有原生的引用类型,都是采用原型模式创建的。原生引用类型(Object、Array、String,等等)都在其构造函数的原型上定义了方法。例如,在Array.prototype 中可以找到sort()方法等等。

eipMVg.png

原型对象的问题

它省略了为构造函数传递初始化参数这一环节,结果所有实例在 默认情况下都将取得相同的属性值。虽然这会在某种程度上带来一些不方便,但还不是原型的最大问题。 原型模式的最大问题是由其共享的本性所导致的。

原型中所有属性是被很多实例共享的,这种共享对于函数非常合适。对于那些包含基本值的属性倒 也说得过去,毕竟(如前面的例子所示),通过在实例上添加一个同名属性,可以隐藏原型中的对应属 性。然而,对于包含引用类型值的属性来说,问题就比较突出了:

function Person(){
}
Person.prototype = {
  constructor : Person,
  name : "kuma",
  age : 20,
  friends: ['Alex', 'Bob'],
  sayName : function () {
    console.log('hello ' + this.name);
  }
};

const p1 = new Person()
const p2 = new Person()
p1.friends.push('Cater')

console.log(p1.friends) // ["Alex", "Bob", "Cater"]
console.log(p2.friends) // ["Alex", "Bob", "Cater"]
console.log(p1.friends === p2.friends) // true

实例一般都是要有属于自己的全部 属性的。而这个问题正是我们很少看到有人单独使用原型模式的原因所在。

组合使用构造函数模式和原型模式

创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实 例属性,而原型模式用于定义方法和共享的属性。

function Person(name, age){
  this.name = name;
  this.age = age;
  this.friends = ['Alex', 'Bob'];
}
Person.prototype = {
  constructor : Person,
  say : function () {
    console.log('hello ' + this.name + ' your age ' + this.age);
  }
};

const p1 = new Person('kuma', 20)
const p2 = new Person('nell', 22)

p1.friends.push('Cater')

p1.say() // "hello kuma your age 20"
p2.say() // "hello nell your age 22"
console.log(p1.friends) // ["Alex", "Bob", "Cater"]
console.log(p1.friends === p2.friends) // false

小结

知道了这些基础的用法是如何一步步完善的之后,我们可以进一步了解JS中继承的使用。