JavaScript核心技术开发解密读书笔记(第九章)

2018-07-30  本文已影响87人  BeLLESS

第九章 面向对象

面向对象是JavaScript中比较不好理解的地方,去年校招的时候,每当被问到原型链,属性,继承时,心里都有些需,结合红宝书,对面向对象进行下总结。

1. 基本概念
对象

在ECMAScript-262中,对象被定义为无序属性的集合,其属性可以包含基本值、对象或者函数。下面就是一个简单的对象。

var person = {
  // 属性为基本值
  name: 'Tom',
  age: 18,
  // 属性为函数
  getName: function () {
    return this.name;
  },
  // 属性为对象
  parent: {}
}
创建对象

在了解对象的定义之后,我们如何去创建一个对象呢?红宝书中对创建对象总结了六种方法,由于红宝书中的内容作者理解的有所欠缺,这里只描述本书中创建对象的方法。
1. 通过关键字new来创建对象

var obj = new Object();

2. 通过字面量的形式创建对象

var obj = {};

当我们想要给创建的对象添加属性与方法时,可以这样操作。

var person = {};
person.name = 'Tom';
person.getName = function () {
  return this.name;
}
// or
var person = {
  name: 'Tom',
  getName: function () {
    return this.name;
  }
}

当我们需要访问对象的属性与方法时,我们可以这样。

var person = {
  name: 'Tom',
  age: 20,
  getName: function () {
    return this.name;
  }
}
// 访问name属性
person.name;
// or
person['name'];
// or 注意这里_name是一个变量
var _name = 'name';
person[_name];

要注意,当我们访问的属性名是一个变量时,只能使用中括号的方式。

构造函数与原型

第八章中提到过,封装函数其实是封装一些公共的逻辑与功能,通过传入参数的形式达到自定义的效果。当面对具有共同特征的一类事物时,就可以结合构造函数与原型的方式将这类事物封装成对象。

例如,我们将“人”这一类事物封装成一个对象,那么可以这样做。

// 构造函数
var Person = function (name, age) {
  this.name = name;
  this.age = age;
}
// Person.prototype为Person的原型,这里在原型上添加了一个方法
Person.prototype.getName = function () {
  return this.name;
}

具体某一个人的特定属性,通常放在构造函数中。所有人公共的方法与属性,通常会放在原型对象中。

var p1 = new Person('Jake', 20);
var p2 = new Person('Tom', 22);

p1.getName(); // Jake
p2.getName(); // Tom

注意this指向问题,忘记的童鞋请看之前第七章的笔记或参照你不知道的JS对象章节部分。
new关键字在创建实例时经历了如下过程:

它们之间的关系如下图所示。


由上图可得,构造函数的prototype与所有实例的proto都指向原型对象,而原型对象的constructor则指向构造函数。
因为在构造函数中声明的变量与方法只属于当前实例,因此我们可以将构造函数中声明的属性与方法称为该实例的私有属性和方法,它们只能被当前实例访问。
而原型中的方法与属性能够被所有的实例访问,因此我们将原型中声明的属性与方法称为公有属性与方法。
与在原型中添加一个方法不同,当在构造函数中声明一个方法时,每创建一个实例,该方法都会被重新创建一次。而原型中的方法仅仅只会被创建一次。
因此在构造函数中,声明私有方法会消耗更多的内存空间。
如果再构造函数中声明的私有方法/属性与原型中的公有方法/属性重名,那么会优先访问私有属性/方法。
function Person (name) {
  this.name = name;
  this.getName = function () {
    return this.name + ' ,你正在访问私有方法';
  }
}
Person.prototype.getName = function () {
  return this.name;
}
var p1 = new Person('Tom');
p1.getName(); // Tom,你正在访问私有方法
判断对象是否拥有某个属性/方法

可以通过in来判断一个对象是否拥有某一个方法/属性,无论该方法/属性是否公有。

// 接上面创建的p1实例
'name' in p1; // true
'getName' in p1; // true
'age' in p1; // false
原型链

原型对象也是普通对象,因此,在创建原型方法时也可按照创建对象的方法去创建。下面几句话有些绕,还需读者好好理解。

当一个对象A作为原型时,它有一个constructor属性指向它的构造函数,即A.constructor。
当一个对象B作为构造函数时,它有一个prototype属性指向它的原型,即B.prototype。
当一个对象C作为实例时,它有一个proto属性指向它的原型,即C.proto

当想要判断一个对象foo是否是构造函数Foo的实例时,可以使用instanceof关键字。

foo instanceof Foo; // true: foo是Foo的实例,false:不是

当创建一个对象时,可以使用new Object()来创建。因此Object其实是一个构造函数,而其对应的原型Object.prototype则是原型链的终点。

foo instanceof Foo; // true: foo是Foo的实例,false:不是,Object.prototype.__proto__ === null
// 所有的函数与对象都有一个toString与vallueOf方法,就是来自于Object.prototype
Object.prototype.toString = function () {}
Object.prototype.valueOf = function () {}

当创建函数时,除可以使用function关键字外,还可以使用Function对象。

var add = new Function('a', 'b', 'return a+ b');
// 等价于
var add = function (a, b) {
  return a + b;
}

因此这里创建的add方法是一个实例,它对应的构造函数是Function,它的原型是Function.prototype。

add.__proto__ === Function.prototype; // true

需要注意的是,当构造函数与原型拥有同名的方法/属性时,如果用创建的实例访问该方法/属性,则优先访问构造函数的方法/属性。

function Person (name) {
  this.name = name;
  this.getName = function () {
    return 'name in Person';
  }
}
Person.prototype.getName = function () {
  return 'name in Person.prototype';
}
var p1 = new Person('alex');
p1.getName(); // name in Person
实例方法、原型方法、静态方法

构造函数中的方法称之为实例方法,通过prototype添加的方法,将会挂载到原型上,称之为原型方法,被直接挂在在构造函数上的方法称之为静态方法。
静态方法不能通过实例访问,只能沟通过构造函数来访问。

function Foo () {
  this.bar = function () {
    return 'bar in Foo'; // 实例方法
  }
}
Foo.bar = function () {
  return 'bar in static'; // 静态方法
}
Foo.prototype.bar = function () {
  return 'bar in prototype'; // 原型方法
}

静态方法又称为工具方法,常用来实现一些常用的,与具体实例无关的功能,如遍历方法each。

继承

红宝书中对继承总结了六种方法,由于红宝书中的内容作者理解的有所欠缺,这里只描述本书中创建对象的方法。(还是建议读者去看下红宝书,比这本讲的详细一些)
继承被分为两种,一种是有构造函数的继承,一种是原型继承。
假设已经封装好了一个父类对象Person。

var Person = function (name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.getName = function () {
  return this.name;
}
Person.prototype.getAge = function () {
  return this.age;
}

构造函数的继承比较简单,可以借助call/apply来实现。假设想要通过继承封装一个Student的子类对象,那么构造函数的实现如下。

var Student = function (name, age, grade) {
  // 通过call方法还原Person构造函数中的所有处理逻辑
  Student.call(Person, name, age);
  this.grade = grade;
}
// 等价于
var Student = function (name, age, grade) {
  this.name = name;
  this.age = age;
  this.grade = grade;
}

原型的继承则需要一点思考。首先应该考虑,如何将子类对象的原型加到原型链中?其实只需让子类对象的原型成为父类对象的一个实例,然后通过proto访问富磊对象的原型,这样就继承了父类原型中的方法与属性了。
可以先封装一个方法,该方法会根据父类对象的原型创建一个实例,该实例即为子类对象的原型。

function create (proto, options) {
  // 创建一个空对象
  var tmp = {};
  // 让这个新的空对象成为父类对象的实例
  tmp.__proto__ = proto;
  // 传入的方法都挂载到新对象上,新对象将作为子类对象的原型
  Object.defineProperties(tmp, options);
  return tmp;
}

在简单封装了create方法之后,就可以使用该方法来实现原型的继承了。

Student.prototype = create(Person.prototype, {
  // 不要忘了重新指定构造函数
  constructor: {
    value: Student
  }
  getGrade: {
    value: function () {
      return this.grade
    }
  }
})

下面来验证这里实现的继承是否正确。

var s1 = new Student('ming', 22, 5);
s1.getName(); // ming
s1.getAge(); // 22
s1.getGrade(); // 5

在ES5里面直接提供了一个Object.create方法来完成上面封装的create功能。

属性类型

在ES5中,对每个属性都添加了几个属性类型,用来描述这些属性的特点。

以上是我对JavaScript核心技术开发解密第九章的读书笔记,码字不易,请尊重作者版权,转载注明出处。
By BeLLESS 2018.7.30 21:11

上一篇下一篇

猜你喜欢

热点阅读