ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

JS类和继承

2022-08-08 19:30:28  阅读:208  来源: 互联网

标签:name 继承 age JS 对象 实例 原型 构造函数


类、原型和继承

ES5中类的继承

类(构造函数)

构造函数的名称通常用作类名,构造函数是类的共有标识

//构造函数
function Person(name){
    this.name = name  //实例属性
    //实例方法
    this.getName = function(){
        return this.name
    }
}
//实例对象
let person = new Person("nlf1")  //{ name: 'nlf1', getName: [Function (anonymous)] }

类的实例对象都有一个不可枚举的属性constructor属性指向类(构造函数)

console.log(person.constructor === Person)  //true

构造函数是类的公有标识,但是原型是类的唯一标识

//实际上instanceof 运算符并不会检测person是否由Person()构造函数初始化而来,
//而是会检查person是否继承自Person.prototype console.log(person instanceof Person) //true 检查实例对象所在的类 console.log(person instanceof Object) //true 继承至原型链顶端的Object类

所有实例的方法并不是同一个

let person1 = new Person("nlf1")
console.log(person1.getName === person.getName)  //false  创建一个对象就会创建一个新的物理空间

 如果不使用new关键字,Person是一个普通的全局作用域中的函数

//全局作用域中调用函数的this指向window全局对象
Person('nlf1')
console.log(window.name)
console.log(window.getName())

构造函数创建实例对象的过程和工厂模式类似

function createPerson(name){
    let person = new Object()
    person.name = name
    person.getName = function(){
        return this.name
    }
    return person
}
let person1 = createPerson('nlf1')
let person2 = createPerson('nlf2')
console.log(person1.getName === person2.getName)  //false

  工厂模式虽然抽象了创建具体对象的过程,解决了创建多个相似对象的问题,但是没有解决对象的识别问题,即如何知道对象的类型,而类(构造函数)创建的实例对象可以事别实例对象对应哪个原型对象(注意原型对象是类的唯一标识,当且仅当两个对象继承自同一个原型对象,才属于同一个类的实例,而构造函数不能作为类的唯一标识)

构造函数创建的过程:

  • 创建一个新对象
  • 将构造函数的作用域赋给新对象(this新对象)
  • 执行构造函数中的代码
  • 返回新对象(最终返回的是new出来的实例对象,因此this执行实例对象)

原型

原型的特性
//Person类(构造函数、Function类的实例对象、函数)
function Person(name){
    this.name = name
}
//Person类是一个Function类的实例
//Person继承自Function
console.log(Person instanceof Function)  //true

只要创建一个类,就会为类创建一个prototype属性,这个属性指向类的原型对象。在默认情况下,原型对象会自动获得一个constructor属性,这个属性对应类本身

console.log(Person.prototype.constructor === Person)  //true

类的所有实例共享一个原型对象,如果实例对象的方法可以通用,可以通过原型对象共享方法,而不需要为每一个实例对象开辟一个需要使用的实例方法

Person.prototype.getName = function(){
    return this.name
}
let person = new Person('nlf1')
let person1 = new Person('nlf2')
//两个实例对象引用的是同一个原型方法
console.log(person.getName === person1.getName)  //true
console.log(person.__proto__ === Person.prototype)  //true
console.log(Person.prototype.isPrototypeOf(person))  //true

读取原型链的属性和方法时,会向上遍历搜索,首先搜索实例对象本身有没有同名属性和方法,有则返回,如果没有,则继续搜索实例对象对应的原型对象的方法和属性

创建类的时候会默认给类创建一个prototype属性,是类的原型对象的引用,也可以重写该类的原型对象

function Person(name){
    this.name = name
}
Person.prototype = {
    getName:function(){
        return this.name
    }
}
let person = new Person('nlf')
//此时Person类原型对象是一个新的对象,注意和Person.prototype.getName 的区别
console.log(Person.prototype.constructor === Object)  //true

Person.prototype = {
    //
    constructor:Person,
    getName:function(){
        return this.name
    }
}

console.log(Person.prototype.constructor === Object)  //true
//未重写原型对象前的实例仍然指向未重写前的原型对象
for(let key in person){
    console.log(key)   //name,getName
}

let person1 = new Person('nlf1')
//重写的原型对象的constructor属性变成了可以枚举的属性
for(let key in person){
    console.log(key)   //name,constructor,getName
}
//将constructor属性设置为不可枚举的属性
Object.defineProperty(Person.prototype,'constructor',{
    enumerable:false,
    value:Person
})
for(let key in person){
    console.log(key)   //name,getName
}
原型的弊端

  原型对象的基本类型数据的属性的共享对于实例对象而言非常便捷有效,但是原型对象的引用类型属性不同,原型对象的引用类型的属性存放在一个指针(物理地址),指针最终指向的是一个连续的物理块,因此通过原型对象的引用类型修改的值都是改变这个物理块中的值,因此所有实例对象的指向都会发生变化。

function Person(name){
    this.name = name
}
Person.prototype = {
    constructor:Person,
    getName:function(){
        return this.name
    },
    age:28,
    names:['nlf1','nlf2']
}
let person = new Person()
person.names[0] = 'nlf_modify'
person.age = 18
let person1 = new Person()
console.log(person1.names)  //['nlf_modify','nlf2']
console.log(person1.age)  //18
组合构造函数和原型

构造函数定义实例属性,原型对象定义共享的方法和基本数据类型的属性

function Person(name){
    this.name = name
    this.names =['nlf1','nlf2']
}
Person.prototype = {
    constructor:Person,
    getName:function(){
        return this.name
    }
}
let person = new Person()
person.names[0] = 'nlf_modify'
let person1 = new Person()
console.log(person1.names) //['nlf1','nlf2']

继承

继承分为接口继承和实现继承,ECMAScript只支持实现继承,实现继承主要依靠原型链

原型链

假如有2个类(Son和Father),Son类对应一个原型对象,通过Son类创建的实例对象都包含一个指向Son类原型对象的内部指针。假如让Son类的原型对象引用Father类的实例对象,则Son类的原型对象将包含一个指向Father类的原型对象的内部指针,从而使Son类的原型可以共享Father类原型对象和实例对象的方法和属性,从而使Son类的实例对象可以共享Father类原型对象的方法和属性,这就是原型链。

原型链实现了方法和属性的继承,此时Son类是子类,继承了Father类这个父类

function Father(){
    this.names = ['nlf1','nlf2']
}
function Son(){}
Father.prototype.getName = function(){
    return Father.name
}
Son.prototype = new Father()
console.log(Son.prototype.constructor === Father)  //true
let son = new Son()
console.log(son.names)  //['nlf1','nlf2']

实现原型链,本质上就是扩展了原型搜索机制

  • 搜索实例
  • 搜索实例的原型
  • 搜索实例的原型的原型
  • ......

该继承有一个缺陷,在原型中使用引用类型的属性,在所有的实例对象中的该属性都引用了同一个物理空间,一旦空间的值发生了变化,那么该实例对象的该属性值也会跟着发生变化

son.names.push('nlf3')
let son1 = new Son()
console.log(son1.names)  //['nlf1','nlf2','nlf3']
借用构造函数(伪造对象或经典继承)

为了避开原型中引用类型数据的问题,做到子类继承父类的实例对象的实例方法和实例属性,在子类的构造函数中调用父类的构造函数,从而使子类的this对象在父类构造函数中执行,并最终返回的是子类的this对象

function Father(){
    this.names = ['nlf1','nlf2']
}
function Son(){
    //此时的Father类当作一个普通的执行函数
    //此时只是让Son类的实例对象创建了和Father类实例对象一样的实例属性和实例方法
    Father.call(this)  //相当于在Son构造函数中执行了this.names = ['nlf1','nlf2']
}
let son = new Son()
son.names.push('nlf3')
let son2 = new Son()
console.log(son2.names)  //['nlf1','nlf2']

如果此时父类有自己的实例属性,而子类也有自己的实例属性

function Father(name,age){
    this.name = name
    this.age = age
}
function Son(){
    Father.apply(this,arguments)
    this.job = arguments[2]
}
let son = new Son('nlf',18,'web前端')
console.log(son)  //{name:'nlf',age:18,job:'web前端'}
组合继承

为了防止原型对象引用类型在实例对象中是同一指针的问题,在原型链的实现中可以混合借用构造函数实现组合继承

function Father(name,age){
    this.name = name
    this.age = age
    this.name = []  //引用类型的实例属性
}
Father.prototype.getNames = function(){
    return this.names
}
function Son(name,age,job){
    //借用构造函数,this执行的过程中也会创建this.names实例属性
    Father.call(this,name,age)
    this.job = job
}
//创建原型,此时Son类的原型对象中还有Father类的实例对象的属性和方法
Son.prototype = new Father()
//调整Son原型对象的constructor指向
Son.prototype.constructor = Son

let son = new Son('nlf1',18,'web')
son.names.push('nlf2')
console.log(son)  //{name:'nlf1',age:18,job:'web',names:['nlf2']}
let son2 = new Son('nlf1',18,'web')
//son.names和son2.names是不同的指针,指向不同的物理空间
console.log(son2.names)  //[]
类型 优缺点
构造函数模式 可以创建不同实例属性的副本,包括引用类型的实例属性,但是不能共享方法
原型模式 引用类型的属性对于实例对象而言共享同一个物理空间,因此可以共享方法
原型链 对父类实现方法和属性继承的过程中,父类实例对象的引用类型属性在子类的实例中共享同一个物理空间,因为父类的实例对象指向了子类的原型对象
借用构造函数 解决了继承中的引用值类型共享物理空间的问题,但是没法实现方法的共享
组合继承 属性的继承使用借用构造函数的方法,方法的继承使用原型链技术,即解决了引用值共享的问题,又实现了方法的共享,但是子类的原型对象中还存在父类实例对象的属性

寄生组合继承

目前而言,组合继承已经解决了大部分问题,但是也有缺陷,就是会调用两次父类的构造函数,一次是实现原型时使子类的原型等于父类的实例对象调用了父类构造函数,一次是使用子类构造函数时调用了一次父类构造函数。

寄生组合继承可以解决在继承过程中子类的原型对象中还存在父类实例对象的实例属性问题

//继承o原型对象的方法和属性
//需要注意o的引用类型属性在F实例对象中共享同一个物理空间
function objectCreate(o){
    function F(){}
    F.prototype = o  //F类实例对象共享o的属性和方法
    return new F()
}
//SubClass实现对于SuperClass原型对象的方法和属性的继承
function inheritPrototype(SubClass,SuperClass){
    var prototype = objectCreate(SuperClass.prototype)
    prototype.constructor = SubClass
    SubClass.prototype = prototype
}

function Father(name,age){
    this.name = name
    this.age = age
}
Father.prototype.getName = function(){
    return this.name
}
function Son1(name,age,job){
    Father.apply(this,arguments)
    this.job = job
}
function Son2(name,age,job){
    Father.apply(this,arguments)
    this.job = job
}

//继承组合的写法
Son1.prototype = new Father()
Son1.prototype.constructor = Son1
// Father {
//     name: undefined,
//     age: undefined,
//     constructor: [Function: Son1]
//   }
console.log(Son1.prototype) 

//寄生组合的写法(借用构造函数实现构造函数的方法和属性的继承)
inheritPrototype(Son2,Father)  //实现原型对象的方法和属性的继承
//Father { constructor: [Function: Son2] }
console.log(Son2.prototype)

let son1 = new Son1('nlf1',18,'web')
let son2 = new Son2('nlf2',28,'front-end')
son1.getName()
son2.getName()

ES6中类的继承

ES6中的类只是ES5封装后的语法糖而已

ES6:

class Es6Person{
    construcor(name,age){
        this.name = name
        this.age = age
        //实例对象的方法
        this.getName = ()=>{
            return this.name
        }
    }
    //Person类原型对象的方法
    getAge(){
        return this.age
    }
}

ES5:

function Es5Person(name,age){
    this.name = name
    this.age = age
    this.getName = ()=>{
        return this.name
    }
}
Es5Person.prototype.getAge = function(){
    return this.age
}

在ES5中类的原型对象的方法是可枚举的,但是ES6中不可枚举

console.log(Object.keys(Es6Person.prototype)) //[]
console.log(Object.keys(Es5Person.prototype))  //['getAge']

在ES5中,如果不使用new,this指向window全局对象,在ES6中,如果不用new关键字会报错

let person2 = Es6Person('nlf1',18)

ES6中的类是不会声明提升的,ES5可以

在ES6中如果不写构造函数

class Es6Person{}
//等同于
class Es6Person{
    construcor(){}
}

ES6中类的属性名可以采用表达式

const getAge = Symbol("getAge")
class Es6Person{
    constructor(name,age){
        this.name = name
        this.age = age
        this.getName = ()=>{
            return this.name
        }   
     }
     //表达式
     [getAge](){
        return this.age
     }
}
let es6Person = new Es6Person('nlf1',18)
es6Person[getAge]()
类的私有方法和私有属性

在ES6的类中目前并没有私有方法和私有属性的标准语法,但是可以通过其他方式模拟实现,例如属性名采用表达式。而ES5很难实现

const getAge = Symbol("getAge")
const job = Symbol('job')
class Es6Person{
    constructor(name,age){
        this.name = name
        this.age = age
        this.getName = ()=>{
            return this.name
        }   
        //私有属性
        this[job] = 'web'
     }
     //私有方法
     [getAge](){
        return this.age
     }
}
let es6Person = new Es6Person('nlf1',18)
es6Person[getAge]()
es6Person[job]

  如果此时Es6Person类处在一个文件中,那么getAge和job变量都是当前文件的局部变量,外部文件无法访问,从而在外部文件调用Es6Person类的时候无法访问具有唯一性的getAge和job变量,从而使之成为私有方法和私有属性。

类的getter和setter
class Es6Person{
    constructor(name,age){
        this.name = name
        this.age = age
    }
    get name(){
        return 'nlf1'
    }
    set name(value){
        console.log('setter',value)
    }
}
let person = new Es6Person('nlf1',18)  //setter:nlf1
console.log(person.name)  //nlf1
person.name = 'nlf2'  //setter:nlf2
类的Generator方法
class Es6Person{
    constructor(...args){
        this.args = args
    }
    *[Symbol.iterator](){
        for(let arg of this.args){
            yield arg
        }
    }
}
let person = new Es6Person('nlf1',18)
for(let value of person){
    console.log(value)  // nlg1   18
}
类的静态方法
class Es6Person{
    //类的静态属性
    static getClassName(){
        return 'ES6Person'
    }
    //类的静态方法
    static getName(){
        //this指向类,而不是实例对象
        return this.getClassName()
    }
}
console.log(Es6Person.getName())  //ES6Person

类的继承

ES5的继承使用借助构造函数实现,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上。ES6的继承机制完全不同,实质是先创造父类的实例对象this(所以必须调用super方法),然后再用子类的构造函数修改this。

子类必须在constructor方法中调用super方法,否则新建实例时就会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用Super方法,子类就得不到this对象。

class ES6Person{
    constructor(name){
        this.name = name
    }
}
class Es6WebDeveloper extends Es6Person{
    constructor(name,age){
        super(name)
        //表示父类构造函数
        //子类实例的构建,是基于对父类实例加工,只有super方法才能返回父类实例
        this.age = age
    }
}

   此时的继承类似于ES5的寄生组合继承,子类的原型对象中没有父类的实例对象和实例方法

//ES6Person {constructor:f}
console.log(Es6WebDeveloper.prototype)

ES6在继承的语法上考虑的更加周到,不仅继承了类的原型对象,还继承了类的静态属性和静态方法

class ES6Person{
    satic getClassName(){
        return ES6Person.name
    }
    constructor(name){
        this.name = name
    }
}
class Es6WebDeveloper extends Es6Person{
    constructor(name,age){
        super(name)
        this.age = age
    }
}

//ES6Person 
console.log(Es6WebDeveloper.getClassName())
super关键字

super作为函数调用时,代表父类的构造函数,只能在子类的构造函数中使用

class ES6Person{
    constructor(name){
        //Es6WebDeveloper
        console.log(new.target.name)
        //Es6WebDeveloper{}
        console.log(this)
        this.name = name
    }
}
class Es6WebDeveloper extends Es6Person{
    constructor(name,age){
        super(name)
        this.age = age
    }
}
let developer = new Es6WebDeveloper('nlf')

除此之外,super当作一个对象使用时,在子类普通方法中,指向父类的原型对象,因此可以调用父类的原型方法,需要注意的是执行父类的原型方法时,在方法中执行时this执行的是子类的实例对象而不是父类的实例对象

ES6的继承原理

__proto__存在于实例于构造函数的原型对象之间

class Es6Person{
    constructor(name){
        this.name = name
    }
}
let es6Person = new ES6Person('nlf')
//true
console.log(es6Person.__proto__ === ES6Person.prototype)
ES6继承的原理
class Es6Person{}
class Es6WebDeveloper extends ES6Person{}
//Es6WebDeveloper被看作是一个实例对象,因此Es6Person看作是一个原型对象
//因此Es6WebDeveloper继承了Es6Person的所有属性和方法
//实现了类的静态属性和方法的继承,子类的原型是父类
console.log(Es6WebDeveloper.__proto__ === Es6Person)//true
//这里类似于Es6WebDeveloper.prototype = new Es6Person()
//和ES5的原型链一样
//子类的原型对象是父类的原型对象的实例
//子类的实例继承了父类的实例
console.log(Es6WebDeveloper.prototype.__proto__ === ES6Person.prototype)  //true

因此以下继承是可以理解的

class A extends Object{}
A.__proto__ === Object  //true
A.prototype.__proto__ === Object.prototype  //true

class A{}
A.__proto__ === Function.prototype  //true
A.prototype.__proto__ === Object.prototype  //true

 

标签:name,继承,age,JS,对象,实例,原型,构造函数
来源: https://www.cnblogs.com/nielifang/p/16561298.html

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有