博客
zyzcos.gitee.io
第八章:对象、类与面向对象编程
8.1 对象
个人理解:具有特定属性和行为的Object实例
let person = {
// 属性
name:'zyzc',
age:'21',
job:'student',
sayName(){ // 行为
console.log(this.name);
}
}
person.sayName(); // zyzc
8.1.1 属性的类型
ECMA-262使用内部特性 来描述属性的特征 ,特征的表达方式:{特征描述}
属性的分类
-
数据属性 包含一个保存数据值的位置,数据的读写是通过对这个位置的读写操作,有4个特征描述 :
修改特征描述,必须使用Object.defineProperty() ;
-
[[Configurable]]:表示属性是否可以通过delete删除并重新定义,是否可以修改特性,是否可以将其改为访问器访问。直接定义属性的时候,默认为true let person = {
age:21,
};
Object.defineProperty(person,'age',{
configurable:false,
})
delete person.age;
console.log(person.age); // 21
try{
Object.defineProperty(person,'age',{
configurable:true,
})
}catch {
console.log("修改为不可设置后,不能再变回可设置");
}
-
[[Enumerable]]:表示是否可以通过for-in 循环返回,直接定义属性的时候,默认为true -
[[Writable]]:表示属性值是否可以修改,直接定义属性的时候,默认为true let person = {
age:21,
}
Object.defineProperty(person,'age',{
writable:false,
value:22
})
console.log(`修改前${person.age}`); // 22
person.age = 18; // 如果是严格模式下,会报错
console.log(`修改后${person.age}`); // 22
Object.defineProperty(person,'age',{
value:18
})
console.log(`再次修改后${person.age}`); // 18
-
[[Value]]:属性的值,默认为undefined -
访问器属性 :不包含数据值,通过getter 和setter 函数来访问与设置,同时也4个特征描述 :
* [[Configurable]]:表示属性是否可以通过delete删除并重新定义,是否可以修改特性,是否可以将其改为数据属性。直接定义属性的时候,默认为true
* [[Enumerable]]:表示是否可以通过`for-in`循环返回,直接定义属性的时候,默认为true
* [[Get]]:读取属性的时候调用
* [[Set]]:写入属性的时候调用
```JS
let book = {
_year:2022, // 听说是程序员间的约定俗成 _xxx为私有变量,表示为不希望外部直接访问
edition:1
};
Object.defineProperty(book,"year",{
get(){
return this._year;
},
set(newValue){
if(newValue > this._year){
this.edition += newValue - this._year;
this._year = newValue;
}
}
})
console.log(book.year);
console.log(book.edition);
book.year = 2024;
console.log(book.year);
console.log(book.edition);
```
8.1.2 定义多属性
使用Object.defineProperties() 定义多属性:
let person = {
_birthday:'',
sex:'',
};
Object.defineProperties(person,{
_birthday:{
value:"2000/1/1"
},
sex:{
value:'male',
writable:false,
},
birthday:{
get(){
return this._birthday;
},
set(newValue){
this._birthday = newValue + "";
}
}
})
person.sex = 'female';
person.birthday = '2000/12/12';
console.log(person.sex);
console.log(person.birthday);
8.1.3 读取属性特征
使用Object.getOwnPropertyDescriptor(aimObject,descriptor) 方法
let person = {
_birthday:'',
sex:'',
};
Object.defineProperties(person,{
_birthday:{
value:"2000/1/1"
},
sex:{
value:'male',
writable:false,
},
birthday:{
get(){
return this._birthday;
},
set(newValue){
this._birthday = newValue + "";
}
}
})
let descriptor = Object.getOwnPropertyDescriptor(person,"birthday");
console.log(descriptor.value); // undefined
console.log(descriptor.get) // [Function:get]
console.log(typeof descriptor.get) // function
ES7新增了Object.getOwnPropertyDescriptors(aimObject) 静态方法,获取所有特征描述
8.1.4 合并对象
又称为混入(mixin) ;
ES6专门提供了Object.assgin(aimObj,[sourceObj1,...,sourceObjN]) 方法,每个源对象 中可枚举、自有属性都会复制到目标对象 上。
8.1.5 增强的对象语法
-
属性简写 let name = 'zyzc';
let person = {
// 原来写法:name:name,
// 简写:
name // 如果没有找到同名变量会报错
}
console.log(person.name); //zyzc
代码压缩程序 会在不同作用域 保留属性名,防止找不到属性,可以如下操作:
function makePerson(name){
return {
name
}
};
let person = makePerson('zyzc');
个人感觉:有一点创建类的味道
-
可计算属性 :可以动态命名属性 : const nameKey = 'name';
const ageKey = 'age';
let person = {
[nameKey]:'zyzc',
[ageKey]:21
}
console.log(person); // { name: 'zyzc', age: 21 }
当然这种动态命名属性 还可以使用函数
const nameKey = 'name';
const ageKey = 'age';
let index = 0;
function getNameKey(key){
return `${nameKey}_${++index}`;
}
let person = {
[getNameKey(nameKey)]:'zyzc',
[ageKey]:21
}
console.log(person); // { name_1: 'zyzc', age: 21 }
-
简写方法名字 let person = {
sayName: function(name){
console.log("This is my name");
}
}
let student = {
sayName(name){
console.log("This is my name");
}
}
简写方法名 可以和可计算属性 一起使用
8.1.6 对象解构
ES6新增的对象解构语法,可以一个语句,实现多个赋值操作。
let person = {
name:'zyzc',
age:21
};
let { name:MyName,age:MyAge } = person; //MyName=guan MyAge=21;
同时,在解构赋值的时候,也可以定义默认值
let person = {
name:'zyzc',
age:21
};
let { name:MyName,age:MyAge,MyJob="font-end" } = person;
//MyName=guan MyAge=21 MyJob='font-end'
注意:null 和undefined 不能被解构,会被抛出错误
解构的用法:
-
嵌套解构 let personCopy = {};
let person = {
name:'zyzc',
age:21,
job:{
title:'font-end'
}
};
({
name:personCopy.name,
age:personCopy.age,
job:personCopy.job
} = person);
console.log(personCopy);
// { name: 'zyzc', age: 21, job: { title: 'font-end' } }
由于job是一个对象,而两个person对象中,都指向了同一个job对象。
personCopy.job.title = 'back-end';
console.log(personCopy);
// { name: 'zyzc', age: 21, job: { title: 'back-end' } }
console.log(person);
// { name: 'zyzc', age: 21, job: { title: 'back-end' } }
可以进一步对对象中的对象进行解构
let { job:{ title } } = person;
console.log(title); // font-end
不能对未定义的属性进行嵌套解构
let { foo:{ message } } = person; // 报错
-
部分解构 对象的解构过程,不需要按照对象属性的定义顺序进行。
解构会按着顺序进行,如果过程中某个属性解构报错,则终止解构,导致解构只完成一部分。
let personCopy = {};
let person = {
name:'zyzc',
age:21,
job:{
title:'font-end'
}
};
try{
({
name:personCopy.name,
sex:personCopy.sex, // 虽然元对象没有该属性,但是可以获取为undefined
foo:{ bar:personCopy.bar }, // 进行了会报错的嵌套解构
age:personCopy.age,
job:personCopy.job
} = person);
}catch{}
console.log(personCopy);
// { name: 'zyzc', age: 21, job: { title: 'font-end' } }
-
参数上下文匹配 在参数列表中,也可以进行解构赋值。 let person = {
name:'zyzc',
age:21
};
function printPerson(id,{ name , age}){
console.log(arguments);
console.log(name,age);
}
printPerson(1,person);
// [Arguments] { '0': 1, '1': { name: 'zyzc', age: 21 } }
// zyzc 21
8.2 创建对象
对象虽然可以通过构建函数 、字面量表达 可以方便创建对象,但是明显不足的是:创建具有同样接口 的多个对象需要编写很多重复性代码。
8.2.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;
}
8.2.2 构造函数模式
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
console.log(this.name);
}
}
let person = new Person('zyzc',21,'font-end');
当调用new创建对象的时候,会执行如下操作:
- 在内存中创建新对象
- 新对象内部的[[Prototype]]赋值为构造函数的prototype属性
- 将
this 指向刚刚创建的对象 - 执行构造函数内部的代码(给新的对象添加属性)
- 返回该对象
构造函数的特点
-
构造函数也是函数:构造函数与函数唯一区别就是调用方法 不同 // 作为构造函数
let person = new Person('zyzc',21,'student');
// 作为函数
Person('student1',18,'stduent'); // 添加到了window对象
window.sayName(); // student1
-
构造函数的问题:每次创建对象实例的时候,需要将其定义的方法 都创建一遍。 function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
/* this.sayName = function(){
console.log(this.name);
} */
// 等价于
this.sayName = new Function("console.log(this.name)");
}
不妨将函数提取出来,这样所有Person实例对象,都会指向这个外部函数 function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayname;
}
sayName(){
console.log(this.name)
}
但是这样做,会混乱全局作用域 。这个问题有更好的解决方法——使用原型模式 来解决
8.2.3 原型模式
原型对象 ,它上面定义的属性和方法可以被对象实例 共享。
function Person() {};
Person.prototype.name = 'zyzc';
Person.prototype.sayName = function() {
console.log(this.name);
}
let personA = new Person();
personA.sayName(); // zyzc
let personB = new Person();
personB.sayName(); // zyzc
所以可以使用原型模式,解决8.2.2 中的问题
- 关于
原型的理解 方面
-
只要创建函数 ,都会有一个prototype 属性,指向原型对象 。原型对象 则会有一个constructor 属性,指向构造函数 。 function Person(){}
console.log(Person.prototype.constructor === Person); // true
-
每次 调用构造函数 创建一个新的实例,该实例内部的prototype指针 都会指向该实例的原型对象 -
每个实例对象都可以通过__proto__ 属性访问原型对象 function Person(){}
let person = new Person();
console.log(person.__proto__ === Person.prototype); // true
-
所有对象实例 都会共享原型对象 上的属性和方法【8.2.3开头讲到过】
注意:
1. 正常的原型链都会终止于`Object`的原型对象
1. `Object`的原型对象是`null`
- 关于
原型层级 方面:
在通过对象访问属性 的时候,会按照该属性进行搜索,先搜索实例对象本身,然后到其原型,再到其原型的原型,直到Object 或在搜索途中找到该属性。
function Person(){}
Person.prototype.name = 'zyzc';
let person1 = new Person();
let person2 = new Person();
person1.name = 'student';
console.log(person1.name); // student
console.log(person2.name); // zyzc
person1的name属性,在搜索实例对象本身 的时候,就搜索到了name属性的值,所以输出student ;由此可知,我们可以使用同名属性 遮蔽原型上的默认值 。当然,如果要恢复实例对象 和原型对象 之间的关联,需要delete 掉person1上的name属性才可以。
- 关于
原型对象的一些方法 :
Object.isPrototypeOf() :判断是不是其原型Object.getPrototypeOf() :获取原型对象Object.create() :创建一个原型的新对象Object.hasOwnProperty() :判断属性是否在实例上Object.keys() :列出对象可枚举属性【枚举顺序因浏览器而异】Object.getOwnPropertyNames() :列出对象所有属性Object.getOwnPropertySymbols() :列出对象的符号属性Object.values() :返回对象值数组Object.entries() :返回对象键值对数组
- 重写对象原型:
改造前:每次都要写object.prototype.property,显得繁琐
function Person(){};
Person.prototype.job = 'student'
Person.prototype.sayName = function(){
console.log(this.name);
}
第一次重写:
function Person(){};
Person.prototype = {
job:'student',
sayName(){
console.log(this.name)
}
}
虽然说这样可以减少代码量,但是会出现一个问题:未声明constructor 且无指向。
第二次重写:
function Person(){};
Person.prototype = {
constructor:Person,
job:'student',
sayName(){
console.log(this.name)
}
}
别忘了,constructor有一个默认的特性:不可枚举
第三次重写:
function Person(){};
Person.prototype = {
job:'student',
sayName(){
console.log(this.name)
}
}
Object.defineProperty(Person.prototype,"constructor",{
enumerable:false,
value:Person
})
- 重写原型与动态属性、方法
因为原型上搜索值 是动态的 ,所以修改原型的属性、方法都会在实例上反映出来 。
function Person(){};
let friend = new Person();
// 修改原型的方法
Person.prototype.sayHi = function(){
console.log('hi');
}
friend.sayHi(); // hi
但是如果重写对象原型 ,就不会在实例上反映出来
function Person(){};
let friend = new Person();
// 重写原型的方法
Person.prototype = {
sayHello(){
console.log('Hello');
}
}
Object.defineProperty(Person.prototype,"constructor",{
enumerable:false,
value:Person
})
friend.sayHello(); // TypeError: friend.sayHello is not a function
因为实例的[[Prototype]]指针 实在调用构造函数 的时候自动指向原型的;重写原型 就相当于写了一个新的原型 ,而实例仍然指向原来的原型。
综上:实例只有指向原型的指针,没有指向构造函数的指针
- 原生对象原型
对于原始对象而言,也可以修改或添加新的属性和方法。但是不推荐使用,因为会造成冲突!
一般推荐使用自定义类。
- 原型存在的问题:
在原型模式上,主要的问题就是:包含引用值的属性!
function Person(){};
Person.prototype = {
name:'zyzc',
age:21,
job:'student',
friends:['tom','john'],
sayName(){
console.log(this.name);
}
}
Object.defineProperty(Person.prototype,"constructor",{
enumerable:false,
value:Person
})
let studentA = new Person();
let studentB = new Person();
studentA.friends.push('mike'); // 学生A新认识了mike同学
console.log(studentA.friends); // [ 'tom', 'john', 'mike' ]
console.log(studentB.friends); // [ 'tom', 'john', 'mike' ]
为什么学生B 也认识了mike同学 ,原因是:他们两个的friends 属性都指向了同一个数组 !
所以在实际开发中,不单独使用原型模式
8.3 继承
面向对象语言都支持两种继承:接口继承 和实现继承 ,由于函数没有标签,所以只能实现继承 。实现继承 是ECMAScript中唯一支持的继承方式,主要通过原型链 实现。
8.3.1 原型链
ECMA-262把原型链 定义为主要继承方法 。其基本思想就是:通过原型继承多个引用类型的属性和方法
function SuperObject(){ // 定义对象A
this.property = true;
}
SuperObject.prototype.getSuperValue = function(){ // 为对象A原型添加方法A
return this.property;
}
function SubObject(){ // 定义对象B
this.flag = false;
}
// 让对象B继承对象A
SubObject.prototype = new SuperObject();
// 给对象B的原型添加方法B
SubObject.prototype.getSubValue = function(){
return this.flag;
}
// 创建对象B的实例对象
let b = new SubObject();
console.log(b.getSuperValue()); // true
实际上,所有引用类都有一个最终原型Object
注意:
- 原型与继承的关系
如果存在继承关系,则可以通过原型链上的一些操作来判断
- 使用
instanceof ,实例对象 与原型链上的所有构造函数 的运算,返回true。
console.log(b instanceof Object);
console.log(b instanceof SuperObject);
console.log(b instanceof SubObject);
- 使用
Object.isPrototypeOf() 方法,实例对象 与原型对象 的运算,返回true
console.log(Object.prototype.isPrototypeOf(b)); // true
console.log(SuperObject.prototype.isPrototypeOf(b)); // true
console.log(SubObject.prototype.isPrototypeOf(b)); // true
- 关于
方法
当实例对象 需要覆盖原型的方法 或添加新的方法 ,必须在原型赋值后 再操作
function SuperObject(){ // 定义对象A
this.property = true;
}
SuperObject.prototype.getSuperValue = function(){ // 为对象A原型添加方法A
return this.property;
}
function SubObject(){ // 定义对象B
this.flag = false;
}
// 让对象B继承对象A(原型赋值)
SubObject.prototype = new SuperObject();
// 给对象B的原型添加方法B
SubObject.prototype.getSubValue = function(){
return this.flag;
}
// 覆盖对象A的原型的方法
SubObject.prototype.getSuperValue = function(){
return false;
}
let b = new SubObject();
console.log(b.getSuperValue()); // false
- 关于字面量创建原型
由于使用字面量创建原型的方法,就相当于重写了原型链,所以不可以使用子面量创建原型。【在8.2.3第五点提到过 】
- 原型链问题(1)
其实原型链的问题,也就是在原型模式问题的基础之上的拓展。
原型模式中,当属性为引用类型 时,会导致属性共享 。
在原型链中,一个普通对象,声明了引用类型 ,当其变为另一个对象的原型后,这个影响就进一步扩散了。
function SuperObject(){
this.friends = ['tom','john']; // 本来只是一个对象的引用类型属性
}
function SubObject(){}
// 让对象B继承对象A(原型赋值)
SubObject.prototype = new SuperObject(); // 将friends提升到了原型的引用类型属性
// 给对象B的原型添加方法B
SubObject.prototype.makeFriend = function(name){
this.friends.push(name);
}
let a = new SubObject();
let b = new SubObject();
a.makeFriend('mike');
console.log(a.friends); // [ 'tom', 'john', 'mike' ]
console.log(b.friends); // [ 'tom', 'john', 'mike' ]
- 原型链问题(2)
子类在实例化的时候,无法给父类的构造函数传参。
8.3.2 盗用构造函数
为了解决原型链包含引用类型 导致的继承问题,可以采用盗用构造函数 的技术来解决。
其基本思路就是:采用call() 或apply() 在子类构造函数中,调用父类构造函数。
function SuperObject(){
this.friends = ['tom','john']; // 本来只是一个对象的引用类型属性
}
function SubObject(){
SuperObject.call(this);
/*
等价于
this.friends = ['tom','john']
*/
}
// 给对象B的原型添加方法B
SubObject.prototype.makeFriend = function(name){
this.friends.push(name);
}
let a = new SubObject();
let b = new SubObject();
a.makeFriend('mike');
console.log(a.friends); // [ 'tom', 'john', 'mike' ]
console.log(b.friends); // [ 'tom', 'john' ]
除了解决引用类型属性 的问题,盗用构造函数 还实现了子类实例化的时候,可以给父类的构造函数传参。
function Person(name,sex){
this.name = name;
this.sex = sex;
this.type = 'human';
}
Person.prototype.sayHi = function(){
console.log('hi');
}
function Student(id,name,sex){
// 伪继承
Person.call(this,name,sex);
this.id = id;
}
let studentA = new Student(1,'zyzc','male');
console.log(studentA); //Student { name: 'zyzc', sex: 'male', type: 'human', id: 1 }
盗用构造函数 存在问题:
-
子类不能访问父类的原型对象 console.log(studentA.__proto__ === Student.prototype); // true
console.log(studentA.__proto__.__proto__ === Person.prototype); // false
-
由于不能访问父类的原型对象 ,所以需要在构造函数内定义方法。
8.3.3 组合继承
又称伪经典继承 ,其综合了原型链 和盗用构造函数 来完成。
其基本思想:使用原型链 继承原型上的属性和方法;通过盗用构造函数 继承实例属性。
这样即可以把方法定义在原型上以实现重用 又可以让每个实例拥有自己的属性
function Person(name,age){
this.name = name;
this.age = age;
this.friends = ['tom','john'];
}
Person.prototype.sayHi = function(){
console.log('Hi');
}
function Student(id,name,age){
this.id = id;
// 盗用构造函数,实现属性继承
Person.call(this,name,age);
}
Student.prototype = new Person(); // 原型链继承,实现原型对象继承
Student.prototype.makeFriend = function(newFriendName){
this.firends.push(newFriendName)
}
let studentA = new Student(1,'aaa',21);
let studentB = new Student(2,'bbb',21);
student
8.3.4 原型式继承
出发点:不自定义类型 ,也可以通过原型实现对象之间的信息共享
function object(o){
function Temp(){} // 定义一个媒介
Temp.prototype = o; // 将传入对象作为媒介的原型
return new Temp(); // 返回'父类'的'子类'
}
let Person = {
name:'',
friends:['tom','john'],
}
let personA = object(Person);
personA.name = 'personA';
personA.friends.push('mike');
console.log(personA.name); // personA
console.log(personA.friends) // [ 'tom', 'john', 'mike' ]
显然,如果遇到了引用类型 属性,还是会暴露出问题,毕竟也是原型式的。
再者,可以通过Object.create() 方法实现,效果一样的:
let Person = {
name:'',
friends:['tom','john'],
}
let personA = Object.create(Person,{
name:{
value:'personA'
}
});
console.log(personA.name); // personA
总结:原型式继承 适合不需要单独创建构造函数,仍需要在对象间共享信息的场合
8.3.5 寄生式继承
其基本思想:类似于寄生构造函数 和工厂模式 的结合体,创建一个实现继承的函数,以某种方式增强对象 [可以理解为给对象添加属性、方法增强其功能],然后返回该对象。
function createPersonPlus(original) {
let temp = Object.create(original); // 创建一个媒介
temp.sayHi = function() { // 增强对象
console.log('Hi!');
};
return temp;
}
let Person = {
name:'',
}
let personA = createPersonPlus(Person);
personA.sayHi(); // Hi!
注意:通过寄生式继承 给对象添加函数,会导致函数难以重用,与构造函数模式类似。
8.3.6 寄生式组合继承
由于组合式继承存在效率问题:父类构造函数会被调用两次
// 组合式继承
function SuperObject(name){
this.name = name;
}
SuperObject.prototype.sayName = function(){
console.log(this.name);
}
function SubObject(name,age){
SuperObject.call(this); // 盗用构造函数,实现属性继承(第二次调用)
this.age = age;
}
SubObject.prototype = new SuperObject(); // 继承(第一次调用)
SubObject.prototype.constructor = SubObject;
寄生式组合继承:通过盗用构造函数 继承属性,使用混合式原型链 继承方法;基本思路是不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。
// 原型式继承
function object(o){
function Temp(){} // 定义一个媒介
Temp.prototype = o; // 将传入对象作为媒介的原型
return new Temp(); // 返回'父类'的'子类'
}
function inheritPrototype(subObject,superObject){
let prototype = object(superObject.prototype); // 创建'父类'的'子类'
prototype.constructor = subObject; // 增强对象
subObject.prototype = prototype; // 赋值对象
}
function SuperObject(){};
function SubObject(){};
inheritPrototype(subObject,superObject);
寄生式组合继承 算是引用类型继承的最佳模式
8.4 类
前面3节,都是围绕着用ES5的特性来模拟类的行为,各策略有优点也有缺点。但是都逃避不过一个:代码显得冗长、混乱的现象。
为了 解决这个问题,ES6引入了class 关键字,具有正式定义类的能力。虽然表面上可以正式支持面向对象编程,实际上背后仍然是原型和构造函数的概念。
8.4.1 类的定义
类的定义有两种 主要方式:
- 类声明:
class Person {}; - 类表达式:
const Person = 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 {}
类受块作用域限制
类的构成:
类包含:构造函数方法、实例方法、获取函数、设置函数和静态类方法;这些不是必需的
class Foo {} // 空类定义
class Bar {
constructor(){} // 有构造函数的类
}
class Baz {
get myBaz(){} // 有获取函数的类
}
class Sub {
set Sub(){} // 有设置函数的类
}
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
8.4.2 类构造函数
constructor 用于定义创建类的构造函数;
- 实例化:
在使用new 操作符创建类的时候,调用函数constructor 进行实例化 ;会执行如下操作:
- 在内存中创建一个新对象
- 将
新对象 内部的[[Prototype]]指针赋值为构造函数 的prototype属性 this 指向新的对象- 执行构造函数内部代码(给新对象添加属性)
- 返回该对象
类构造函数 与构造函数 的区别:
类构造函数 必须使用new 操作符,不然会报错!
普通构造函数 如果不使用new 调用,那么this指向window对象 。
- 类 = 特殊函数
ECMAScript类就是一种特殊函数
class Person {};
console.log(Person); // [class Person]
console.log(typeof Person); // function
ECMAScript类也具有prototype 属性,该原型的constructor 也指向类自身。
class Person {};
console.log(Person === Person.prototype.constructor);// true
- 类也可以作为参数传递
8.4.3 实例、原型和类成员
类的语法,可以定义实例成员 、原型成员 、类成员
-
实例成员:在构造函数内部,可以通过this 给新创建的实例 添加自有属性 ;即使构造函数执行后,仍然可以继续添加。 class Person {
constructor(){
this.name = new String('zyzc');
this.sayName = ()=> console.log(this.name);
this.nickName = ['fff','yyy'];
}
}
let p1 = new Person(),
p2 = new Person();
// 下面的都不相等,说明二者具有‘自有属性’
console.log(p1.name === p2.name); // false
console.log(p1.sayName === p2.sayName); // false
console.log(p1.nickName === p2.nickName); // false
-
原型方法:类块中定义的方法 会成为原型方法 ,但是不能在类块 中给原型添加原始值 或对象 。 class Person {
constructor(){
this.locate = ()=> console.log('实例调用的');
}
locate(){
console.log('原型调用的');
}
}
let p = new Person();
p.locate(); // 实例调用的
Person.prototype.locate(); // 原型调用的
由于类方法等同于对象属性,也可以使用字符串 、符号 或计算属性 作为键
const symbolKey = Symbol('symbolKey');
class Person {
stringKey(){
console.log("这是字符串作为键");
}
[symbolKey](){
console.log("这是符号作为键");
}
['computed' + 'Key'](){
console.log("这是计算属性作为键");
}
}
let p = new Person();
p.stringKey(); // 这是字符串作为键
p[symbolKey](); // 这是符号作为键
p.computedKey();// 这是计算属性作为键
-
访问器 class Person {
constructor(name){
this._name = name;
}
set name(newName) {
this._name = newName;
}
get name() {
return this._name;
}
}
let p = new Person('zyzc');
console.log(p.name); // zyzc
p.name = 'studentA';
console.log(p.name); // studentA
-
静态类方法:使用static 作为前缀,执行不依赖于实例、类的操作。 class Person {
constructor(){
this.locate = ()=> console.log('实例调用的');
}
locate(){
console.log('原型调用的');
}
static locate(){
console.log('静态类调用的');
}
}
let p = new Person();
p.locate(); // 实例调用的
Person.prototype.locate(); // 原型调用的
Person.locate(); // 静态类调用的
静态类方法非常适合作为实例工厂
class Person {
constructor(age){
this._age = age;
}
sayAge() {
console.log(this._age);
}
static create(){
return new Person(Math.floor(Math.random()*100));
}
}
let p = Person.create();
console.log(p); // Person { _age: 43 }
p.sayAge(); // 43
-
非函数原型成员和类成员
虽然类块中不能显式添加类成员、原型成员,但是可以在外部手动添加 ;
class Person {
sayName(){
console.log(Person.greeting + this.name);
}
}
// 定义类成员
Person.greeting = 'Hi!My name is:';
// 定义原型成员
Person.prototype.name = 'zyzc';
let p = new Person();
p.sayName(); // Hi!My name is zyzc
-
迭代器与生成器方法 class Person {
// 在原型上定义生成器方法
*createNicknameIterator() {
yield 'Jack';
yield 'Jake';
yield 'J-Dog';
}
// 在类上定义生成器方法
static *createJobIterator() {
yield 'Butcher';
yield 'Baker';
yield 'Candlestick maker';
}
}
let jobIter = Person.createJobIterator();
console.log(jobIter.next().value); // Butcher
console.log(jobIter.next().value); // Baker
console.log(jobIter.next().value); // Candlestick maker
let p = new Person();
let nicknameIter = p.createNicknameIterator();
console.log(nicknameIter.next().value); // Jack
console.log(nicknameIter.next().value); // Jake
console.log(nicknameIter.next().value); // J-Dog
由于类支持生成器方法,所以可以把类实例,变为可迭代对象
class Person {
constructor(){
this.nicknames = ['fff','yyy'];
}
*[Symbol.iterator]() {
yield *this.nicknames.entries();
}
}
let p = new Person();
for(let item of p){
console.log(item);
// [ 0, 'fff' ]
// [ 1, 'yyy' ]
}
也可以返回迭代器
class Person {
constructor(){
this.nicknames = ['fff','yyy'];
}
[Symbol.iterator]() {
return this.nicknames.entries();
}
}
let p = new Person();
for(let item of p){
console.log(item);
// [ 0, 'fff' ]
// [ 1, 'yyy' ]
}
8.4.4 继承
虽然ES6有extends 关键字支持继承,但其背后依旧使用的是原型链
- 继承基础
ES6支持单继承,不仅仅可以继承类、还可以继承构造函数
class Animal {};
function Person(){};
class dog extends Animal{}
class student extends Person{}
由于继承也是通过原型链构造的,也会像一般对象一样,遵循规则。
- super()
派生类可以通过super 关键字引用 它们的原型 。
class Animal {
static speak(message){
console.log(message);
}
}
class Dog extends Animal {
constructor(name){
// 不能再super之前使用this,不然会抛出错误
super(); // 调用了父类的构造函数
this.name = name;
}
static speak(){
super.speak('汪汪汪');
}
}
let dogA = new Dog("dogA");
Dog.speak();
使用super 需要注意以下几点:
- 只能在派生
类构造函数 和静态方法 中使用 - 不能单独引用
super ,要么调用构造函数、要么引用静态方法 - 调用
super() 会调用父类构造函数 ,并将返回的实例赋值给this super() 的行为如同调用父类构造函数 ,所以可以传入参数 - 如果子类
没有定义构造函数 ,在实例化子类的时候,自动调用super() 来替代 - 在
类构造函数 中,不能在super() 之前引用this - 子类定义了构造函数,要么调用
super() 、要么返回一个对象return {}
- 抽象基类
其实就像Java中的抽象类 ,只能继承 ,不能被实例化 。
可以通过new.target 实现
class Animal {
constructor(){
if( new.target === Animal){
throw new Error('Animal cannot be directly instantiated');
}
}
}
既然说像Java的抽象类 ,那也可以定义抽象方法 ,要求子类必须定义 这个抽象方法 。
因为原型方法在调用类构造函数之前就存在了,所以可以采用this来检查。
class Animal {
constructor(){
if( new.target === Animal){
throw new Error('Animal cannot be directly instantiated');
}
if( !this.isLive ){
throw new Error('Inheriting class must define isLive() out of constructor');
}
}
}
class Dog extends Animal{
isLive(){}
}
class Cat extends Animal{}
new Dog(); // 创建成功
new Cat(); // Inheriting class must define isLive() out of constructor
- 继承内置对象
通过继承内置对象,可以拓展 内置类型。
class SuperArray extends Array {
shuffle() {
// 添加了洗牌算法
for (let i = this.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[this[i], this[j]] = [this[j], this[i]];
}
}
}
let a = new SuperArray(1, 2, 3, 4, 5);
console.log(a instanceof Array); // true
console.log(a instanceof SuperArray); // true
console.log(a); // [1, 2, 3, 4, 5]
a.shuffle();
console.log(a); // [3, 1, 4, 5, 2]
有一些内置类型的方法会返回新的实例 ,可以通过覆盖Symbol.species 访问器,决定返回什么类型的实例
class SuperArray extends Array {
static get [Symbol.species](){
return Array;
}
}
let a1 = new SuperArray(1,2,3,4,5);
let a2 = a1.filter(x => !!(x%2));
console.log(a1 instanceof SuperArray); // true
console.log(a1 instanceof Array); // true
console.log(a2 instanceof SuperArray); // false
console.log(a2 instanceof Array); // true
- 类混入
把不同类的行为集中到一个类是一种常见的JS模式;虽然没有显示的支持多继承,但是可以模拟这种行为
class Animal {}
// 定义并使其返回一个'混合类'
let HeadMixin = (Superclass) => class extends Superclass {
head(){
console.log('head');
}
}
let BodyMixin = (Superclass) => class extends Superclass {
body(){
console.log('body');
}
}
let FootMixin = (Superclass) => class extends Superclass {
foot(){
console.log('foot');
}
}
function mix(BaseClass,...Mixins) {
return Mixins.reduce((accumulator, current) => current(accumulator), BaseClass);
}
// 模仿出多继承的行为
class Dog extends mix(Animal,HeadMixin,BodyMixin,FootMixin){}
let dog = new Dog();
dog.head(); // head
dog.body(); // body
dog.foot(); // foot
8.5 小结
-
JavaScript的继承主要通过原型链 来实现。通过子类可以通过原型链搜索到父类的属性、行为,达到共享的状态 。 -
原型链的问题是:所有继承的属性和方法都会在对象实例间共享,无法做到实例私有。 -
继承的分类:
- 原型链
- 盗用构造函数
- 组合继承【目前最流行】
- 原型式继承
- 寄生式继承
- 寄生式组合继承【最有效】
-
ES6提供了类、继承的语法,可以优雅地解决这些问题。但是类、继承的本质 还是在原型链 上。
|