“面向对象” 是以 “对象” 为中间的编程头脑,它的头脑体式格局是组织。
“面向对象” 编程的三大特征:“封装、继续、多态”:
- 封装:属性要领的笼统
- 继续:一个类继续(复制)另一个类的属性/要领
- 多态:要领(接口)重写
“面向对象” 编程的中心,离不开 “类” 的看法。简朴地舆解下 “类”,它是一种笼统要领。经由历程 “类” 的体式格局,能够建立出多个具有雷同属性和要领的对象。
然则!然则!然则JavaScript中并没有 “类” 的看法,对的,没有。
ES6 新增的 class
语法,只是一种模仿 “类” 的语法糖,底层机制照旧不能算是范例 “类” 的完成体式格局。
在邃晓JavaScript中怎样完成 “面向对象” 编程之前,有必要对JavaScript中的对象先作进一步地相识。
什么是对象
对象是“无序属性”的鸠合,表现为“键/值对”的情势。属性值可包括任何范例值(基本范例、援用范例:对象/函数/数组)。
有些文章指出“JS中一切都是对象”,略有偏颇,修正为:“JS中一切援用范例都是对象”越发稳妥些。
函数 / 数组都属于对象,数组就是对象的一种子范例,不过函数轻微复杂点,它跟对象的关联,有点”鸡生蛋,蛋生鸡”的关联,可先记着:“对象由函数建立”。
简朴对象的建立
- 字面量声明(经常运用)
-
new
操纵符挪用Object
函数
// 字面量
let person = {
name: '以乐之名'
};
// new Object()
let person = new Object();
person.name = '以乐之名';
以上两种建立对象的体式格局,并不具有建立多个具有雷同属性的对象。
TIPS:new
操纵符会对一切函数举行挟制,将函数变成组织函数(对函数的组织挪用)。
对象属性的接见体式格局
-
.
操纵符接见 (也称 “键接见”) -
[]
操纵符接见(也称 “属性接见”)
.
操纵符 VS []
操纵符:
-
.
接见属性时,属性名需遵照标识符范例,兼容性比[]
略差; -
[]
接收恣意UTF-8/Unicode字符串作为属性名; -
[]
支撑动态属性名(变量); -
[]
支撑表达式盘算(字符串衔接 / 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. 数据属性
-
Configurable
可设置(可删除)?[true|false]
-
Enumerable
可罗列[true|false]
-
Writable
可写?[true|false]
-
Value
值?默许undefined
2. 接见器属性
-
Get [[Getter]]
读取要领 -
Set [[Setter]]
设置要领
接见器属性优先级高于数据属性
接见器属性会优于
writeable/value
- 猎取属性值时,假如对象属性存在
get()
,会疏忽其value
值,直接挪用get()
; - 设置属性值时,假如对象属性存在
set()
,会疏忽writable
的设置,直接挪用set()
;
- 猎取属性值时,假如对象属性存在
接见器属性一样平常运用:
- 属性值联动修正(一个属性值修正,会触发别的属性值修正);
- 属性值庇护(只能经由历程
set()
制订逻辑修正属性值)
定义属性特征
-
Object.defineProperty()
定义单个属性 -
Object.defineProperties()
定义多个属性
let Person = {};
Object.defineProperty(Person, 'name', {
writable: true,
enumerable: true,
configurable: true,
value: '以乐之名'
});
Person.name; // 以乐之名
TIPS:运用 Object.defineProperty/defineProperties
定义属性时,属性特征 configurable/enumerable/writable
值默许为 false
,value
默许为 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的实行历程
- 建立(组织)一个全新的空对象
- “这个新对象会被实行”原型”链接(新对象的
__proto__
会指向函数的prototype
)” - 组织函数的
this
会指向这个新对象,并对this
属性举行赋值 - 假如函数没有返回其他对象,则返回这个新对象(注重组织函数的
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 操纵都是一切属性(基本 + 原型)
-
for...in...
遍历对象一切可罗列属性 -
in
推断对象是不是具有该属性
Object.keys(…)与Object.getOwnPropertyNames(…)
-
Object.keys(...)
返回一切可罗列属性 -
Object.getOwnPropertyNames(...)
返回一切属性
屏障属性
修正对象属性时,假如属性名与原型链上属性重名,则在实例对象上建立新的属性,屏障对象对原型属性的运用(发作屏障属性)。屏障属性的条件是,对象基本属性名与原型链上属性名存在重名。
建立对象属性时,属性特征对屏障属性的影响
- 对象原型链上有同名属性,且可写,在对象上建立新属性(屏障原型属性);
- 对象原型链上有同名属性,且只读,疏忽;
- 对象原型链上有同名属性,存在接见器属性
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'
瑕玷:
- 没法处置惩罚对象辨认题目
- 属性值为函数时没法共用,差别实例对象的
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中经常运用的继续体式格局,预先定义两个类:
- “Person” 父类(超类)
- “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);
道理:
子类的原型对象为父类的实例对象,因而子类原型对象中具有父类的一切属性
瑕玷:
- 没法向父类组织函数传参,初始化属性值
- 属性值是援用范例时,存在内存共用的状况
- 没法完成多继续(只能为子类指定一个原型对象)
组织函数继续
// 组织函数继续
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
作者:以乐之名
本文原创,有不当的处所迎接指出。转载请指明出处。