理解对象

242 阅读12分钟

提到对象,有人可能就想到封装、继承、多态。不过我这里主要是说js的对象,不是面向对象。 在ECMAScript中,对象被定义被一组无序的属性集合,键值对。散列表?。

只能用字符串作为键(数字表示不服?用数字作为键最终是被转为字符串了,为啥不说是数字字符被转为数字,当然是为了方便归类。)

image.png 先重点看一下js对象的属性,个人理解方法是特殊的属性。毕竟,一个对象的属性才是我们看中的,一个空对象,没有对应的方法和属性,要这对象有何用

属性

js对象属性分两种,数据属性(也是我们常用的)和访问器属性。

数据属性

数据属性 包含一个保存数据值的位置。值会从这个位置读取,也会被写入到这个位置。 它有4个特性 \

    [Configurable]  可配置其特性 也就是能否再次调用defineProperty去修改其特性 。如果为false 则对其进行任何 非writable的操作都会抛出错误  也不能delete. 
    [Enumerable] 可遍历(for-in) 
    [Writable] 是否可修改
    [Value] 值 默认 undefined

访问器属性

访问器属性 不包含值 但是有getter 和 setter 来定义如何读取修改属性

    [Configurable] 同数据属性
    [Enumerable]  同数据属性
    [getter] 读取时调用
    [setter] 修改时调用
    

所以直接设值的都是数据属性,设置get set都是访问器属性,并且 configurable enumerable 都是true

const book = {
    get page(){
        return 10
    },
    set page(val){
        this.page = Number(val)
    },
    set log(val){
        return '快去看书'
    },
    name: '百炼刚',
    author:'unknown'
}
// page  log  都是访问器属性  name author 都是数据属性

小结

属性分两种: 数据和访问器。所以一个属性不可能既是数据属性又是访问器属性。 一个属性必然有 configurable 和 enumerable 这两个特性。
configurable 是否可配置 。可配置什么。

1 配置是否可配置 ,所以为false之后不能将其改为true2 配置能否将其从数据属性转为访问器属性 或者反过来。所以为false之后就不能用defineProperty或其复数形式 更改
3 配置能否更改其特性 如 enumerable

总的来说就是能否修改其特性
将一个访问器属性重新定义为数据属性, 那么他的gettersetter就没有了,configurable eunmerable 保持。反过来 就是value writable没有,剩余两个特性保持不变

要想修改属性的特性就要使用Object.defineProperty(obj, key ,opts)。 它也有复数形式

//调用此方法时 enumerable configurable writable 默认false;
        Object.defineProperty(b, 'key1', {
            value: 0,
            enumerable: true ,
            configurable:false,
            writable: true 
        })
/* 只定义 get 或set 会导致这个访问器属性 不可修改 或者 不可读取 但是它的确有这个属性*/
        Object.defineProperty(b, 'name', {
            get(){ return '000'},
            set(val) { 
                if(val ==='111'){
                    this.name = val
                }
            },
        })
// 复数形式        
    Object.defineProperties(b, {
            key2:{
                configurable: true ,
                enumerable:true ,
                get(){
                    return 4
                },
                set(val) {
                    this.key2 = Number(val)
                }

            },
            key3: {
                writable:false ,
                configurable: false ,

            }
        })
      

使用Object.getOwnPropertyDescriptor() 可以获取属性的特性,这个方法同样也有它的的复数形式。

let des =  Object.getOwnPropertyDescriptor(b,'key2')
console.log(des)
        /*{
        configurable: true
        enumerable: false
        value: undefined
        writable: false}
        访问器属性会包括 get  set 类型是 function
        */
// 对应的有 多属性 特性
const features = Object.getOwnPropertyDescriptors(b)
{
    "key1": {
        "value": 0,
        "writable": true,
        "enumerable": true,
        "configurable": true
    },
    "name": {
        "enumerable": false,
        "configurable": false
    },
    "key2": {
        "writable": false,
        "enumerable": true,
        "configurable": true
    },
    "key3": {
        "writable": false,
        "enumerable": false,
        "configurable": false
    }
}

对于可枚举enumerable的理解

之前老是把可枚举想成可读了。 上面说的很清楚了,for in 循环只能遍历可枚举的属性。

我们都知道 in 操作符会爬原型链, for in 循环不推荐使用就是这个原因,但是大多数时候,forin循环似乎又没出什么Bug ,原因就在于,内置对象的原型上的方法大多为不可枚举。 下面看一个简单的例子。

image.png

简单创建了一个空对象,没有任何(实例)属性。 可以看到 , 用 in 判断 valueOf 是在上面的,因为in会爬原型链,但是用 for in 循环 ,结果啥也没有。 众所周知,for in并不是只会遍历实例上的属性,它还会遍历原型上的属性, 但是这里好像不太符合啊。

接着往下看, 用对象方法getOwnPropertyDescriptors获取了实例的原型(也是一个对象)的全部属性描述, 可以看到, 这些个属性(方法)的enumerable 全为false ,所以for in 啥也遍历不出来。

还有一个问题哟,那就是Object.getOwnPropertyDescriptors(obj) 是空的, 其实很明显了,因为__proto__这个属性它是不存在于实例上的,而是存在于原型上的。

创建对象

js 中创建一个对象一般有两种方式字面量和调用构造函数 new 关键字。

字面量

字面量写法,简单直观。

let person  ={name:'无名'}

工厂模式 与构造函数

:下文所有的——proto——都是下划线 __proto__

工厂模式 就是写一个函数 这个函数返回一个对象。这些对象有相同的属性和方法, 但是没有标识去判断, 是哪一类对象。没有办法判断两个对象是否产自同一家工厂

    function factory(){

        return {
            name :'无名',
            walk (){
                console.log('%c奔跑吧', 'color:#23aded')
            }
        }
    }

构造函数 函数内部会把this绑定要实例化的对象, 最后隐式返回this .调用构造函数,需要new 关键字来绑定this。 构造函数解决了对象的标识问题,通过实例的constructor属性可以判断它的厂家。

    function Person(name ) {
        this.name = name ;
        this.walk = function () {
            console.log('%c奔跑吧', 'color:#23aded')
            
        }
    }

如果不使用 new 关键字 直接调用构造函数 那么会给当前函数作用域上文this添加对应的属性和方法, this没有明确值就会指向 globalThis.
使用 new 操作符 会发生如下事件

1在内存中创建一个新对象
2这个新对象内部的[[prototype]]特性被赋值为构造函数的 prototype属性。 这里应该是指实例的__proto__属性。
3构造函数内部的this 被指向这个新建对象。
4 执行代码
5 如果构造函数返回非空对象,则返回该对象,否则返回新建的对象

因此 构造函数一般都不会写return。而且自定义构造函数 首字母大写,这样ecma才能识别,这是规定 。

构造函数本身也是函数。使用new 操作符时,如果不传参,可以不写(),但是这样就不能用链式写法了,会被识别为调用函数的函数。

使用构造函数创建的对象都有自己的出厂标识,constructor,__proto__, 每个实例都有自己的属性和方法, 即便值相同,引用也是不同。

这对于属性来说,很好,但是对于方法来说就显得有些浪费。相同可复用的函数,却在每次实例化都重新定义。解决这个问题,可以把这个函数放在全局作用域中,但是这样一来,全局作用域就乱了,而且自定义类型的代码不能很好的聚集在一起。

原型模式

每个函数都会自带一个prototype属性,该属性是一个对象,包含了该类型实例共享的属性和方法。 原型本身也是用该函数实例化的对象 ,因此, prototype.constructor会指向相应的构造函数。如果这个函数就是构造函数,那么会指向自身。
正常的原型链都会止于 Object.prototype。 Object.prototype.__proto__=== null ;


    function Person2(name ) {
        this.name = name ;
        Person2.prototype.walk = function () {
            console.log('%c奔跑吧', 'color:#23aded')
            
        }
    }

原型链

在通过对象(js万物皆对象)访问属性时,会先在这个对象实例上查找该属性, 如果实例上不存这个属性,那么会去它的原型 __proto__(这个原型也是一个对象,构造函数的原型对象)上继续查找,找到后返回,找不到,就会去原型的原型上继续找,直到找到或者止于Object.prototype, 这样一个过程就可以看到原型链。

这也是为什么挂在原型上的属性方法可以直接使用。如果实例上有同名属性,不会影响到原型,而且它已经在自身找到属性了,就不会再去原型上找了。 对象和构造函数之间没有直接关系, 但是对象和构造函数的原型对象有联系。任何在原型上的修改,会立马反映到实例上,这是对象的属性访问方式决定的。

只要通过对象可以访问,in 操作符就返回 true,而 hasOwnProperty()只有属性存在于实例上

时才返回 true。因此,只要 in 操作符返回 true 且 hasOwnProperty()返回 false,就说明该属性是一个原型属性。 这也是不推荐使用for-in来遍历对象实例的原因之一。

要修改原型上的方法和属性, 除了通过 实例的——proto——属性之外, 还可以调用Object.getPropertyOf(obj) ,对应的有 Object.setPropertyOf(obj, proto)方法直接修改原型,这个方法慎用

另外, 原型的属性如果是一个引用类型,可能发生意外,通过实例去访问这个属性并对其进行修改 ,会影响到原型上的属性, 因为是引用类型。

前面说了对象被定义为无序键值对 ,for-in 和 Object.keys()的顺序都和浏览器 js引擎有关。 但是 Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()和 Object.assign()

的枚举顺序是确定性的。先以升序枚举数值键,然后以插入顺序枚举字符串和符号键。在对象字面量中

定义的键以它们逗号分隔的顺序插入。

继承

很多面向对象语言都支持两种继承:接口继承和实现继承。

前者只继承方法签名,后者继承实际的方法。接口继承在 ECMAScript 中是不可能的,因为函数没有签名。实现继承是 ECMAScript 唯一支持的继承方式,而这主要是通过原型链实现的。 ECMA-262 把原型链定义为ECMAscript的主要继承方式。

基本思路就是子类的原型是父类的一个实例,这样子类就可以顺着原型链访问到父类的公共属性和方法以及默认属性, 从而实现继承。


    function SuperType(){

    }
    function SubType() {
        SubType.prototype = new SubType() ;
        
    }

组合 寄生

组合继承是综合了原型链和 盗用构造函数。

盗用构造函数就是在子构造函数中调用父构造函数,同时绑定this 。优点是可以向父类传参, 缺点就是构造函数的缺点, 不能复用方法, 每次实例化会重新定义方法。

function Dad(name,age) {
    this.name = name;
    this.age = age;
    this.money = "100000";
}
Dad.prototype.fn = function () {
    console.log("喜欢象棋");
}

function Son(name,age) {
    Dad.call(this,name,age);
    this.sex = "男";
}

组合 就是通过原型链来继承方法,通过调用父构造函数继承属性。

function Dad(name,age) {
    this.name = name;
    this.age = age;
    this.money = "100000";
}
Dad.prototype.fn = function () {
    console.log("喜欢象棋");
}

function Son(name,age) {
    Son.prototype = new Dad()
    Dad.call(this,name,age);
    this.sex = "男";
    Son.prototype.fn = function (){} 

虽然es6 给我们提供了类这个语法, 但是js中并没有 类 这个数据类型。声明一个类, 你会发现它的类型是 function typeof。 把类当做是特殊的函数即可。

每个类都可以包括构造函数方法,实例方法、获取函数、设置函数、静态类方法、但也可以都不写。首字母大写。

    class People {
        constructor(name, sex){
            this.name = name ;
            this.sex = sex ;
        }
        get age(){
        return 18
        }
        set age(val){
        
        }
        name = '' ;
        sex = 'male';

        say(word){
            alert(word);
        }

    }

类的构造函数会在 使用new 实例化对象时调用 (1) 在内存中创建一个新对象。

(2) 这个新对象内部的[[Prototype]]指针被赋值为构造函数的 prototype 属性。

(3) 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。

(4) 执行构造函数内部的代码(给新对象添加属性)。

(5) 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。

实例成员 就是只定义在单个实例上, 而不是所有实例共享, 这些就是写在 构造函数内部的

原型方法和属性 就是直接写在构造函数之外的,这种就是所有实例共享,就如同原型

静态成员 用static 关键字定义 可以是一个方法 或者属性。所谓静态成员就是说这个成员属于这个类,而不是它的实例。但是 statc只能声明一个成员。 如果要写额外的,直接在类上挂载就可以了 私有变量 用#做前缀来声明 , 注意#不是关键字 ,而是标识符的一部分.这样的属性或方法只能在类这个作用域中使用。


    class Person{
         static instance;
         constructor(name){
             if(!Person.instance){
                 Person.instance = this;
             }else{
                 return Person.instance;
             }
             this.name = name;
         }
         #name = ''
         
         #getName(){return this.#name}
         
         get #age(){return 18}
         
         sayName(){console.log(this.name)}
         
    }
    
    Person.fn = function(){}

类的继承

可以使用 extends 关键字来继承。这里有一点需要注意。

super 只能在子类中调用 , 在子类的constructor中必须在调用super后才能使用this, 如果子类没有constructor, 子类会自动调用super 且传参 到父类.

super在其他地方可以作为一个父类的实例或者父类本身。 意思就是说,可以通过super访问到父类的公共属性和默认属性, 也可以访问到父类的静态属性。


    class Student extends People {
        constructor(name, sex){
            /**/
            super(name, sex)
        }

        eduNumber = 000;
        class = '' ;
        grade = '' ;
        score = 000;
        sayHellow(){
        super.say()
}
        
    }

虽然创建对象有多种方式, 但是其本质还是构造函数和原型链的应用。

理解原型链的形成,是因为js在访问对象上的属性时,会先查找其私有属性,如果没有则会去其原型对象上查找同名属性,如此而已。 至于这个查找规则的实现,先不用考虑那么多。