【前端笔记】JS面向对象

245 阅读8分钟

原文链接

ES 中没有类的概念,因此它的对象也与基于类的语言中的对象有所不同。

ES 支持面向对象编程,但不使用类或者接口。对象可以在代码执行过程中创建和增强,因此具有动态性而非严格定义的实体。

创建对象

每个对象都是基于一个引用类型创建的,这个引用类型可以是原生类型,也可以是开发人员定义的类型。

  • new Object()
  • 对象字面量

使用同一个接口创建很多对象,会产生大量的重复代码。

工厂模式

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("Nicholas", 29, "Software Engineer");
var person2 = createPerson("Greg", 27, "Doctor");

用函数来封装以特定接口创建对象的细节。

工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。

构造函数模式

使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。

ES 中的函数是对象,每定义一个函数,也就是实例化一个对象。从逻辑角度讲,此时的构造函数也可以这样定义:

function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = new Function("alert(this.name)"); // 与声明函数在逻辑上是等价的
}

原型模式

function Person() {}
Person.prototype = {
  constructor: Person,
  name: "Nicholas",
  age: 29,
  job: "Software Engineer",
  friends: ["Shelby", "Court"],
  sayName: function() {
    alert(this.name);
  }
};
var person1 = new Person();
var person2 = new Person();

原型模式的最大问题是由其共享的本性所导致的。

person1.friends.push("Van");
alert(person1.friends); //"Shelby,Court,Van"
alert(person2.friends); //"Shelby,Court,Van"
alert(person1.friends === person2.friends); //true

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

function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  this.friends = ["Shelby", "Court"];
}
Person.prototype = {
  constructor: Person,
  sayName: function() {
    alert(this.name);
  }
};
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

这种构造函数与原型混成的模式,是目前在 ES 中使用最广泛,认同度最高的一种创建自定义类型的方法。可以说,这是用来定义引用类型的一种默认模式。

person1.friends.push("Van");
alert(person1.friends); //"Shelby,Count,Van"
alert(person2.friends); //"Shelby,Count"
alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName); //true

动态原型模式

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);
    };
  }
}
var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();

寄生构造函数模式

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 friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName(); //"Nicholas"

构造函数在不返回值得情况下,默认会返回新对象实例,而通过在构造函数的末尾添加一个 return 语句,可以重写调用构造函数时的返回值。

稳妥构造函数模式

function Person(name, age, job) {
  // 创建要返回的对象
  var o = new Object();

  // 可以在这里定义私有变量和函数。

  //添加方法
  o.sayName = function() {
    alert(name);
  };
  // 返回对象
  return o;
}

继承

js 主要通过原型链实现继承。原型链的构建是通过将一个类型的实例赋值给另一个构造函数的原型实现的。这样,子类型就能够访问超类型的所有属性和方法,这一点与基于类的继承很相似。

原型链的问题是对象实例共享所有继承的属性和方法,因此不适宜单独使用。解决这个问题的技术是借用构造函数,即在子类型构造函数的内部调用超类型构造函数。这样就可以做到每个实例都具有自己的属性,同时还能保证只使用构造函数模式来定义类型。

使用最多的继承模式是组合继承,这种模式使用原型链继承共享的属性和方法,而通过借用构造函数继承实例属性。

原型链

function SuperType() {
  this.property = true;
}
SuperType.prototype.getSuperValue = function() {
  return this.property;
};

function SubType() {
  this.subproperty = false;
}
// 继承了SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function() {
  return this.subproperty;
};

var instance = new SubType();
console.log(instance.getSuperValue()); //true

借用构造函数

function SuperType() {
  this.colors = ["red", "blue", "green"];
}
function SubType() {
  SuperType.call(this);
}

var instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); //"red,blue,green,black"

var instance2 = new SubType();
console.log(instance2.colors); //"red,blue,green"

组合继承

function SuperType(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
  alert(this.name);
};

function SubType(name, age) {
  // 继承属性
  SuperType.call(this, name);
  this.age = age;
}
// 继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
  console.log(this.age);
};

var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29

var instance2 = new SubType("Greg", 27);
console.log(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27

思路

组合继承(有时候也叫做伪经典继承),其背后的思路是:
使用原型链实现实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承。

缺点

组合继承最大的问题就是,无论什么情况下,都会调用两次超类型构造函数;一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。没错,子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类型构造函数时重写这些属性。

原型式继承

function object() {
  function F() {}
  F.prototype = o;
  return new F();
}

原理

借助原型,可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。

ES5 通过新增 Object.create()方法规范化了原型式继承。

场景

在没有必要兴师动众的创建构造函数,而只想让一个对象与另一个对象保持类似的情况下,原型式继承是完全可以胜任的。不过别忘了,包含引用类型值的属性始终都会共享相应的值,就像使用原型模式一样。

其本质是执行对给定对象的浅复制。而复制得到的副本还可以得到进一步改造。

寄生式继承

function createAnother(original) {
  // 通过调用函数创建一个新对象
  var clone = object(original);

  // 以这种方式来增强这个对象
  clone.sayHi = function() {
    console.log("hi");
  };

  return clone; // 返回这个对象
}

思路

寄生式继承的思路与寄生构造函数工厂模式类似,即
创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。

缺点

使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率;这一点与构造函数模式类似。

场景

在主要考虑 对象 而不是 自定义类型构造函数 的情况下,寄生式继承也是一种有用的模式。

寄生组合式继承

function inheritPrototype(subType, superType) {
  var prototype = object(superType.prototype); // 创建对象
  prototype.constructor = subType; // 增强对象, 弥补因为重写原型而失去的默认的constructor属性
  subType.prototype = prototype; // 指定对象
}

思路

所谓的寄生组合式继承,即:
通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是:
不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。
本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。

优点

寄生组合式继承,集 寄生式继承组合继承 的优点与一身,是实现基于类型继承的最有效的方式。

属性类型

ES 中有两种属性

  • 数据属性
  • 访问器属性

** 特性(attribute)** 是用于描述属性(property)的各种特征。特性是为了实现 js 引擎用的,因此在 js 中不能直接访问他们。

数据属性

  • [[Configurable]]
  • [[Enumerable]]
  • [[Writable]]
  • [[Value]]

直接在对象上定义的属性,它们的[[Configurable]],[[Enumerable]],[[Writable]]特性都被设置为true

要修改属性默认的特性,必须使用 ES5 的 Object.defineProperty()方法。

访问器属性

  • [[Configurable]]
  • [[Enumerable]]
  • [[Get]]
  • [[Set]]

访问器属性不能直接定义,必须使用 Object.defineProperty()来定义。

理解原型对象

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

我们创建的每个函数,都有一个prototype属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法

Object 对象

Object 的一些方法

Object.defineProperty()

参数(属性所在的对象,属性的名字,描述符对象)

  • 描述符(descriptor)对象的属性必须是configurableenumerablewritablevalue
  • [[Configurable]],一旦把属性定义为不可配置的,就不能再把它变成可配置了。

Object.defineProperties()

Object.getOwnPropertyDescriptor()

Object.getPrototypeOf()

可以方便的取得一个对象的原型,这在利用原型实现继承的情况下是非常重要的。

console.log(Object.getPrototypeOf(person1) == Person.prototype); //true

hasOwnProperty()

它是从 Object 继承来的。

使用此方法可以检测一个属性是存在于实例中还是存在于原型中,这个方法只在给定属性存在于对象实例中时,才会返回true

Object.create()

总结

面向对象的三大特性: 封装,继承,多态。

关键字:代码复用,共享。

思考:为什么我们需要继承,继承是为了解决什么问题,反复思考。