"Code tailor",为前端开发者提供技术相关资讯以及系列基础文章,微信关注“小和山的菜鸟们”公众号,及时获取最新文章。javascript
在开始学习以前,咱们想要告诉您的是,本文章是对 JavaScript
语言知识中 "对象、类与面向对象编程" 部分的总结,若是您已掌握下面知识事项,则可跳过此环节直接进入题目练习前端
若是您对某些部分有些遗忘,👇🏻 已经为您准备好了!java
ECMA-262
将对象定义为一组属性的无序集合。严格来讲,这意味着对象就是一组没有特定顺序的值。对象的每一个属性或方法都由一个名称来标识,这个名称映射到一个值。正由于如此(以及其余还未讨论的缘由),能够把ECMAScript
的对象想象成一张散列表,其中的内容就是一组名/值对,值能够是数据或者函数。web
建立自定义对象的一般方式是建立 Object 的一个新实例,而后再给它添加属性和方法,以下例 所示:编程
let person = new Object()
person.name = 'XHS-rookies'
person.age = 18
person.job = 'Software Engineer'
person.sayName = function () {
console.log(this.name)
}
复制代码
这个例子建立了一个名为 person
的对象,并且有三个属性(name
、age
和 job
)和一个方法(sayName()
)。sayName()
方法会显示 this.name
的值,这个属性会解析为 person.name
。早期JavaScript
开发者频繁使用这种方式建立新对象。几年后,对象字面量变成了更流行的方式。前面的例子若是使用对象字面量则能够这样写:设计模式
let person = {
name: 'XHS-rookies',
age: 18,
job: 'Software Engineer',
sayName() {
console.log(this.name)
},
}
复制代码
这个例子中的 person
对象跟前面例子中的 person
对象是等价的,它们的属性和方法都同样。这些属性都有本身的特征,而这些特征决定了它们在 JavaScript
中的行为。浏览器
综观 ECMAScript
规范的历次发布,每一个版本的特性彷佛都出人意料。ECMAScript 5.1
并无正式 支持面向对象的结构,好比类或继承。可是,正如接下来几节会介绍的,巧妙地运用原型式继承能够成 功地模拟一样的行为。ECMAScript 6
开始正式支持类和继承。ES6
的类旨在彻底涵盖以前规范设计的基于原型的继承模式。不过,不管从哪方面看,ES6
的类都仅仅是封装了ES5.1
构造函数加原型继承的语法糖而已。微信
工厂模式是一种众所周知的设计模式,普遍应用于软件工程领域,用于抽象建立特定对象的过程。下面的例子展现了一种按照特定接口建立对象的方式:markdown
function createPerson(name, age, job) {
let o = new Object()
o.name = name
o.age = age
o.job = job
o.sayName = function () {
console.log(this.name)
}
return o
}
let person1 = createPerson('XHS-rookies', 18, 'Software Engineer')
let person2 = createPerson('XHS-boos', 18, 'Teacher')
复制代码
这里,函数 createPerson()
接收 3 个参数,根据这几个参数构建了一个包含 Person
信息的对象。能够用不一样的参数屡次调用这个函数,每次都会返回包含 3 个属性和 1 个方法的对象。这种工厂模式虽然能够解决建立多个相似对象的问题,但没有解决对象标识问题(即新建立的对象是什么类型)。app
ECMAScript
中的构造函数是用于建立特定类型对象的。像 Object
和 Array
这 样的原生构造函数,运行时能够直接在执行环境中使用。固然也能够自定义构造函数,以函数的形式为 本身的对象类型定义属性和方法。 好比,前面的例子使用构造函数模式能够这样写:
function Person(name, age, job) {
this.name = name
this.age = age
this.job = job
this.sayName = function () {
console.log(this.name)
}
}
let person1 = new Person('XHS-rookies', 18, 'Software Engineer')
let person2 = new Person('XHS-boos', 18, 'Teacher')
person1.sayName() // XHS-rookies
person2.sayName() // XHS-boos
复制代码
在这个例子中,Person()
构造函数代替了createPerson()
工厂函数。实际上,Person()
内部 的代码跟 createPerson()
基本是同样的,只是有以下区别。
没有显式地建立对象。
属性和方法直接赋值给了 this
。
没有 return
。
另外,要注意函数名 Person
的首字母大写了。按照惯例,构造函数名称的首字母都是要大写的, 非构造函数则以小写字母开头。这是从面向对象编程语言那里借鉴的,有助于在 ECMAScript
中区分构 造函数和普通函数。毕竟 ECMAScript
的构造函数就是能建立对象的函数。
要建立 Person
的实例,应使用 new
操做符。以这种方式调用构造函数会执行以下操做。
(1)在内存中建立一个新对象。
(2)这个新对象内部的 [[Prototype]]
特性被赋值为构造函数的 prototype
属性。
(3)构造函数内部的 this
被赋值为这个新对象(即 this
指向新对象)。
(4)执行构造函数内部的代码(给新对象添加属性)。
(5)若是构造函数返回非空对象,则返回该对象;不然,返回刚建立的新对象。
上一个例子的最后,person1
和 person2
分别保存着 Person
的不一样实例。这两个对象都有一个 constructor
属性指向 Person
,以下所示:
console.log(person1.constructor == Person) // true
console.log(person2.constructor == Person) // true
复制代码
constructor
原本是用于标识对象类型的。不过,通常认为 instanceof
操做符是肯定对象类型更可靠的方式。前面例子中的每一个对象都是 Object
的实例,同时也是 Person
的实例,以下面调用 instanceof
操做符的结果所示:
console.log(person1 instanceof Object) // true
console.log(person1 instanceof Person) // true
console.log(person2 instanceof Object) // true
console.log(person2 instanceof Person) // true
复制代码
定义自定义构造函数能够确保实例被标识为特定类型,相比于工厂模式,这是一个很大的好处。在 这个例子中,person1
和 person2
之因此也被认为是 Object
的实例,是由于全部自定义对象都继承自 Object
(后面再详细讨论这一点)。构造函数不必定要写成函数声明的形式。赋值给变量的函数表达式也能够表示构造函数:
let Person = function (name, age, job) {
this.name = name
this.age = age
this.job = job
this.sayName = function () {
console.log(this.name)
}
}
let person1 = new Person('XHS-rookies', 18, 'Software Engineer')
let person2 = new Person('XHS-boos', 18, 'Teacher')
person1.sayName() // XHS-rookies
person2.sayName() // XHS-boos
console.log(person1 instanceof Object) // true
console.log(person1 instanceof Person) // true
console.log(person2 instanceof Object) // true
console.log(person2 instanceof Person) // true
复制代码
在实例化时,若是不想传参数,那么构造函数后面的括号可加可不加。只要有 new
操做符,就能够调用相应的构造函数:
function Person() {
this.name = 'rookies'
this.sayName = function () {
console.log(this.name)
}
}
let person1 = new Person()
let person2 = new Person()
person1.sayName() // rookies
person2.sayName() // rookies
console.log(person1 instanceof Object) // true
console.log(person1 instanceof Person) // true
console.log(person2 instanceof Object) // true
console.log(person2 instanceof Person) // true
复制代码
1. 构造函数也是函数
构造函数与普通函数惟一的区别就是调用方式不一样。除此以外,构造函数也是函数。并无把某个函数定义为构造函数的特殊语法。任何函数只要使用 new
操做符调用就是构造函数,而不使用 new
操做符调用的函数就是普通函数。好比,前面的例子中定义的 Person()
能够像下面这样调用:
// 做为构造函数
let person = new Person('XHS-rookies', 18, 'Software Engineer')
person.sayName() // "XHS-rookies"
// 做为函数调用
Person('XHS-boos', 18, 'Teacher') // 添加到 window 对象
window.sayName() // "XHS-boos"
// 在另外一个对象的做用域中调用
let o = new Object()
Person.call(o, 'XHS-sunshineboy', 25, 'Nurse')
o.sayName() // "XHS-sunshineboy"
复制代码
这个例子一开始展现了典型的构造函数调用方式,即便用 new
操做符建立一个新对象。而后是普通函数的调用方式,这时候没有使用 new
操做符调用 Person()
,结果会将属性和方法添加到 window
对象。这里要记住,在调用一个函数而没有明确设置 this
值的状况下(即没有做为对象的方法调用,或 者没有使用 call()/apply()
调用),this
始终指向 Global
对象(在浏览器中就是 window
对象)。 所以在上面的调用以后,window
对象上就有了一个 sayName()
方法,调用它会返回 "Greg"
。最后展现的调用方式是经过 call()
(或apply()
)调用函数,同时将特定对象指定为做用域。这里的调用将 对象 o
指定为 Person()
内部的 this
值,所以执行完函数代码后,全部属性和 sayName()
方法都会添加到对象 o
上面。
2. 构造函数的问题
构造函数虽然有用,但也不是没有问题。构造函数的主要问题在于,其定义的方法会在每一个实例上都建立一遍。所以对前面的例子而言,person1
和 person2
为 sayName()
的方法,但这两个方法不是同一个 Function
实例。咱们知道,ECMAScript
中的函数是对象,所以每次定义函数时,都会初始化一个对象。逻辑上讲,这个构造函数其实是这样的:
function Person(name, age, job) {
this.name = name
this.age = age
this.job = job
this.sayName = new Function('console.log(this.name)') // 逻辑等价
}
复制代码
这样理解这个构造函数能够更清楚地知道,每一个 Person
实例都会有本身的 Function
实例用于显 示 name
属性。固然了,以这种方式建立函数会带来不一样的做用域链和标识符解析。但建立新 Function
实例的机制是同样的。所以不一样实例上的函数虽然同名却不相等,以下所示:
console.log(person1.sayName == person2.sayName) // false
复制代码
由于都是作同样的事,因此不必定义两个不一样的 Function
实例。何况,this
对象能够把函数 与对象的绑定推迟到运行时。 要解决这个问题,能够把函数定义转移到构造函数外部:
function Person(name, age, job) {
this.name = name
this.age = age
this.job = job
this.sayName = sayName
}
function sayName() {
console.log(this.name)
}
let person1 = new Person('XHS-rookies', 18, 'Software Engineer')
let person2 = new Person('XHS-boos', 18, 'Teacher')
person1.sayName() // XHS-rookies
person2.sayName() // XHS-boos
复制代码
在这里,sayName()
被定义在了构造函数外部。在构造函数内部,sayName
属性等于全局 sayName()
函数。由于这一次 sayName
属性中包含的只是一个指向外部函数的指针,因此 person1
和 person2
共享了定义在全局做用域上的 sayName()
函数。这样虽然解决了相同逻辑的函数重复定义的问题,但全局做用域也所以被搞乱了,由于那个函数实际上只能在一个对象上调用。若是这个对象须要多个方法, 那么就要在全局做用域中定义多个函数。这会致使自定义类型引用的代码不能很好地汇集一块儿。这个新问题能够经过原型模式来解决。
每一个函数都会建立一个 prototype
属性,这个属性是一个对象,包含应该由特定引用类型的实例 共享的属性和方法。实际上,这个对象就是经过调用构造函数建立的对象的原型。使用原型对象的好处是,在它上面定义的属性和方法能够被对象实例共享。原来在构造函数中直接赋给对象实例的值,能够直接赋值给它们的原型,以下所示:
function Person() {}
Person.prototype.name = 'XHS-rookies'
Person.prototype.age = 18
Person.prototype.job = 'Software Engineer'
Person.prototype.sayName = function () {
console.log(this.name)
}
let person1 = new Person()
person1.sayName() // "XHS-rookies"
let person2 = new Person()
person2.sayName() // "XHS-rookies"
console.log(person1.sayName == person2.sayName) // true
复制代码
使用函数表达式也能够:
let Person = function () {}
Person.prototype.name = 'XHS-rookies'
Person.prototype.age = 18
Person.prototype.job = 'Software Engineer'
Person.prototype.sayName = function () {
console.log(this.name)
}
let person1 = new Person()
person1.sayName() // "XHS-rookies"
let person2 = new Person()
person2.sayName() // "XHS-rookies"
console.log(person1.sayName == person2.sayName) // true
复制代码
这里,全部属性和 sayName()
方法都直接添加到了Person
的 prototype
属性上,构造函数体中什么也没有。但这样定义以后,调用构造函数建立的新对象仍然拥有相应的属性和方法。与构造函数模式不一样,使用这种原型模式定义的属性和方法是由全部实例共享的。所以 person1
和 person2
访问的都是相同的属性和相同的 sayName()
函数。要理解这个过程,就必须理解 ECMAScript
中原型的本质。(详细学习 ECMAScript
中的原型请见:对象原型)
有读者可能注意到了,在前面的例子中,每次定义一个属性或方法都会把 Person.prototype
重写一遍。为了减小代码冗余,也为了从视觉上更好地封装原型功能,直接经过一个包含全部属性和方法 的对象字面量来重写原型成为了一种常见的作法,以下面的例子所示:
function Person() {}
Person.prototype = {
name: 'XHS-rookies',
age: 18,
job: 'Software Engineer',
sayName() {
console.log(this.name)
},
}
复制代码
在这个例子中,Person.prototype
被设置为等于一个经过对象字面量建立的新对象。最终结果是同样的,只有一个问题:这样重写以后,Person.prototype
的 constructor
属性就不指向 Person
了。在建立函数时,也会建立它的prototype
对象,同时会自动给这个原型的 constructor
属性赋值。而上面的写法彻底重写了默认的prototype
对象,所以其 constructor
属性也指向了彻底不一样的新对象(Object
构造函数),再也不指向原来的构造函数。虽然 instanceof
操做符还能可靠地返回值,但咱们不能再依靠 constructor
属性来识别类型了,以下面的例子所示:
let friend = new Person()
console.log(friend instanceof Object) // true
console.log(friend instanceof Person) // true
console.log(friend.constructor == Person) // false
console.log(friend.constructor == Object) // true
复制代码
这里,instanceof
仍然对 Object
和 Person
都返回 true
。但 constructor
属性如今等于 Object
而不是 Person
了。若是constructor
的值很重要,则能够像下面这样在重写原型对象时专门设置一 下它的值:
function Person() {}
Person.prototype = {
constructor: Person,
name: 'XHS-rookies',
age: 18,
job: 'Software Engineer',
sayName() {
console.log(this.name)
},
}
复制代码
此次的代码中特地包含了 constructor
属性,并将它设置为 Person
,保证了这个属性仍然包含恰当的值。 但要注意,以这种方式恢复 constructor
属性会建立一个 [[Enumerable]]
为 true
的属性。而原生 constructor
属性默认是不可枚举的。所以,若是你使用的是兼容 ECMAScript
的 JavaScript
引擎, 那可能会改成使用 Object.defineProperty()
方法来定义 constructor
属性:
function Person() {}
Person.prototype = {
name: 'XHS-rookies',
age: 18,
job: 'Software Engineer',
sayName() {
console.log(this.name)
},
}
// 恢复 constructor 属性
Object.defineProperty(Person.prototype, 'constructor', {
enumerable: false,
value: Person,
})
复制代码
前几节深刻讲解了如何只使用 ECMAScript 5
的特性来模拟相似于类(class-like
)的行为。不难看出,各类策略都有本身的问题,也有相应的妥协。正由于如此,实现继承的代码也显得很是冗长和混乱。
为解决这些问题,ECMAScript 6
新引入的class
关键字具备正式定义类的能力。类(class
)是 ECMAScript
中新的基础性语法糖结构,所以刚开始接触时可能会不太习惯。虽然 ECMAScript 6
类表面 上看起来能够支持正式的面向对象编程,但实际上它背后使用的仍然是原型和构造函数的概念。
与函数类型类似,定义类也有两种主要方式:类声明和类表达式。这两种方式都使用 class
关键 字加大括号:
// 类声明
class Person {}
// 类表达式
const Animal = class {}
复制代码
与函数表达式相似,类表达式在它们被求值前也不能引用。不过,与函数定义不一样的是,虽然函数声明能够提高,但类定义不能:
console.log(FunctionExpression) // undefined
var FunctionExpression = function () {}
console.log(FunctionExpression) // function() {}
console.log(FunctionDeclaration) // FunctionDeclaration() {}
function FunctionDeclaration() {}
console.log(FunctionDeclaration) // FunctionDeclaration() {}
console.log(ClassExpression) // undefined
var ClassExpression = class {}
console.log(ClassExpression) // class {}
console.log(ClassDeclaration) // ReferenceError: ClassDeclaration is not defined
class ClassDeclaration {}
console.log(ClassDeclaration) // class ClassDeclaration {}
复制代码
另外一个跟函数声明不一样的地方是,函数受函数做用域限制,而类受块做用域限制:
{
function FunctionDeclaration() {}
class ClassDeclaration {}
}
console.log(FunctionDeclaration) // FunctionDeclaration() {}
console.log(ClassDeclaration) // ReferenceError: ClassDeclaration is not defined
复制代码
类能够包含构造函数方法、实例方法、获取函数、设置函数和静态类方法,但这些都不是必需的。 空的类定义照样有效。默认状况下,类定义中的代码都在严格模式下执行。
与函数构造函数同样,多数编程风格都建议类名的首字母要大写,以区别于经过它建立的实例(好比,经过 class Foo {}
建立实例 foo
):
// 空类定义,有效
class Foo {}
// 有构造函数的类,有效
class Bar {
constructor() {}
}
// 有获取函数的类,有效
class Baz {
get myBaz() {}
}
// 有静态方法的类,有效
class Qux {
static myQux() {}
}
复制代码
类表达式的名称是可选的。在把类表达式赋值给变量后,能够经过 name
属性取得类表达式的名称字符串。但不能在类表达式做用域外部访问这个标识符。
let Person = class PersonName {
identify() {
console.log(Person.name, PersonName.name)
}
}
let p = new Person()
p.identify() // PersonName PersonName
console.log(Person.name) // PersonName
console.log(PersonName) // ReferenceError: PersonName is not defined
复制代码
constructor
关键字用于在类定义块内部建立类的构造函数。方法名 constructor
会告诉解释器 在使用 new
操做符建立类的新实例时,应该调用这个函数。构造函数的定义不是必需的,不定义构造函 数至关于将构造函数定义为空函数。
实例化
使用 new
操做符实例化 Person 的操做等于使用 new
调用其构造函数。惟一可感知的不一样之处就 是,JavaScript
解释器知道使用 new
和类意味着应该使用 constructor
函数进行实例化。 使用 new
调用类的构造函数会执行以下操做。
(1)在内存中建立一个新对象。
(2)这个新对象内部的 [[Prototype]]
指针被赋值为构造函数的 prototype
属性。
(3)构造函数内部的 this
被赋值为这个新对象(即 this
指向新对象)。
(4)执行构造函数内部的代码(给新对象添加属性)。
(5)若是构造函数返回非空对象,则返回该对象;不然,返回刚建立的新对象。
来看下面的例子:
class Animal {}
class Person {
constructor() {
console.log('person ctor')
}
}
class Vegetable {
constructor() {
this.color = 'orange'
}
}
let a = new Animal()
let p = new Person() // person ctor
let v = new Vegetable()
console.log(v.color) // orange
复制代码
类实例化时传入的参数会用做构造函数的参数。若是不须要参数,则类名后面的括号也是可选的:
class Person {
constructor(name) {
console.log(arguments.length)
this.name = name || null
}
}
let p1 = new Person() // 0
console.log(p1.name) // null
let p2 = new Person() // 0
console.log(p2.name) // null
let p3 = new Person('Jake') // 1
console.log(p3.name) // Jake
复制代码
默认状况下,类构造函数会在执行以后返回 this
对象。构造函数返回的对象会被用做实例化的对 象,若是没有什么引用新建立的 this
对象,那么这个对象会被销毁。不过,若是返回的不是 this
对 象,而是其余对象,那么这个对象不会经过 instanceof
操做符检测出跟类有关联,由于这个对象的原型指针并无被修改。
class Person {
constructor(override) {
this.foo = 'foo'
if (override) {
return {
bar: 'bar',
}
}
}
}
let p1 = new Person(),
p2 = new Person(true)
console.log(p1) // Person{ foo: 'foo' }
console.log(p1 instanceof Person) // true
console.log(p2) // { bar: 'bar' }
console.log(p2 instanceof Person) // false
复制代码
类构造函数与构造函数的主要区别是,调用类构造函数必须使用 new
操做符。而普通构造函数若是不使用 new
调用,那么就会以全局的 this
(一般是 window
)做为内部对象。调用类构造函数时若是 忘了使用 new
则会抛出错误:
function Person() {}
class Animal {}
// 把 window 做为 this 来构建实例
let p = Person()
let a = Animal()
// TypeError: class constructor Animal cannot be invoked without 'new'
复制代码
类构造函数没有什么特殊之处,实例化以后,它会成为普通的实例方法(但做为类构造函数,仍然要使用 new
调用)。所以,实例化以后能够在实例上引用它:
class Person {}
// 使用类建立一个新实例
let p1 = new Person()
p1.constructor()
// TypeError: Class constructor Person cannot be invoked without 'new'
// 使用对类构造函数的引用建立一个新实例
let p2 = new p1.constructor()
复制代码
类的语法能够很是方便地定义应该存在于实例上的成员、应该存在于原型上的成员,以及应该存在 于类自己的成员。
1. 实例成员
每次经过 new
调用类标识符时,都会执行类构造函数。在这个函数内部,能够为新建立的实例(this
) 添加“自有”属性。至于添加什么样的属性,则没有限制。另外,在构造函数执行完毕后,仍然能够给 实例继续添加新成员。
每一个实例都对应一个惟一的成员对象,这意味着全部成员都不会在原型上共享:
class Person {
constructor() {
// 这个例子先使用对象包装类型定义一个字符串
// 为的是在下面测试两个对象的相等性
this.name = new String('xhs-rookies')
this.sayName = () => console.log(this.name)
this.nicknames = ['xhs-rookies', 'J-Dog']
}
}
let p1 = new Person(),
p2 = new Person()
p1.sayName() // xhs-rookies
p2.sayName() // xhs-rookies
console.log(p1.name === p2.name) // false
console.log(p1.sayName === p2.sayName) // false
console.log(p1.nicknames === p2.nicknames) // false
p1.name = p1.nicknames[0]
p2.name = p2.nicknames[1]
p1.sayName() // xhs-rookies
p2.sayName() // J-Dog
复制代码
2. 原型方法与访问器
为了在实例间共享方法,类定义语法把在类块中定义的方法做为原型方法。
class Person {
constructor() {
// 添加到 this 的全部内容都会存在于不一样的实例上
this.locate = () => console.log('instance')
}
// 在类块中定义的全部内容都会定义在类的原型上
locate() {
console.log('prototype')
}
}
let p = new Person()
p.locate() // instance
Person.prototype.locate() // prototype
复制代码
能够把方法定义在类构造函数中或者类块中,但不能在类块中给原型添加原始值或对象做为成员数据:
class Person {
name: 'xhs-rookies'
}
// Uncaught SyntaxError: Unexpected token
复制代码
类方法等同于对象属性,所以可使用字符串、符号或计算的值做为键:
const symbolKey = Symbol('symbolKey')
class Person {
stringKey() {
console.log('invoked stringKey')
}
[symbolKey]() {
console.log('invoked symbolKey')
}
['computed' + 'Key']() {
console.log('invoked computedKey')
}
}
let p = new Person()
p.stringKey() // invoked stringKey
p[symbolKey]() // invoked symbolKey
p.computedKey() // invoked computedKey
复制代码
类定义也支持获取和设置访问器。语法与行为跟普通对象同样:
class Person {
set name(newName) {
this.name_ = newName
}
get name() {
return this.name_
}
}
let p = new Person()
p.name = 'xhs-rookies'
console.log(p.name) // xhs-rookies
复制代码
3. 静态类方法
能够在类上定义静态方法。这些方法一般用于执行不特定于实例的操做,也不要求存在类的实例。与原型成员相似,静态成员每一个类上只能有一个。 静态类成员在类定义中使用 static
关键字做为前缀。在静态成员中,this
引用类自身。其余所 有约定跟原型成员同样:
class Person {
constructor() {
// 添加到 this 的全部内容都会存在于不一样的实例上
this.locate = () => console.log('instance', this)
}
// 定义在类的原型对象上
locate() {
console.log('prototype', this)
}
// 定义在类自己上
static locate() {
console.log('class', this)
}
}
let p = new Person()
p.locate() // instance, Person {}
Person.prototype.locate() // prototype, {constructor: ... }
Person.locate() // class, class Person {}
复制代码
静态类方法很是适合做为实例工厂:
class Person {
constructor(age) {
this.age_ = age
}
sayAge() {
console.log(this.age_)
}
static create() {
// 使用随机年龄建立并返回一个 Person 实例
return new Person(Math.floor(Math.random() * 100))
}
}
console.log(Person.create()) // Person { age_: ... }
复制代码
4. 非函数原型和类成员
虽然类定义并不显式支持在原型或类上添加成员数据,但在类定义外部,能够手动添加:
class Person {
sayName() {
console.log(`${Person.greeting} ${this.name}`)
}
}
// 在类上定义数据成员
Person.greeting = 'My name is'
// 在原型上定义数据成员
Person.prototype.name = 'xhs-rookies'
let p = new Person()
p.sayName() // My name is xhs-rookies
复制代码
注意 类定义中之因此没有显式支持添加数据成员,是由于在共享目标(原型和类)上添 加可变(可修改)数据成员是一种反模式。通常来讲,对象实例应该独自拥有经过
this
引用的数据(注意在不一样状况下使用this
的状况会略有些不一样,详细this
学习请见this-MDN)。
5. 迭代器与生成器方法
类定义语法支持在原型和类自己上定义生成器方法:
class Person {
// 在原型上定义生成器方法
*createNicknameIterator() {
yield 'xhs-Jack'
yield 'xhs-Jake'
yield 'xhs-J-Dog'
}
// 在类上定义生成器方法
static *createJobIterator() {
yield 'xhs-Butcher'
yield 'xhs-Baker'
yield 'xhs-Candlestick maker'
}
}
let jobIter = Person.createJobIterator()
console.log(jobIter.next().value) // xhs-Butcher
console.log(jobIter.next().value) // xhs-Baker
console.log(jobIter.next().value) // xhs-Candlestick maker
let p = new Person()
let nicknameIter = p.createNicknameIterator()
console.log(nicknameIter.next().value) // xhs-Jack
console.log(nicknameIter.next().value) // xhs-Jake
console.log(nicknameIter.next().value) // xhs-J-Dog
复制代码
由于支持生成器方法,因此能够经过添加一个默认的迭代器,把类实例变成可迭代对象:
class Person {
constructor() {
this.nicknames = ['xhs-Jack', 'xhs-Jake', 'xhs-J-Dog']
}
*[Symbol.iterator]() {
yield* this.nicknames.entries()
}
}
let p = new Person()
for (let [idx, nickname] of p) {
console.log(nickname)
}
// xhs-Jack
// xhs-Jake
// xhs-J-Dog
//也能够只返回迭代器实例:
class Person {
constructor() {
this.nicknames = ['xhs-Jack', 'xhs-Jake', 'xhs-J-Dog']
}
[Symbol.iterator]() {
return this.nicknames.entries()
}
}
let p = new Person()
for (let [idx, nickname] of p) {
console.log(nickname)
}
// xhs-Jack
// xhs-Jake
// xhs-J-Dog
复制代码
ECMAScript 6
新增了对象解构语法,能够在一条语句中使用嵌套数据实现一个或多个赋值操做。简单地说,对象解构就是使用与对象匹配的结构来实现对象属性赋值。 下面的例子展现了两段等价的代码,首先是不使用对象解构的:
// 不使用对象解构
let person = {
name: 'xhs-Matt',
age: 18,
}
let personName = person.name,
personAge = person.age
console.log(personName) // xhs-Matt
console.log(personAge) // 18
复制代码
而后,是使用对象解构的:
// 使用对象解构
let person = {
name: 'xhs-Matt',
age: 18,
}
let { name: personName, age: personAge } = person
console.log(personName) // xhs-Matt
console.log(personAge) // 18
复制代码
使用解构,能够在一个相似对象字面量的结构中,声明多个变量,同时执行多个赋值操做。若是想让变量直接使用属性的名称,那么可使用简写语法,好比:
let person = {
name: 'xhs-Matt',
age: 18,
}
let { name, age } = person
console.log(name) // xhs-Matt
console.log(age) // 18
复制代码
解构不成功以及对象解构能够指定一些默认值的状况,这些详细内容能够见咱们的解构赋值文章,在对象中咱们不过多赘述。
本章前面花了大量篇幅讨论如何使用 ES5
的机制实现继承。ECMAScript 6
新增特性中最出色的一 个就是原生支持了类继承机制。虽然类继承使用的是新语法,但背后依旧使用的是原型链。
ES6
类支持单继承。使用 extends
关键字,就能够继承任何拥有 [[Construct]]
和原型的对象。 很大程度上,这意味着不只能够继承一个类,也能够继承普通的构造函数(保持向后兼容):
class Vehicle {}
// 继承类
class Bus extends Vehicle {}
let b = new Bus()
console.log(b instanceof Bus) // true
console.log(b instanceof Vehicle) // true
function Person() {}
// 继承普通构造函数
class Engineer extends Person {}
let e = new Engineer()
console.log(e instanceof Engineer) // true
console.log(e instanceof Person) // true
复制代码
派生类都会经过原型链访问到类和原型上定义的方法。this
的值会反映调用相应方法的实例或者类:
class Vehicle {
identifyPrototype(id) {
console.log(id, this)
}
static identifyClass(id) {
console.log(id, this)
}
}
class Bus extends Vehicle {}
let v = new Vehicle()
let b = new Bus()
b.identifyPrototype('bus') // bus, Bus {}
v.identifyPrototype('vehicle') // vehicle, Vehicle {}
Bus.identifyClass('bus') // bus, class Bus {}
Vehicle.identifyClass('vehicle') // vehicle, class Vehicle {}
复制代码
注意: extends
关键字也能够在类表达式中使用,所以 let Bar = class extends Foo {}
是有效的语法。
派生类的方法能够经过 super
关键字引用它们的原型。这个关键字只能在派生类中使用,并且仅限于类构造函数、实例方法和静态方法内部。在类构造函数中使用 super
能够调用父类构造函数。
class Vehicle {
constructor() {
this.hasEngine = true
}
}
class Bus extends Vehicle {
constructor() {
// 不要在调用 super()以前引用 this,不然会抛出 ReferenceError
super() // 至关于 super.constructor()
console.log(this instanceof Vehicle) // true
console.log(this) // Bus { hasEngine: true }
}
}
new Bus()
复制代码
在静态方法中能够经过 super
调用继承的类上定义的静态方法:
class Vehicle {
static identify() {
console.log('vehicle')
}
}
class Bus extends Vehicle {
static identify() {
super.identify()
}
}
Bus.identify() // vehicle
复制代码
注意: ES6
给类构造函数和静态方法添加了内部特性 [[HomeObject]]
,这个特性是一个指针,指向定义该方法的对象。这个指针是自动赋值的,并且只能在 JavaScript 引擎内部访问。super
始终会定义为[[HomeObject]]
的原型。
super
只能在派生类构造函数和静态方法中使用。class Vehicle {
constructor() {
super()
// SyntaxError: 'super' keyword unexpected
}
}
复制代码
super
关键字,要么用它调用构造函数,要么用它引用静态方法。class Vehicle {}
class Bus extends Vehicle {
constructor() {
console.log(super)
// SyntaxError: 'super' keyword unexpected here
}
}
复制代码
super()
会调用父类构造函数,并将返回的实例赋值给 this
。class Vehicle {}
class Bus extends Vehicle {
constructor() {
super()
console.log(this instanceof Vehicle)
}
}
new Bus() // true
复制代码
super()
的行为如同调用构造函数,若是须要给父类构造函数传参,则须要手动传入。class Vehicle {
constructor(licensePlate) {
this.licensePlate = licensePlate
}
}
class Bus extends Vehicle {
constructor(licensePlate) {
super(licensePlate)
}
}
console.log(new Bus('1337H4X')) // Bus { licensePlate: '1337H4X' }
复制代码
super()
,并且会传入全部传给派生类的 参数。class Vehicle {
constructor(licensePlate) {
this.licensePlate = licensePlate
}
}
class Bus extends Vehicle {}
console.log(new Bus('1337H4X')) // Bus { licensePlate: '1337H4X' }
复制代码
super()
以前引用this
。class Vehicle {}
class Bus extends Vehicle {
constructor() {
console.log(this)
}
}
new Bus()
// ReferenceError: Must call super constructor in derived class
// before accessing 'this' or returning from derived constructor
复制代码
super()
,要么必须在其中返回 一个对象。class Vehicle {}
class Car extends Vehicle {}
class Bus extends Vehicle {
constructor() {
super()
}
}
class Van extends Vehicle {
constructor() {
return {}
}
}
console.log(new Car()) // Car {}
console.log(new Bus()) // Bus {}
console.log(new Van()) // {}
复制代码
为了方便操做原始值,ECMAScript
提供了 3 种特殊的引用类型:Boolean
、Number
和 String
。 这些类型具备本章介绍的其余引用类型同样的特色,但也具备与各自原始类型对应的特殊行为。每当用到某个原始值的方法或属性时,后台都会建立一个相应原始包装类型的对象,从而暴露出操做原始值的 各类方法。来看下面的例子:
let s1 = 'xhs-rookies'
let s2 = s1.substring(2)
复制代码
在这里,s1
是一个包含字符串的变量,它是一个原始值。第二行紧接着在 s1
上调用了 substring()
方法,并把结果保存在 s2
中。咱们知道,原始值自己不是对象,所以逻辑上不该该有方法。而实际上 这个例子又确实按照预期运行了。这是由于后台进行了不少处理,从而实现了上述操做。具体来讲,当 第二行访问 s1
时,是以读模式访问的,也就是要从内存中读取变量保存的值。在以读模式访问字符串 值的任什么时候候,后台都会执行如下 3 步:
(1)建立一个 String
类型的实例;
(2)调用实例上的特定方法;
(3)销毁实例。
能够把这 3 步想象成执行了以下 3 行 ECMAScript
代码:
let s1 = new String('xhs-rookies')
let s2 = s1.substring(2)
s1 = null
复制代码
这种行为可让原始值拥有对象的行为。对布尔值和数值而言,以上 3 步也会在后台发生,只不过 使用的是 Boolean
和 Number
包装类型而已。 引用类型与原始值包装类型的主要区别在于对象的生命周期。在经过 new
实例化引用类型后,获得 的实例会在离开做用域时被销毁,而自动建立的原始值包装对象则只存在于访问它的那行代码执行期 间。这意味着不能在运行时给原始值添加属性和方法。好比下面的例子:
let s1 = 'xhs-rookies'
s1.color = 'red'
console.log(s1.color) // undefined
复制代码
这里的第二行代码尝试给字符串 s1 添加了一个 color
属性。但是,第三行代码访问 color
属性时, 它却不见了。缘由就是第二行代码运行时会临时建立一个 String
对象,而当第三行代码执行时,这个对象已经被销毁了。实际上,第三行代码在这里建立了本身的 String
对象,但这个对象没有 color
属性。
能够显式地使用 Boolean
、Number
和String
构造函数建立原始值包装对象。不过应该在确实必 要时再这么作,不然容易让开发者疑惑,分不清它们究竟是原始值仍是引用值。在原始值包装类型的实 例上调用 typeof
会返回 "object"
,全部原始值包装对象都会转换为布尔值true
。
另外,Object
构造函数做为一个工厂方法,可以根据传入值的类型返回相应原始值包装类型的实 例。好比:
let obj = new Object('xhs-rookies')
console.log(obj instanceof String) // true
复制代码
若是传给 Object
的是字符串,则会建立一个 String
的实例。若是是数值,则会建立 Number
的 实例。布尔值则会获得 Boolean
的实例。
注意,使用 new
调用原始值包装类型的构造函数,与调用同名的转型函数并不同。例如:
let value = '18'
let number = Number(value) // 转型函数
console.log(typeof number) // "number"
let obj = new Number(value) // 构造函数
console.log(typeof obj) // "object"
复制代码
在这个例子中,变量 number
中保存的是一个值为 25 的原始数值,而变量 obj
中保存的是一个 Number
的实例。
虽然不推荐显式建立原始值包装类型的实例,但它们对于操做原始值的功能是很重要的。每一个原始值包装类型都有相应的一套方法来方便数据操做。
一:全部对象都有原型。
除了基本对象(base object
),全部对象都有原型。基本对象能够访问一些方法和属性,好比 .toString
。这就是为何你可使用内置的 JavaScript
方法!全部这些方法在原型上都是可用的。虽然JavaScript
不能直接在对象上找到这些方法,但 JavaScript
会沿着原型链找到它们,以便于你使用。
二:如下哪一项会对对象 person 有反作用?
const person = {
name: 'Lydia Hallie',
address: {
street: '100 Main St',
},
}
Object.freeze(person)
复制代码
person.name = "Evan Bacon"
delete person.address
person.address.street = "101 Main St"
person.pet = { name: "Mara" }
Answer:C
使用方法 Object.freeze
对一个对象进行 冻结。不能对属性进行添加,修改,删除。
然而,它仅对对象进行浅冻结,意味着只有 对象中的 直接 属性被冻结。若是属性是另外一个 object
,像案例中的 address
,address
中的属性没有被冻结,仍然能够被修改。
三:使用哪一个构造函数能够成功继承Dog
类?
class Dog {
constructor(name) {
this.name = name
}
}
class Labrador extends Dog {
// 1
constructor(name, size) {
this.size = size
}
// 2
constructor(name, size) {
super(name)
this.size = size
}
// 3
constructor(size) {
super(name)
this.size = size
}
// 4
constructor(name, size) {
this.name = name
this.size = size
}
}
复制代码
Answer:B
在子类中,在调用 super
以前不能访问到 this
关键字。 若是这样作,它将抛出一个 ReferenceError:1
和 4 将引起一个引用错误。
使用 super
关键字,须要用给定的参数来调用父类的构造函数。 父类的构造函数接收 name
参数,所以咱们须要将 name
传递给 super
。
Labrador
类接收两个参数,name
参数是因为它继承了 Dog
,size
做为 Labrador
类的额外属性,它们都须要传递给 Labrador
的构造函数,所以使用构造函数 2 正确完成。
JavaScript 系列的对象,咱们到这里结束啦,谢谢各位对做者的支持!大家的关注和点赞,将会是咱们前进的最强动力!谢谢你们!