对JS中对象和类的记录!!

81 阅读11分钟

对象

创建对象

Object创建

let person = new Object()

对象字面量

let person = {}

属性的类型

数据属性

  • [[Configurable]]---delete是否有效,特性是否可修改,是否可以将其更改为服务器属性,即:不可配置

  • [[Enumerable]]---是否可通过for-in循环返回,不可枚举

  • [[Writable]]---属性值是否可以被更改,不可更改

  • [[Value]]---实际的值

直接定义在对象上的属性:Value默认为undefined,其他默认为true

使用Object.defineProperty()定义时,如果configurable,enumerable,writable未指定,则其默认为false

let person = {}

Object.defineProperty(person,'nData',{
    configurable:false,//严格模式下,尝试【删除】只读属性的值会抛出错误,
    //configurable:false的操作不可逆,此后使用Object.defineProperty操作该属性会报错。
    
    enumerable:false,
    writable:false,//严格模式下,尝试【修改】只读属性的值会抛出错误
    value:'测试!!'
})

console.log(person.nData)

服务器属性

  • [[Configurable]]---delete是否有效,特性是否可修改,是否可以将其更改为数据属性

  • [[Enumerable]]---是否可通过for-in循环返回

  • [[Get]]---读取时调用,默认为undefined

  • [[Set]]---更改时调用,默认为undefined

Get和Set默认为undefined,其他默认为true

let person = {
    nData__:'测试!!',
}

Object.defineProperty(person,'nData',{
    configurable:false,//严格模式下,尝试【删除】只读属性的值会抛出错误,该操作不可逆,此后使用Object.defineProperty操作该属性会报错
    enumerable:false,

    get(){//严格模式下,如果没有定义get(),在修改时会抛出错误,非严格模式则返回undefined
        return this.nData__
    },
    set(value){//严格模式下,如果没有定义set(),在修改时会抛出错误
        this.nData__ = value
    }
})

console.log(person.nData)
person.nData = '更改!!'
console.log(person.nData)

相关方法

定义一个属性

Object.defineProperty(目标对象,属性名称,描述符对象)

定义多个属性

Object.defineProperties(目标对象,描述符对象)

let person = {
}

Object.defineProperties(person,{
    data_1:{
        value:'第一个数据!!'
    },
    data_2:{
        value:'第二个数据!!'
    }
})

console.log(person.data_1)
console.log(person.data_2)

读取(查看)一个属性的特性

Object.getOwnPropertyDescriptor(目标对象,属性名称)

let person = {
}

Object.defineProperty(person,'nData',{
    writable:true,
    value:'测试!!'
})

let descriptor = Object.getOwnPropertyDescriptor(person,'nData')

console.log(descriptor)
//打印如下:
// {
//     value: '测试!!',
//     writable: true,
//     enumerable: false,//未指定,默认为false
//     configurable: false//未指定,默认为false
// }

读取(查看)多个属性的特性

Object.getOwnPropertyDescriptors(目标对象)

let person = {
    textData:'第二个数据!!'
}

Object.defineProperty(person,'nData',{
    writable:true,
    value:'测试!!'
})

let descriptor = Object.getOwnPropertyDescriptors(person)

console.log(descriptor)

//打印如下:
// {
//     textData: {
//       value: '第二个数据!!',
//       writable: true,
//       enumerable: true,
//       configurable: true
//     },
//     nData: {
//       value: '测试!!',
//       writable: true,
//       enumerable: false,
//       configurable: false
//     }
//   }

合并对象

Object.assign(目标对象,...源对象)

let person = {
    textData:'person!!',
    name:'微尔利特.伊芙加登'
}

let person2 = {
    textData:'person2!!'
}

//1参为目标对象,后参为源对象,实际是将后参中可枚举的属性复制到1参
let res = Object.assign(person,person2)

console.log(res)
// { textData: 'person2!!', name: '微尔利特.伊芙加登' }
console.log(person)//Object.assign()在返回新对象的同时,也修改了目标对象
// { textData: 'person2!!', name: '微尔利特.伊芙加登' }
  • Object.assign()为浅复制,相同属性取最后;可以使用Object.assign()实现对象浅复制。
  • Object.assign()没有“回滚”的概念,如果赋值中途出错,则只完成已复制的部分

相等判断

Object.is(1参,2参)

只接收两个参数

//类似于===,但有如下区别
console.log(Object.is(+0,-0))//false   ===比较为true
console.log(Object.is(0,-0))//false    ===比较为true
console.log(Object.is(+0,0))//true

console.log(Object.is(NaN,NaN))//true 和isNaN(NaN)相同效果,使用===比较为false

获取[[Prototype]]的值

Object.getPrototypeOf(目标对象)

更改[[Prototype]]的值

Object.setPrototypeOf(目标对象,新父亲)

  • 重写对象的原型继承关系,严重影响性能,不建议使用,可以通过Object.create()实现效果

创建对象,并为其指定原型

Object.create(新父亲)

let person = {
    name:'巴布'
}

let person2 = Object.create(person)//返回一个指定了继承的新对象
person2.size = 21

console.log(person2.name)//巴布
console.log(person2.size)//21
console.log(Object.getPrototypeOf(person2) === person)//true

确定属性是否在目标对象上

obj.hasOwnProperty(属性名)

  • 属性存在于目标对象上返回true,不存在则返回false
let obj = function(){
    this.a = 'a'
}
obj.prototype.b = 'b'

let test = new obj()

console.log(test.hasOwnProperty('a'))//true 来自实例
console.log(test.hasOwnProperty('b'))//false 来自原型

对象迭代

可枚举

  • Object.keys(目标对象):返回对象实例上所有可枚举属性所构成的数组

  • Object.values(目标对象):返回对象实例上所有可枚举属性的所构成的数组

  • Object.entries(目标对象):返回对象实例上所有可枚举属性和值(键值对)所构成的数组

let obj = function(){
    this.a = 'a-值'
    this.c = 'c-值'
}
obj.prototype.b = 'b-值'

let test = new obj()

console.log(Object.keys(test))//[ 'a', 'c' ]
console.log(Object.values(test))//[ 'a-值', 'c-值' ]
console.log(Object.entries(test))//[ [ 'a', 'a-值' ], [ 'c', 'c-值' ] ]

for-in循环遍历包括原型上的属性在内的所有可枚举属性

  • for-in和Object.keys()的枚举顺序是不确定的。
  • Object.getOwnPropertyNames(),Object.getOwnPropertySymbols(),Object.assign()的枚举顺序是确定的。

列出所有实例的属性

所有属性,包括不可枚举属性

Object.getOwnPropertyNames(目标对象)

列出所有实例的符号属性

所有属性,包括不可枚举属性

Object.getOwnPropertySymbols(目标对象)

创建对象

工厂模式

function createObj(name){//工厂-加工并返回一个对象
    let o = new Object()
    o.name = name
    o.setName = function(p){
        this.name = p
    }
    return o
}

let obj1 = createObj('杰骆驼')

console.log(obj1.name)//杰骆驼
obj1.setName('术士猎人!!')
console.log(obj1.name)//术士猎人!!

存在的问题

  • 没法确定创建的对象是什么类型,即对象标识问题

构造函数模式

  • 解决了对象标识问题
function createObj(name){//这是一个构造函数
    this.name = name
    this.setName = function(p){
        this.name = p
    }
}

let obj1 = new createObj('洋葱骑士!!')//使用new关键字实例化对象

console.log(obj1.name)//洋葱骑士!!
obj1.setName('干杯!!')
console.log(obj1.name)//干杯!!

存在的问题

  • 相同逻辑的函数重复初始化 将函数提到外部虽然能够解决上述问题,但又出现了新的问题:
  • 将函数提到外部后导致代码过于分散

原型模式

理解原型

  1. 只要创建一个函数,其内部就会自行创建一个prototype属性指向其原型对象。

  2. 默认情况下,所有原型对象都自动获得一个指向其构造函数的constructor属性。

  3. 原型对象默认只会获得constructor属性,其他所有方法都继承自Object。

  4. 每次实例化时,实例中都会有一个[prototype]指针指向原型。

实现

let obj1 = function(weapon){
    this.weapon = weapon
}
obj1.prototype.setweapon = function(weapon){//将一部分内容放在原型上
    this.weapon = weapon
}

let test = new obj1('中子灭杀')

console.log(test.weapon)//中子灭杀
test.setweapon('地爆天星')
console.log(test.weapon)//地爆天星

test.weapon = '!!!' //实例中的属性遮蔽了原型对象上面的同名属性
console.log(test.weapon)//'!!!'

存在的问题

  • 虽然原型上的属性不可更改,但引用类型内部不受限制,内部会被更改并共享。

继承

原型链继承

  • 将父类型的实例赋到子类型的原型上,子类型的原型上就有了一个指向父类型原型的指针
function person(){}

person.prototype.getName = function(p){
    return '测试!!'
}

function person2(){}

person2.prototype = new person()//继承原型上的属性和方法

let test = new person2()

console.log(test.getName())

原型链继承的弊端

  1. 因为所有子类型使用同一套原型上的数据,虽然基本类型无法通过子类更改,但引用类型会被更改并共享,导致数据混乱。

  2. 子类型在实例化时不能给父类型的实例化传参。

盗用构造函数

  • 通过call()或apply()传入子类型this调用父类型构造方法,达到继承父类型上实例属性的效果
function person(name){
    this.name = name
}

function person2(){
    person.call(this,'测试!!')
}

let test = new person2()

console.log(test.name)

盗用构造函数的弊端

  1. 必须在构造函数中定义方法,导致方法不能复用。

  2. 子类型不能访问父类型原型上定义的属性和方法。

组合继承

实际使用最多的继承方式

  • 通过盗用构造函数-继承实例属性。

  • 通过原型链继承-继承原型上的属性和方法。

function person(name){
    this.name = name
}

person.prototype.setName = function(name){
    this.name = name
}

function person2(){
    person.call(this,'测试!!')//继承实例属性
}

person2.prototype = new person()//继承原型上的属性和方法

let test = new person2()

console.log(test.name)//测试!!
test.setName('更改!!')
console.log(test.name)//更改!!

原型式继承

在已有对象的基础上再创建一个对象

let person = {
    name:'巴布'
}

let person2 = Object.create(person)//通过Object.create()实现

console.log(person2.name)//巴布

使用场景

  • 不需要单独创建构造函数,但仍然需要在对象间共享数据的情况(引用类型会被实例间共享,和原型模式一样)

寄生式继承

  • 实现思路类似于寄生构造函数工厂模式:创建一个实现继承的函数,以某种方式增强对象,然后返回该对象。
function createAnother(obj){//将函数增强并返回
    let newObj = Object(obj)//Object()非必须
    newObj.name = '拉妮'
    newObj.setName = function(name){
        this.name = name
    }
    return newObj
}

let obj1 = {size:'36D'}

let obj2 = createAnother(obj1)

console.log(obj2.size)//36D
console.log(obj2.name)//拉妮
obj2.setName('娇小拉妮')
console.log(obj2.name)//娇小拉妮

寄生式组合继承

  • 是组合式继承的升级版本,较组合式继承,效率有所提升,但较复杂

组合式继承问题所在:

父类构造函数始终会被调用两次:一次是在创建子类原型时调用,另外一次是在子类构造函数中调用。 多次调用,影响效率。

function person1(name){
    this.name = name
}

person1.prototype.setName = function(name){
    this.name = name
}

function person2(){
    person1.call(this,'卡尔弗兰兹')
}

function createAnother(obj1,obj2){//对组合式继承中原型链继承部分的替换
    let prototype = Object(obj2.prototype)//创建对象
    prototype.constructor = obj1//增强对象
    obj1.prototype = prototype//赋值对象
}
createAnother(person2,person1)

let test = new person2()

console.log(test.name)//卡尔弗兰兹
test.setName('死亡爪陛下')
console.log(test.name)//死亡爪陛下

new关键字的执行过程

1.生成一个空对象

2.改变新对象proto指向,将其赋值为构造函数的prototype属性

3.将构造函数中的this指向改变为新对象并执行构造函数

4.返回新对象(如果构造函数有返回,则返回构造函数的返回,否则返回新对象)

类定义

块级作用域

类就是一种特殊的函数

声明式

class Person {}//类声明不会提升

类表达式

const Person = class PersonName{}
const Person = class {}//简化
  • 类表达式名称是可选的;在把类表达式赋值给变量后可以通过name属性取得类表达式的名称字符串,但不能在表达式作用域外部访问这个标识符。
  • 类内部:PersonName.name和Person.name返回PersonName
  • 类外部:Person.name返回PersonName
  • PersonName在类外部无法访问。

类构造函数

constructor

class Person {
    constructor(name){//类构造函数-实例化时被调用执行,类构造函数必须配合new使用,否则抛出错误!
        this.name = name
    }
}

let obj1 = new Person('未闻蹊语')

console.log(obj1.name)//未闻蹊语

类内部构成

class Person {
    constructor(name){//实例化时被调用执行
        this.name = name//使用this定义的变量都存在于实例上
    }
    setName(name){//被定义在了原型对象上
        this.name = name
    }
    static locate(){//静态方法---定义在类本身
        console.log('我是你爸爸!!')
    }
}

let obj1 = new Person('未闻蹊语')

console.log(obj1.name)//未闻蹊语
obj1.setName('!!!')
console.log(obj1.name)//!!!

console.log(obj1.locate)//undefined
Person.locate()//我是你爸爸!!

在类外部是可以手动添加属性到类原型上面的。

类继承

简单继承

extends

class Person {
    constructor(){//实例化时被调用执行
        this.name = '未闻蹊语'
    }
    setName(name){//被定义在了原型对象上
        this.name = name
    }
}

class Person2 extends Person {}//类也可以使用extends继承普通的构造函数

let obj1 = new Person2()

console.log(obj1.name)//未闻蹊语
obj1.setName('!!!')
console.log(obj1.name)//!!!

super关键字

  • 派生类的方法可以通过super关键字引用它们的原型。

  • super只能在派生类中使用,而且仅限于类构造函数·实例方法和静态方法内部。

    • 类构造函数中可以通过supper调用父类构造函数
    • 静态方法中可以通过supper调用父类静态方法
class Person {
    constructor(name){
        this.name = name
    }
    setName(name){
        this.name = name
    }
    static locate(){//静态方法
        console.log('我是你爸爸!!')
    }
}

class Person2 extends Person {
    constructor(name){
        //如果派生类没有构造方法,则JS会自己调用super(),并将派生类接收到的所有参数传递给super()作为参数。
        //有构造函数就必须要有super(),除非构造函数返回一个对象。
        super(name)//调用父类构造方法
        //不能再super前使用this,否则会抛出错误!
        
        this.size = '36D'
    }
    static locate(){//静态方法
        super.locate()//使用super调用父类静态方法
    }
}

let obj1 = new Person2('未闻蹊语')

console.log(obj1.name)//未闻蹊语
obj1.setName('!!!')
console.log(obj1.name)//!!!
console.log(obj1.size)//36D

Person2.locate()//我是你爸爸!!

使用super时的要求

  • 只能在派生类构造函数和静态方法中使用。

  • 不能单独使用super,否则会抛出错误。

  • 调用super()会调用父类构造方法,并将返回的实例赋值给this。

类构造函数和静态方法内部有一个[[HomeObject]]指针,该指针指向定义该方法的对象,这个指针是自动赋值的。

抽象基类

  • 使用场景:一个类需要被提供给其他类继承,但本身不会被实例化

  • ECMAScript内没有支持抽象类的语法,但可以通过new.target实现。

class Person {
    constructor(){
        if(new.target === Person){//new.target保存了通过new关键字调用的类或函数
            throw new Error('不能实例化抽象类!!')
        }
    }
}

class Person2 extends Person {
    constructor(){
        super()
    }
}

let obj1 = new Person()//抛出错误

限制子类必须定义某个方法

class Person {
    constructor(){
        if(new.target === Person){
            throw new Error('不能实例化抽象类!!')
        }
        if(!this.foo){
            throw new Error('派生类必须定义foo方法!!')
        }
    }
}

class Person2 extends Person {
    constructor(){
        super()
    }
    foo(){
        console.log('foo方法!!')
    }
}

let obj1 = new Person2()

继承内置类型

使用场景:可以方便的扩展内置类型

class Person2 extends Array {//继承于Array
    constructor(){
        super()
    }
}

let obj = new Person2()

类混入-多类继承

思路:先让B继承A,C继承B,最后再让Person继承前面合成的超类,达到多类继承的效果。

class A{
    getA(){
        console.log('A')
    }
}
let B = (req)=>{
    return class extends req{
        getB(){
            console.log('B')
        }
    }
}
let C = (req)=>{
    return class extends req{
        getC(){
            console.log('C')
        }
    }
}

function mixi(BaseClass,...nClass){//将传入的类继承为一个超类,再将其返回。
    for(let e of nClass){
        BaseClass = e(BaseClass)
    }
    return BaseClass
}

class Person extends mixi(A,B,C) {}

let obj = new Person()
obj.getA()//A
obj.getB()//B
obj.getC()//C