前端进击的伟人(七):走进面向对象,原型与原型链,继续体式格局

《前端进击的伟人(七):走进面向对象,原型与原型链,继续体式格局》

“面向对象” 是以 “对象” 为中间的编程头脑,它的头脑体式格局是组织。

“面向对象” 编程的三大特征:“封装、继续、多态”

  1. 封装:属性要领的笼统
  2. 继续:一个类继续(复制)另一个类的属性/要领
  3. 多态:要领(接口)重写

“面向对象” 编程的中心,离不开 “类” 的看法。简朴地舆解下 “类”,它是一种笼统要领。经由历程 “类” 的体式格局,能够建立出多个具有雷同属性和要领的对象。

然则!然则!然则JavaScript中并没有 “类” 的看法,对的,没有。

ES6 新增的 class 语法,只是一种模仿 “类” 的语法糖,底层机制照旧不能算是范例 “类” 的完成体式格局。

在邃晓JavaScript中怎样完成 “面向对象” 编程之前,有必要对JavaScript中的对象先作进一步地相识。

什么是对象

对象是“无序属性”的鸠合,表现为“键/值对”的情势。属性值可包括任何范例值(基本范例、援用范例:对象/函数/数组)。

有些文章指出“JS中一切都是对象”,略有偏颇,修正为:“JS中一切援用范例都是对象”越发稳妥些。

函数 / 数组都属于对象,数组就是对象的一种子范例,不过函数轻微复杂点,它跟对象的关联,有点”鸡生蛋,蛋生鸡”的关联,可先记着:“对象由函数建立”

简朴对象的建立

  1. 字面量声明(经常运用)
  2. new 操纵符挪用 Object 函数
// 字面量
let person = {
  name: '以乐之名'
};

// new Object()
let person = new Object();
person.name = '以乐之名';

以上两种建立对象的体式格局,并不具有建立多个具有雷同属性的对象。

TIPS:new 操纵符会对一切函数举行挟制,将函数变成组织函数(对函数的组织挪用)。

对象属性的接见体式格局

  1. . 操纵符接见 (也称 “键接见”
  2. [] 操纵符接见(也称 “属性接见”

. 操纵符 VS [] 操纵符:

  1. . 接见属性时,属性名需遵照标识符范例,兼容性比 [] 略差;
  2. [] 接收恣意UTF-8/Unicode字符串作为属性名;
  3. [] 支撑动态属性名(变量);
  4. [] 支撑表达式盘算(字符串衔接 / ES6的Symbol

TIPS: 标识符定名范例 —— 数字/英文字母/下划线构成,开首不能是数字。

// 恣意UTF-8/Unicode字符串作为属性名
person['$my-name'];

// 动态属性名(变量)
let attrName = 'name';
person[attrName];  

// 表达式盘算
let attrPrefix = 'my_';
person[attrPrefix + 'name'];  // person['my_name']
person[Symbol.name];          // Symbol在属性名的运用

属性形貌符

ES5新增 “属性形貌符”,可针对对象属性的特征举行设置。

属性特征的范例

1. 数据属性
  1. Configurable 可设置(可删除)?[true|false]
  2. Enumerable 可罗列 [true|false]
  3. Writable 可写? [true|false]
  4. Value 值?默许undefined
2. 接见器属性
  1. Get [[Getter]] 读取要领
  2. Set [[Setter]] 设置要领

接见器属性优先级高于数据属性

  1. 接见器属性会优于 writeable/value

    • 猎取属性值时,假如对象属性存在 get(),会疏忽其 value 值,直接挪用 get()
    • 设置属性值时,假如对象属性存在 set(),会疏忽 writable 的设置,直接挪用 set();
  2. 接见器属性一样平常运用:

    • 属性值联动修正(一个属性值修正,会触发别的属性值修正);
    • 属性值庇护(只能经由历程 set() 制订逻辑修正属性值)

定义属性特征

  1. Object.defineProperty() 定义单个属性
  2. Object.defineProperties() 定义多个属性
let Person = {};
Object.defineProperty(Person, 'name', {
  writable: true,
  enumerable: true,
  configurable: true,
  value: '以乐之名'
});
Person.name;   // 以乐之名

TIPS:运用 Object.defineProperty/defineProperties 定义属性时,属性特征 configurable/enumerable/writable 值默许为 falsevalue 默许为 undefined。别的体式格局建立对象属性时,前三者值都为 true

可运用Object.getOwnPropertyDescriptor() 来猎取对象属性的特征形貌。

原型

JavaScript中模仿 “面向对象” 中 “类” 的完成体式格局,是利用了JavaScript中函数的一个特征(属性)——prototype(自身是一个对象)。

每一个函数默许都有一个 prototype 属性,它就是我们所说的 “原型”,或称 “原型对象”。每一个实例化建立的对象都有一个 __proto__ 属性(隐式原型),它指向建立它的组织函数的 prototype 属性。

new + 函数(完成”原型关联”)

let Person = function(name, age) {
  this.name = name;
  this.age = age;
};
Person.prototype.say = function() {};

let father = new Person('David', 48);
let mother = new Person('Kelly', 46);

《前端进击的伟人(七):走进面向对象,原型与原型链,继续体式格局》

new操纵符的实行历程,会对实例对象举行 “原型关联”,或称 “原型链接”。

new的实行历程

  1. 建立(组织)一个全新的空对象
  2. “这个新对象会被实行”原型”链接(新对象的__proto__会指向函数的prototype)”
  3. 组织函数的this会指向这个新对象,并对this属性举行赋值
  4. 假如函数没有返回其他对象,则返回这个新对象(注重组织函数的return,平常不会有return)

原型链

“对象由函数建立”,既然 prototype 也是对象,那末它的 __proto__ 原型链上应当另有属性。Person.prototype.__proto__ 指向 Function.prototype,而Function.prototype.__proto__ 终究指向 Object.prototype

TIPS:Object.prototype.__proto__ 指向 null(惯例)。

一样平常挪用对象的 toString()/valueOf() 要领,虽然没有去定义它们,但却能一般运用。实际上这些要领来自 Object.prototype,一切一般对象的原型链终究都邑指向 Object.prototype,而对象经由历程原型链关联(继续)的体式格局,使得实例对象能够挪用 Object.prototype 上的属性 / 要领。

接见一个对象的属性时,会先在其基本属性上查找,找到则返回值;假如没有,会沿着其原型链上举行查找,整条原型链查找不到则返回 undefined。这就是原型链查找。

基本属性与原型属性

hasOwnProperty()

推断对象基本属性中是不是有该属性,基本属性返回 true

触及 in 操纵都是一切属性(基本 + 原型)

  1. for...in... 遍历对象一切可罗列属性
  2. in 推断对象是不是具有该属性

Object.keys(…)与Object.getOwnPropertyNames(…)

  1. Object.keys(...) 返回一切可罗列属性
  2. Object.getOwnPropertyNames(...) 返回一切属性

屏障属性

修正对象属性时,假如属性名与原型链上属性重名,则在实例对象上建立新的属性,屏障对象对原型属性的运用(发作屏障属性)。屏障属性的条件是,对象基本属性名与原型链上属性名存在重名

建立对象属性时,属性特征对屏障属性的影响

  1. 对象原型链上有同名属性,且可写,在对象上建立新属性(屏障原型属性);
  2. 对象原型链上有同名属性,且只读,疏忽;
  3. 对象原型链上有同名属性,存在接见器属性 set(),挪用 set()

批量建立对象的体式格局

建立多个具有雷同属性的对象

1. 工场形式

function createPersonFactory(name, age) {
  var obj = new Object();
  obj.name = name;
  obj.age = age;
  obj.say = function() {
    console.log(`My name is ${this.name}, i am ${this.age}`);
  };
  return obj;
}

var father = createPersonFactory('David', 48);
var mother = createPersonFactory('Kelly', 46);
father.say();  // 'My name is David, i am 48'
mother.say();  // 'My name is Kelly, i am 46'

瑕玷:

  1. 没法处置惩罚对象辨认题目
  2. 属性值为函数时没法共用,差别实例对象的 say 要领没有共用内存空间

obj.say = function(){...} 实例化一个对象时都邑拓荒新的内存空间,去存储function(){...},形成没必要要的内存开支。

father.say == mother.say;  // false

2. 组织函数(new)

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.say = function() {
    console.log(`My name is ${this.name}, i am ${this.age}`);
  }
}

let father = new Person('David', 48);

瑕玷:属性值为援用范例(say要领)时没法共用,差别实例对象的 say 要领没有共用内存空间(与工场形式一样)。

3. 原型形式

function Person() {}
Person.prototype.name = 'David';
Person.prototype.age = 48;
Person.prototype.say = function() {
  console.log(`My name is ${this.name}, i am ${this.age}`);
};

let father = new Person();

长处:处置惩罚大众要领内存占用题目(一切实例属性的 say 要领共用内存)
瑕玷:属性值为援用范例时,因内存共用,一个对象修正属性会形成别的对象运用属性发作转变。

Person.prototype.like = ['sing', 'dance'];
let father = new Person();
let mother = new Person();
father.like.push('travel');

// 援用范例共用内存,一个对象修正属性,会影响别的对象
father.like;  // ['sing', 'dance', 'travel']
mother.like;  // ['sing', 'dance', 'travel']

4. 组织函数 + 原型(典范组合)

function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.say = function() {
  console.log(`My name is ${this.name}, i am ${this.age}`);
}

道理:连系组织函数和原型的长处,“组织函数初始化属性,原型定义大众要领”

5. 动态原型

组织函数 + 原型的组合体式格局,区分于别的 “面向对象” 言语的声明体式格局。属性要领的定义并没有一致在组织函数中。因而动态原型建立对象的体式格局,则是在 “组织函数 + 原型组合” 基本上,优化了定义体式格局(地区)。

function Person(name, age) {
  this.name = name;
  this.age = age;
 
  // 推断原型是不是有要领,没有则增加;
  // 原型上的属性在组织函数内定义,仅实行一次 
  if (!Person.prototype.say) {
    Person.prototype.say = function() {
      console.log(`My name is ${this.name}, i am ${this.age}`);
    }
  }
}

长处:属性要领一致在组织函数中定义。

除了以上引见的几种对象建立体式格局,另外另有”寄生组织函数形式”、”稳妥组织函数形式”。一样平常开辟较少运用,感兴趣的同伴们可自行相识。

“类” 的继续

传统的面向对象言语中,”类” 继续的道理是 “类” 的复制。但JavaScript模仿 “类” 继续则是经由历程 “原型关联” 来完成,并非 “类” 的复制。正如《你不知道的JavaScript》中提出的看法,这类模仿 “类” 继续的体式格局,更像是 “托付”,而不是 “继续”

以下枚举JavaScript中经常运用的继续体式格局,预先定义两个类:

  1. “Person” 父类(超类)
  2. “Student” 子类(用来继续父类)
// 父类一致定义
function Person(name, age) {
  // 组织函数定义初始化属性
  this.name = name;
  this.age = age;
}
// 原型定义大众要领
Person.prototype.eat = function() {};
Person.prototype.sleep = function() {};

原型继续

// 原型继续
function Student(name, age, grade) {
  this.grade = grade;
};
Student.prototype = new Person();  // Student原型指向Person实例对象
Student.prototype.constructor = Student;  // 原型对象修正,须要修复constructor属性
let pupil = new Student(name, age, grade);
道理:

子类的原型对象为父类的实例对象,因而子类原型对象中具有父类的一切属性

瑕玷:
  1. 没法向父类组织函数传参,初始化属性值
  2. 属性值是援用范例时,存在内存共用的状况
  3. 没法完成多继续(只能为子类指定一个原型对象)

组织函数继续

// 组织函数继续
function Student(name, age, grade) {
  Person.call(this, name, age);
  this.grade = grade;
}
道理:

挪用父类组织函数,传入子类的上下文对象,完成子类参数初始化赋值。仅完成部份继续,没法继续父类原型上的属性。可 call 多个父类组织函数,完成多继续。

瑕玷:

属性值为援用范例时,需拓荒多个内存空间,多个实例对象没法同享大众要领的存储,形成没必要要的内存占用。

原型 + 组织函数继续(典范)

// 原型 + 组织函数继续
function Student(name, age, grade) {
  Person.call(this, name, age);  // 第一次挪用父类组织函数
  this.grade = grade;
}
Student.prototype = new Person();  // 第二次挪用父类组织函数
Student.prototype.constructor = Student;  // 修复constructor属性
道理:

连系原型继续 + 组织函数继续二者的长处,“组织函数继续并初始化属性,原型继续大众要领”

瑕玷:

父类组织函数被挪用了两次。

待优化:父类组织函数第一次挪用时,已完成父类组织函数中 “属性的继续和初始化”,第二次挪用时只须要 “继续父类原型属性” 即可,无须再实行父类组织函数。

寄生组合式继续(抱负)

// 寄生组合式继续
function Student(name, age, grade) {
  Person.call(this, name, age);
  this.grade = grade;
}
Student.prototype = Object.create(Person.prototype);  
// Object.create() 会建立一个新对象,该对象的__proto__指向Person.prototype
Student.prototype.constructor = Student;

let pupil = new  Student('小明', 10, '二年级');
道理:

建立一个新对象,将该对象原型关联至父类的原型对象,子类 Student 已运用 call 来挪用父类组织函数完成初始化,所以只需再继续父类原型属性即可,避免了典范组合继续挪用两次父类组织函数。(较圆满的继续计划)

ES6的class语法

class Person {
  constructor(name, age) {
    this.name = name;
    this.grade = grade;
  }
  
  eat () {  //...  }
  sleep () {  //...  }
}

class Student extends Person {
  constructor (name, age, grade) {
    super(name, age);
    this.grade = grade;
  }

  play () {  //...  }
}

长处:ES6供应的 class 语法使得类继续代码语法越发简约。

Object.create(…)

Object.create()要领会建立一个新对象,运用现有对象来供应新建立的对象的
__proto__

Object.create 完成的实际上是”对象关联”,直接上代码更有助于邃晓:

let person = {
  eat: function() {};
  sleep: function() {};
}

let father = Object.create(person); 
// father.__proto__ -> person, 因而father上有eat/sleep/talk等属性

father.eat();
father.sleep();

上述代码中,我们并没有运用组织函数 / 类继续的体式格局,但 father 却能够运用来自 person 对象的属性要领,底层道理依赖于原型和原型链的魔力。

// Object.create完成道理/模仿
Object.create = function(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

Object.create(...) 完成的 “对象关联” 的设想形式与 “面向对象” 形式差别,它并没有父类,子类的看法,以至没有 “类” 的看法,只要对象。它提倡的是 “托付” 的设想形式,是基于 “面向托付” 的一种编程形式。

文章篇幅有限,仅作浅易相识,后续可另开一章讲讲 “面向对象” VS “面向托付”,孰优孰劣,说一道二。

对象辨认(搜检 “类” 关联)

instanceof

instanceof 只能处置惩罚对象与函数的关联推断。instanceof 左侧是对象,右侧是函数。推断划定规矩:沿着对象的 __proto__ 举行查找,沿着函数的 prototype 举行查找,假如有关联援用则返回 true,不然返回 false

let pupil = new Student();
pupil instanceof Student;  // true
pupil instanceof Person;   // true Student继续了Person

Object.prototype.isPrototypeOf(…)

Object.prototype.isPrototyepOf(...) 能够辨认对象与对象,也能够是对象与函数。

let pupil = new Student();
Student.prototype.isPrototypeOf(pupil); // true

推断划定规矩:在对象 pupil 原型链上是不是涌现过 Student.prototype , 假如有则返回 true, 不然返回 false

ES6新增修正对象原型的要领: Object.setPrototypeOf(obj, prototype),存在有机能题目,仅作相识,更引荐运用 Object.create(...)

Student.prototype = Object.create(Person.prototype);
// setPrototypeOf改写上行代码
Object.setPrototypeOf(Student.prototype, Person.prototype);

后语

“面向对象” 是顺序编程的一种设想形式,具有 “封装,继续,多态” 的特征,在ES6的 class 语法未出来之前,原型继续确实是JavaScript入门的一个难点,特别是对新入门的朋侪,邃晓起来并不友爱,模仿继续的代码写的冗余又难明。幸亏ES6有了 class 语法糖,没必要写冗余的类继续代码,代码写少了,眼镜片都明亮了。

老话说的好,“会者不难”。深切邃晓面向对象,原型,继续,对日后代码才能的提拔及编码体式格局优化都有好处。好的计划不只要一种,邃晓其中启事,带你走进新世界大门。

参考文档:

本文首发Github,期待Star!
https://github.com/ZengLingYong/blog

作者:以乐之名

本文原创,有不当的处所迎接指出。转载请指明出处。

    原文作者:以乐之名
    原文地址: https://segmentfault.com/a/1190000018196125
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞