七. 面向对象

61 阅读8分钟

七. 面向对象

7.1. 属性描述符

如果属性是直接定义在对象内部,或者直接添加到对象内部,这时候我们就不能对这个属性进行一些限制:比如这个属性是否是可以通过delete删除的?这个属性是否在for-in遍历的时候被遍历出来呢?

 var obj = {
     name:"why",
     age:18
 }
 delete obj.name
 ​
 for(var key in obj){
     console.log(key);
 }

如果我们想要对一个属性进行比较精准的操作控制,那么我们就可以使用属性描述符(Object.defineProperty)。

  • Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

     Object.defineProperty(obj, prop, descriptor)
    
  • 可接收三个参数:

    • obj要定义属性的对象;
    • prop要定义或修改的属性的名称或 Symbol;
    • descriptor要定义或修改的属性描述符;
  • 返回值:

    • 被传递给函数的对象。

7.2. 属性的类型(数据/访问器属性)

7.2.1 数据属性

数据数据描述符有如下四个特性:

  • [[Configurable]]:表示属性是否可以通过delete删除属性,是否可以修改它的特性,或者是否可以将它修改为存取属性描述符;

    • 当我们直接在一个对象上定义某个属性时,这个属性的[[Configurable]]为true;
    • 当我们通过属性描述符定义一个属性时,这个属性的[[Configurable]]默认为false;
  • [[Enumerable]]:表示属性是否可以通过for-in或者Object.keys()返回该属性;

    • 当我们直接在一个对象上定义某个属性时,这个属性的[[Enumerable]]为true;
    • 当我们通过属性描述符定义一个属性时,这个属性的[[Enumerable]]默认为false;
  • [[Writable]]:表示是否可以修改属性的值;

    • 当我们直接在一个对象上定义某个属性时,这个属性的[[Writable]]为true;
    • 当我们通过属性描述符定义一个属性时,这个属性的[[Writable]]默认为false;
  • [[value]]:属性的value值,读取属性时会返回该值,修改属性时,会对其进行修改;

    • 当我们直接在一个对象上定义某个属性时,这个属性的[[value]]特性会被设置为指定的值;
    • 默认情况下这个值是undefined;

7.2.2. 存取属性(访问器属性)

访问器属性不包含数据值(没有[[Writable]]和[[value]])。相反,它们包含一个获取(getter)函数和一个设置(setter)函数,不过这两个函数不是必需的。

  • [[Configurable]]:表示属性是否可以通过delete删除属性,是否可以修改它的特性,或者是否可以将它修改为存取属性描述符;

    • 和数据属性描述符是一致的;
    • 当我们直接在一个对象上定义某个属性时,这个属性的[[Configurable]]为true;
    • 当我们通过属性描述符定义一个属性时,这个属性的[[Configurable]]默认为false;
  • [[Enumerable]]:表示属性是否可以通过for-in或者Object.keys()返回该属性;

    • 和数据属性描述符是一致的;
    • 当我们直接在一个对象上定义某个属性时,这个属性的[[Enumerable]]为true;
    • 当我们通过属性描述符定义一个属性时,这个属性的[[Enumerable]]默认为false;
  • [[get]]:获取属性时会执行的函数。默认为undefined

  • [[set]]:设置属性时会执行的函数。默认为undefined

7.3. 定义多个属性

Object.defineProperties() 方法直接在一个对象上定义 多个 新的属性或修改现有属性,并且返回该对象。

 let book = {}; 
 Object.defineProperties(book, { 
     year_: { 
         value: 2017 
     }, 
     edition: { 
         value: 1 
     }, 
     year: { 
         get() { 
             return this.year_; 
         },  
         set(newValue) { 
             if (newValue > 2017) { 
                 this.year_ = newValue; 
                 this.edition += newValue - 2017; 
             } 
         } 
     } 
 });

这段代码在 book 对象上定义了两个数据属性 year_和 edition,还有一个访问器属性 year。最终的对象跟上一节示例中的一样。唯一的区别是所有属性都是同时定义的,并且数据属性的configurable、enumerable 和 writable 特性值都是 false。

7.4. 对象方法补充

  • 获取对象的属性描述符

    • Object.getOwnPropertyDescriptor(对象,属性名)

      • 返回值是一个对象
    • Object.getOwnPropertyDescriptors(对象)

      • ES7新增方法,返回值是一个对象
     let obj = {
         name: "why",
         age: 18
     }
     ​
     let result = Object.getOwnPropertyDescriptor(obj, "name")
     console.log(result);  
     console.log(result.writable);  
     let results= Object.getOwnPropertyDescriptors(obj)
     console.log(results);
    

    image-20220222135617752.png

  • 禁止对象继续添加新属性

    • Object.preventExtensions(对象)

      • 给一个对象添加新的属性会失败(在严格模式下会报错);
  • 禁止对象配置/删除里面的属性

    • Object.seal(对象)

      • 将现有属性的[[Configurable]] 设为了false
      • 实际是调用preventExtensions,并且将现有属性的configurable:false
  • 冻结属性,禁止属性修改

    • Object.seal(对象)

      • 实际上是调用seal,并且将现有属性的writable: false

7.5. 创建多个对象的方法

创建对象通过会用new Object() 和 字面量方式,都存在一个很大的弊端,创建同样的对象时,需要编写重复的代码

7.5.1. 工厂模式

工厂模式是一种常见的设计模式,缺点是获取不到对象的真实类型,都是Object类型

 function createPerson(name, age, height, addresss) {
     let p = new Object()
     p.name = name
     p.age = age
     p.height = height
     p.addresss = addresss
     return p
 }
 let p1 = createPerson("张三", 18, 180, "北京市")
 let p2 = createPerson("李四", 19, 181, "成都市")
 let p3 = createPerson("王五", 21, 182, "上海市")

7.5.2. 构造函数

什么是构造函数

  • 构造函数也称之为构造器(constructor),通常是我们在创建对象时会调用的函数,
  • 构造函数也是一个普通的函数,从表现形式来说,和千千万万个普通的函数没有任何区别;
  • 如果这么一个普通的函数被使用new操作符来调用了,那么这个函数就称之为是一个构造函数

使用new关键字来调用函数时,会执行如下的操作

  • 在内存中创建一个新对象(空对象)。

  • 这个新对象内部的[[Prototype]]特性被赋值为构造函数的 prototype 属性。222 第 8 章 对象、类与面向对象编程

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

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

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

     // 下面是模拟代码
     function foo() {
         // 1.创建一个空对象
         var moni = {}
         // 2.将函数的prototype赋值给空对象的__proto__
         moni.__proto__ = foo.prototype
         // 3.this指向新对象
         this = moni
         // 4.执行函数内部代码
         // 5.返回moni
         return moni
     }
    

构造函数的缺点

  • 我们需要为每个对象的函数去创建一个函数对象实例;

  • 如果构造函数里没有函数则 没有这个缺点

     function Person() {
         this.name = "why"
         this.eating = function () {
             console.log("再吃东西");
         }
         this.runing = function () {
             console.log("在跑步");
         }
     }
     let p1 = new Person("张三")
     let p2 =new Person("李四")
     // 每次都会执行new Person()时都会给每个对象的eating和runing属性创建函数对象,但每个对象的eating()和runing()都是一样的,每次都在重复的创建eating和runing函数对象
     console.log(p1.eating === p2.eating); // false
     console.log(p1.runing === p2.runing); // false
    

    image-20220223231009024.png

7.5.3. 补充:js模拟实现new函数

 function objectFactory() {
     // 1.创建一个空对象
     var obj = new Object()
     // 删除并返回arguments的第一个参数 (会改变原数组)
     Constructor = [].shift.call(arguments);
     // 2.将函数的prototype赋值给空对象的__proto__
     obj.__proto__ = Constructor.prototype;
     // 3&&4 把Person的this绑定为obj,并执行Person,实际是给obj添加name,age属性
     var ret = Constructor.apply(obj, arguments);
     // ret || obj 这里这么写考虑了构造函数显示返回 null或对象 的情况
     // 返回创建的对象
     return typeof ret === 'object' ? ret || obj : obj;
 };
 ​
 function person(name, age) {
     this.name = name
     this.age = age
     // return null
     // return []
     // return {adress:"成都"}
 }
 let p = objectFactory(person, '布兰', 12)
 console.log(p) // { name: '布兰', age: 12 }

[].shift.call(arguments) 等同 Array.prototype.shift.call(arguments)

shift内部实现是使用的this代表调用对象。那么当[].shift.call() 传入 arguments对象的时候,通过 call函数改变原来 shift方法的this指向, 使其指向arguments,并对arguments进行复制操作,而后返回第一个参数。至此便是完成了arguments类数组转为数组的目的!

blog.csdn.net/Sherry_1997…

7.5.4. 原型和构造函数结合

将这些函数放到Person.prototype的对象上,让所有的对象去共享这些函数,

 function Person() {
     this.name = "why"
 }
 Person.prototype.eating=function(){
     console.log(this.name + "在吃东西");
 }
 Person.prototype.runing=function(){
     console.log(this.name + "在跑步");
 }
 let p1 = new Person("张三")
 let p2 =new Person("李四")

image-20220224102809569.png

问:name可以写在原型吗? 答:不可以,因为每次创建对象的name都会覆盖之前原型的name,每次创建的Person对象都是空的,当执行p1.name时,在自己的对象上都找不到,然后去原型找,结果每个对象的name都是”李四“

问:原型里的this执行会有问题吗? 答:不会,this指向和函数放的位置没有关系,运行时才绑定,比如p1.eating(),this指向的就是p1对象

7.6补充:关于枚举Enumerable

Enumerable为false表示不可枚举,node打印是看不到该属性的,但浏览器可以,这是浏览器方便我们查看对象的属性,明显可以看到,age的颜色是暗色的,这也是一个区分,表示age和name的枚举属性是不同的

image-20220224105510835.png