“Code tailor”,为前端开发者提供技术相干资讯以及系列根底文章,微信关注“小和山的菜鸟们”公众号,及时获取最新文章。
前言
在开始学习之前,咱们想要告诉您的是,本文章是对JavaScript
语言常识中 “ 对象、类与面向对象编程 ” 局部的总结,如果您已把握上面常识事项,则可跳过此环节间接进入题目练习
- 对象的根本结构
- 对象申明及应用
- 类
- 对象的构造赋值
- 继承
- 包装对象
如果您对某些局部有些忘记,👇🏻 曾经为您筹备好了!
汇总总结
ECMA-262
将对象定义为一组属性的无序汇合。严格来说,这意味着对象就是一组没有特定程序的值。对象的每个属性或办法都由一个名称来标识,这个名称映射到一个值。正因为如此(以及其余还未探讨的起因),能够把ECMAScript
的对象设想成一张散列表,其中的内容就是一组名 / 值对,值能够是数据或者函数。
对象的根本结构
创立自定义对象的通常形式是创立 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
构造函数加原型继承的语法糖而已。
工厂模式
工厂模式是一种家喻户晓的设计模式,广泛应用于软件工程畛域,用于形象创立特定对象的过程。上面的例子展现了一种依照特定接口创建对象的形式:
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 个办法的对象。这种工厂模式尽管能够解决创立多个相似对象的问题,但没有解决对象标识问题(即新创建的对象是什么类型)。
构造函数模式
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 {}
是无效的语法。
构造函数、HomeObject 和 super()
派生类的办法能够通过 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 时要留神几个问题
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
的实例。
尽管不举荐显式创立原始值包装类型的实例,但它们对于操作原始值的性能是很重要的。每个原始值包装类型都有相应的一套办法来不便数据操作。
题目自测
一:所有对象都有原型。
- A: 对
- B: 错
二:以下哪一项会对对象 person 有副作用?
const person = {
name: 'Lydia Hallie',
address: {street: '100 Main St',},
}
Object.freeze(person)
- A:
person.name = "Evan Bacon"
- B:
delete person.address
- C:
person.address.street = "101 Main St"
- D:
person.pet = {name: "Mara"}
三:应用哪个构造函数能够胜利继承 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
}
}
- A: 1
- B: 2
- C: 3
- D: 4
题目解析
一、
Answer:B
除了根本对象(base object
),所有对象都有原型。根本对象能够拜访一些办法和属性,比方 .toString
。这就是为什么你能够应用内置的 JavaScript
办法!所有这些办法在原型上都是可用的。尽管 JavaScript
不能间接在对象上找到这些办法,但 JavaScript
会沿着原型链找到它们,以便于你应用。
二、
Answer:C
应用办法 Object.freeze
对一个对象进行 解冻。不能对属性进行增加,批改,删除。
然而,它仅对对象进行浅解冻,意味着只有 对象中的 间接 属性被解冻。如果属性是另一个 object
,像案例中的 address
,address
中的属性没有被解冻,依然能够被批改。
三、
Answer:B
在子类中,在调用 super
之前不能拜访到 this
关键字。如果这样做,它将抛出一个 ReferenceError:1
和 4 将引发一个援用谬误。
应用 super
关键字,须要用给定的参数来调用父类的构造函数。父类的构造函数接管 name
参数,因而咱们须要将 name
传递给 super
。
Labrador
类接管两个参数,name
参数是因为它继承了 Dog
,size
作为 Labrador
类的额定属性,它们都须要传递给 Labrador
的构造函数,因而应用构造函数 2 正确实现。