IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> 《JavaScript高级程序设计》- 第八章:对象、类与面向对象编程 -> 正文阅读

[JavaScript知识库]《JavaScript高级程序设计》- 第八章:对象、类与面向对象编程

博客

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使用内部特性来描述属性的特征,特征的表达方式:{特征描述}

属性的分类

  1. 数据属性包含一个保存数据值的位置,数据的读写是通过对这个位置的读写操作,有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

  2. 访问器属性:不包含数据值,通过gettersetter函数来访问与设置,同时也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);		// male
console.log(person.birthday);	// 2000/12/12

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 增强的对象语法

  1. 属性简写

    let name = 'zyzc';
    let person = {
        // 原来写法:name:name,
        // 简写:
        name	// 如果没有找到同名变量会报错
    }
    console.log(person.name);	//zyzc
    

    代码压缩程序会在不同作用域保留属性名,防止找不到属性,可以如下操作:

    function makePerson(name){
        return {
            name
        }
    };
    let person = makePerson('zyzc');
    

    个人感觉:有一点创建类的味道

  2. 可计算属性:可以动态命名属性

    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 }
    
  3. 简写方法名字

    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'

注意:nullundefined不能被解构,会被抛出错误

解构的用法:

  1. 嵌套解构

    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;	// 报错
    
  2. 部分解构

    对象的解构过程,不需要按照对象属性的定义顺序进行。

    解构会按着顺序进行,如果过程中某个属性解构报错,则终止解构,导致解构只完成一部分。

    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' } }
    
  3. 参数上下文匹配

    在参数列表中,也可以进行解构赋值。

    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创建对象的时候,会执行如下操作:

  1. 在内存中创建新对象
  2. 新对象内部的[[Prototype]]赋值为构造函数的prototype属性
  3. this指向刚刚创建的对象
  4. 执行构造函数内部的代码(给新的对象添加属性)
  5. 返回该对象

构造函数的特点

  1. 构造函数也是函数:构造函数与函数唯一区别就是调用方法不同

    // 作为构造函数
    let person = new Person('zyzc',21,'student');
    
    // 作为函数
    Person('student1',18,'stduent');	// 添加到了window对象
    window.sayName();	// student1
    
  2. 构造函数的问题:每次创建对象实例的时候,需要将其定义的方法都创建一遍。

    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中的问题

  1. 关于原型的理解方面
  1. 只要创建函数,都会有一个prototype属性,指向原型对象原型对象则会有一个constructor属性,指向构造函数

    function Person(){}
    console.log(Person.prototype.constructor === Person);	// true
    
  2. 每次 调用构造函数创建一个新的实例,该实例内部的prototype指针都会指向该实例的原型对象

  3. 每个实例对象都可以通过__proto__属性访问原型对象

    function Person(){}
    let person = new Person();
    console.log(person.__proto__ === Person.prototype);		// true
    
  4. 所有对象实例都会共享原型对象上的属性和方法【8.2.3开头讲到过】

注意:

1. 正常的原型链都会终止于`Object`的原型对象
1. `Object`的原型对象是`null`
  1. 关于原型层级方面:

在通过对象访问属性的时候,会按照该属性进行搜索,先搜索实例对象本身,然后到其原型,再到其原型的原型,直到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属性才可以。

  1. 关于原型对象的一些方法
  • Object.isPrototypeOf():判断是不是其原型
  • Object.getPrototypeOf():获取原型对象
  • Object.create():创建一个原型的新对象
  • Object.hasOwnProperty():判断属性是否在实例上
  • Object.keys():列出对象可枚举属性【枚举顺序因浏览器而异】
  • Object.getOwnPropertyNames():列出对象所有属性
  • Object.getOwnPropertySymbols():列出对象的符号属性
  • Object.values():返回对象值数组
  • Object.entries():返回对象键值对数组
  1. 重写对象原型:

改造前:每次都要写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
})
  1. 重写原型与动态属性、方法

因为原型上搜索值动态的,所以修改原型的属性、方法都会在实例上反映出来

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]]指针实在调用构造函数的时候自动指向原型的;重写原型就相当于写了一个新的原型,而实例仍然指向原来的原型。

综上:实例只有指向原型的指针,没有指向构造函数的指针

  1. 原生对象原型

对于原始对象而言,也可以修改或添加新的属性和方法。但是不推荐使用,因为会造成冲突!

一般推荐使用自定义类。

  1. 原型存在的问题:

在原型模式上,主要的问题就是:包含引用值的属性

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

注意:

  1. 原型与继承的关系

如果存在继承关系,则可以通过原型链上的一些操作来判断

  • 使用instanceof实例对象原型链上的所有构造函数的运算,返回true。
console.log(b instanceof Object);		// true
console.log(b instanceof SuperObject);	// true
console.log(b instanceof SubObject);	// true
  • 使用Object.isPrototypeOf()方法,实例对象原型对象的运算,返回true
console.log(Object.prototype.isPrototypeOf(b));			// true
console.log(SuperObject.prototype.isPrototypeOf(b));	// true
console.log(SubObject.prototype.isPrototypeOf(b));		// true
  1. 关于方法

实例对象需要覆盖原型的方法添加新的方法,必须在原型赋值后再操作

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
  1. 关于字面量创建原型

由于使用字面量创建原型的方法,就相当于重写了原型链,所以不可以使用子面量创建原型。【在8.2.3第五点提到过

  1. 原型链问题(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' ]
  1. 原型链问题(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 }

盗用构造函数存在问题:

  1. 子类不能访问父类的原型对象

    console.log(studentA.__proto__ === Student.prototype);	// true
    console.log(studentA.__proto__.__proto__ === Person.prototype);	// false
    
  2. 由于不能访问父类的原型对象 ,所以需要在构造函数内定义方法。

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用于定义创建类的构造函数;

  1. 实例化:

在使用new操作符创建类的时候,调用函数constructor进行实例化会执行如下操作:

  • 在内存中创建一个新对象
  • 新对象内部的[[Prototype]]指针赋值为构造函数的prototype属性
  • this指向新的对象
  • 执行构造函数内部代码(给新对象添加属性)
  • 返回该对象
  1. 类构造函数构造函数的区别:

类构造函数必须使用new操作符,不然会报错!

普通构造函数如果不使用new调用,那么this指向window对象

  1. 类 = 特殊函数

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
  1. 类也可以作为参数传递

8.4.3 实例、原型和类成员

类的语法,可以定义实例成员原型成员类成员

  1. 实例成员:在构造函数内部,可以通过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
    
  2. 原型方法:类块中定义的方法会成为原型方法,但是不能在类块中给原型添加原始值对象

    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();// 这是计算属性作为键
    
  3. 访问器

    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
    
  4. 静态类方法:使用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
    
  5. 非函数原型成员和类成员

    虽然类块中不能显式添加类成员、原型成员,但是可以在外部手动添加

    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
    
  6. 迭代器与生成器方法

    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关键字支持继承,但其背后依旧使用的是原型链

  1. 继承基础

ES6支持单继承,不仅仅可以继承类、还可以继承构造函数

class Animal {};
function Person(){};

class dog extends Animal{}
class student extends Person{}

由于继承也是通过原型链构造的,也会像一般对象一样,遵循规则。

  1. 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 {}
  1. 抽象基类

其实就像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
  1. 继承内置对象

通过继承内置对象,可以拓展内置类型。

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
  1. 类混入

把不同类的行为集中到一个类是一种常见的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 小结

  1. JavaScript的继承主要通过原型链来实现。通过子类可以通过原型链搜索到父类的属性、行为,达到共享的状态

  2. 原型链的问题是:所有继承的属性和方法都会在对象实例间共享,无法做到实例私有。

  3. 继承的分类:

    • 原型链
    • 盗用构造函数
    • 组合继承【目前最流行
    • 原型式继承
    • 寄生式继承
    • 寄生式组合继承【最有效
  4. ES6提供了类、继承的语法,可以优雅地解决这些问题。但是类、继承的本质还是在原型链上。

  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2022-02-19 01:03:52  更:2022-02-19 01:04:33 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 10:27:08-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码