对JavaScript类的理解与使用

72 阅读10分钟

前言

  1. JavaScript为什么既有原型,也有类思想
  2. 理清JavaScript中原型、类思想的区别和关联
  3. 了解object原型中的实用方法

一、什么是对象

在英文中,对象表示一切有形和无形物体的总称

对象具有唯一标识性、状态和行为

  1. 唯一标识性:指的是即使完全相同的对象也不一样,代码的设计表现是因为他们的内存地址不同
  2. 状态、行为:在java等语言中用属性表示状态,方法表示行为。
  3. 而JavaScript中将状态、行为统一抽象为了属性。代码的设计表现是方法、或者属性最终都基于类

属性又被设计为数据属性和访问器属性两种

  • 数据属性,特征有以下四种
    1. value 属性的值
    2. writeable 是否可以被赋值
    3. enumerable 是否可以被枚举
    4. configurable 是否可以被删除,以及writeable、enumerable和configurable是否可以被重置
  • 访问器属性
    1. getter 函数,用于属性值的获取
    2. setter 函数,用于属性的设置
    3. enumerable 是否可被枚举
    4. configurable 是否可以被删除,以及writeable、enumerable和configurable是否可以被重置
  • 数据属性和访问器属性的定义和读取分别可以用Object.defineProperty、Object.getOwnPropertyDescriptor()两个方法

综上,对象其实是一个属性的索引结构

  • 索引结构是一类常见的数据结构,我们可以把它理解为一个能够以比较快的速度用key来查找value的字典
  • 属性以字符串或者Symbol为key,以数据属性特征值或者访问器属性特征值为value

对象的分类

  1. 宿主对象:由宿主如浏览器提供的document、window等
  2. 内置对象:
    1. 固有对象:由标准规定,随着JavaScript运行时创建而自动创建
      • 由JavaScript语言提供的对象,固有对象是由标准规定,随着JavaScript运行时而创建而自动创建的对象实例,通常扮演类似基础库的角色
      • 具有150+个固有对象,可以参考以下链接www.ecma-international.org/ecma-262/9.…
    2. 原生对象: 可以由用户通过Array、RegExpd等内置构造器或者特殊语法创建的对象
      • JavaScript的标准提供了30多个构造器,所以原生对象按照构造器分为以下种类

      • %E6%9E%84%E9%80%A0%E5%99%A8.png

    3. 普通对象:由{}语法、Object构造器或者class关键字定义类创建的对象,它能够被原型继承

二、原型思想构建对象

原型是顺应自然思维的产物。类似于”照猫画虎“,猫就是虎的原型

原型复制操作的两种思路

1. 一个不是真的去复制原型对象,而是去持有原型的引用
2. 另一个是切实的复制对象,从此两个对象再无关联
  • 而JavaScript选的是第一种,复制对象时由__proto__去持有原型的引用

抛开JavaScript用于模拟Java类的复杂语法设置(如new/Function Object/函数的prototype等),原型系统可以总结为:

1. 如果所有对象都有私有字段prototype,就是对象的原型
2. 读一个属性,如果对象本身没有,则会继续访问对象的原型,直到原型为空或者找到为止

利用原型来实现抽象和复用,用下面的代码展示用原型来抽象猫和虎的例子

var cat = {
    say(){
        console.log("meow~");
    },
    jump(){
        console.log("jump");
    }
}

var tiger = Object.create(cat,  {
    say:{
        writable:true,
        configurable:true,
        enumerable:true,
        value:function(){
            console.log("roar!");
        }
    }
})

var anotherCat = Object.create(cat);
anotherCat.say();
var anotherTiger = Object.create(tiger);
anotherTiger.say();
  • 通过这种方式就可以通过原始猫、原始虎控制所有的猫和虎

三、类思想构建对象

  1. 类即是对一类事物属性和行为的抽象总称
  2. 也是顺着人类思维模式产生的一种抽象,如老虎属于猫科豹属亚种
    class Animal { 
       constructor(name) {
         this.name = name;
       }
       
       speak() {
         console.log(this.name + ' makes a noise.');
       }
     }
     
     class Dog extends Animal {
       constructor(name) {
         super(name); 
       }
     
       speak() {
         console.log(this.name + ' barks.');
       }
     }
     
     let d = new Dog('Mitzie');
     d.speak(); // Mitzie barks.
    

四、原型和类的关联

为什么JavaScript中既有原型思想,又有类的思想

  1. 首先JavaScript的创始者选择原型思想,因为原型系统与高动态性语言更加契合,而且多数基于原型的语言提倡运行时的原型修改
  2. 但是因为java等语言的大火,公司高层要求JavaScript模仿java语言,所以JavaScript中又有了类的思想

类思想和原型思想的区别

  1. 基于类的编程中,关注分类与类之间关系的开发模型。总是先有类,再从类去实例化一个对象。同时类与类之间再行程组合继承关系
  2. 基于原型的编程,关注一系列对象实例的行为。而后才去关心如何将这些对象,划分到最近的使用方式相似的原型对象

早起版本“类”的定义是一个私有属性,来表示他们的类。类的概率是比较弱的

  • ES5之前用[[class]]来表示他的类,ES5开始用Symbol.toStringTag代替
  • 而且[[class]]或者Symbol.toStringTag都只能通过Object.prototype.toString()来访问到,返回[Object type],其中type表示它所从属的类

通过构造器来模拟类


function c1(){
    this.p1 = 1;
    this.p2 = function(){
        console.log(this.p1);
    }
} 
var o1 = new c1;
o1.p2();


function c2(){
}
c2.prototype.p1 = 1;
c2.prototype.p2 = function(){
    console.log(this.p1);
}

var o2 = new c2;
o2.p2();
  • 第一种是直接在构造器中给this添加属性
  • 第二种是修改构造器的prototype属性指向的对象,它是从这个构造器构造出来的所有对象的原型

实际上JavaScript中,类的写法也是由原型运行时来承载的,逻辑上JavaScript认为每个类是有共同原型的一组对象,类的定义方法和属性则会被写在原型对象上

如果用类的思想设计代码时,应该尽量使用class来声明类

  1. 使用class的语法来定义类,可以令function回归原本的函数语义,这样更明确的区分了定义函数和定义类两种意图
  2. 比起早期的原型模拟方式,即使使用extends关键字自动设置了constructor,并且会自动调用父类的构造函数,只能更能有效减少语法错误

五、对象的遍历方法

Object.keys()

功能:返回自身可枚举属性的键值数组

  1. 处理对象时,键值为数值时按照数字的大小自动排序,为非数字时按照插入时间排序
let arr1 = { 100: 'a', 2: 'b', 7: 'c', [Symbol()]:123 };
// console.log(Object.keys(arr1)); // console: ['2', '7', '100']
  1. ES6中参数为非对象,会强制转换为对象,ES5会出现类型错误
Object.keys("foo");
// TypeError: "foo" is not an object (ES5 code)

Object.keys("foo");
// ["0", "1", "2"]  (ES6
  1. 对比for in不会遍历出原型上的属性

Object.values()

返回自身可枚举属性值的数组

  1. 可枚举属性顺序和keys()一样
let arr1 = { 100: 'a', 2: 'b', 7: 'c', [Symbol()]:123 };
console.log(Object.values(arr1)); // console: ['b', 'c', 'a']
  1. ES6会将非对象参数临时转换为对象
console.log(Object.values("foo"));
// ["f", "o", "o"]

Object.entries()

返回自身可枚举属性值的键值对数组

  1. 可枚举属性的排列顺序和keys()一样
const anObj = { 100: 'a', 2: 'b', 7: 'c' };
console.log(Object.entries(anObj)); // [ ['2', 'b'], ['7', 'c'], ['100', 'a'] ]
  1. 不是对象时也会强制转换
console.log(Object.entries('foo')); // [ ['0', 'f'], ['1', 'o'], ['2', 'o'] ]
  1. 同keys()/values()一样不会遍历出原型上的属性
  2. 同时Object.keys/values/entries均不能遍历出名为symbol的属性
    • 如果需要的话可以使用Reflect.ownKeys(obj)返回一个数组,包含对象自身所有属性,无论键名是否是symbol,也不管是否可枚举

object.keys/values/entries的低版本兼容

主要是通过for in来实现的

  1. Object.keys低版本兼容
If(!Object.keys) 
Object.keys = function(obj){
    If(obj !== Object(obj)) throw new TypeError('Object.keys called on a non-object')
    var result = []
    for(var prop in obj){
        if(Object.hasOwnProperty.call(obj,prop)) result.push(prop)
    }
    return result
}
  1. Object.values的兼容类似于keys
  2. Object.entries低版本兼容
If(!Object.entries)
Object.entries = function(obj) {
    If(obj !== Object(obj)) throw new TypeError('Object.keys called on a non-object')
    var result = []
    for(var prop in obj){
        if(Object.hasOwnProperty.call(obj,prop)) result.push([prop, obj[prop]])
    }
    return result
}

object.keys/values/entries底层实现方式

  • 属性不超过128个,使用Search Cache,当属性是较为连续的数字时,使用数组,此种方式最快。其它情况使用哈希表,并且数字和字符串的哈希不一样。
  • 具体内容较多且较为复杂,详细请参考从chrome源码看JS Object的实现

Object.getOwnPropertyNames()

返回自身可枚举和不可枚举属性名称的字符串数组

  1. 可枚举属性顺序和keys()一致,不可枚举属性顺序未定义
  2. 同样ES6会强制转化非对象属性

Object.getOwnPropertyDescriptions()

返回自身可枚举、不可枚举属性描述符对象

  1. 可枚举属性顺序和keys()一致,不可枚举属性顺序未定义
  2. 对比Object.getOwnPropertyNames()会将名为symbol的参数也遍历出来
var parent = Object.create(Object.prototype, {
    a: {
        value: 1,
        writable: true,
        enumerable: true,
        configurable: true            
    }
});
var child = Object.create(parent, {
    b: {
        value: 2,
        writable: true,
        enumerable: true,
        configurable: true
    },
    d: {
        value: () => {},
        writable: true,
        enumerable: true,
        configurable: true
    },
    c: {
        value: 3,
        writable: true,
        enumerable: false,
        configurable: true
    },
    [Symbol()]: {
        value: 4,
        writable: true,
        enumerable: true,
        configurable: true
    }
});
for(let i in child){
    console.log(i)
}
console.log(Object.keys(child))

console.log(Object.getOwnPropertyNames(child))

console.log(Object.getOwnPropertyDescriptors(child))

console.log(Reflect.ownKeys(child))

console.log(Object.getOwnPropertyDescriptors('123'))

六、对象的不可扩展、密封和冻结

Object.preventExtensions()

  1. 不可扩展之后不能新增属性
  2. 原有属性仍可以删除
  3. 可以将属性添加在对象原型上
  4. 对象的__proto__不能变更
  5. 通过Object.isExtensiable()判断是否可扩展

Object.seal()

  1. 在preventExtensions()不能新增的基础上,新特点是不能删除属性
  2. 可以修改属性值,但已有属性会变得不可配置
  3. 不会影响原型链上继承的属性,但__proto__的值不能修改
  4. 通过Object.isSeal()判断是否密封

Object.freeze()

  1. 在seal()的基础之上,新特性所有的属性无法被修改
  2. 如果一个属性的值是个独享,则这个属性是可以修改的
  3. 不会影响原型链上集成度属性,但__proto__的值也不能修改
  4. 可以通过Object.isFrozen()判断是否冻结
let obj = {
    prop: function(){},
    foo: 'bar',
    ba: {
        a:333
    }
}
let o = Object.freeze(obj)
console.log(o === obj) //true
obj.ba.a = 222
console.log(Object.getOwnPropertyDescriptors(obj))
obj.foo = 'quux' //无反应
obj.quaxxor = '12312' //无反应
delete obj.prop //无反应

// Object.setPrototypeOf(obj, {x:20}) //报错
// obj.__proto__ = {x:20}  //报错
console.log(Object.getOwnPropertyDescriptors(obj))

Object.freeze()和const之间的区别

  • const和let唯一的区别是变量的地址值不可变,const申明的对象writeable、configurable属性依然是true,所以const声明的复合类型数据仍然可变
  • 所以如果想让对象或者数组无法变更需要freeze
  • 但同时freeze仍无法冻结对象下的对象属性,需要使用逐层遍历(递归)冻结,如下所示
const user = {
    first_name: 'bolaji',
    last_name: 'ayodeji',
    contact: {
        email: 'hi@bolajiayodeji.com',
        telephone: 08109445504,
    }
}
console.log(Object.getOwnPropertyDescriptors(user))

let constantize = (obj) => {
  Object.freeze(obj);
  Object.keys(obj).forEach( (key, i) => {
    if ( typeof obj[key] === 'object' ) {
      constantize( obj[key] );
    }
  })
}

constantize(user)

preventExtensible()、seal()、freeze()的使用场景

  1. 这三个方法可以用来实现不可变数据。而不可变数据正是函数式编程最重要的原则之一。
    • 另外不可变对象对于react同样重要,因为React内是通过state/props比较(===)去判断是否render 的,三个等号的比较就要求新的 state 必须是新的引用。
    • 在 facebook 的实现的不可变对象库 Immutable 中,Map 是相当于 JavaScript 中的对象。这里可以通过preventExtensible()来实现
    function ImmutableMap(object) {
      Object.keys(object).forEach((key) => {
        Object.defineProperty(this, key, {
          value: object[key],
          writable: false,
          enumerable: true,
          configurable: false,
        });
      });
      Object.preventExtensions(this);
    }
    
    • 同时在另一个不可变对象库 seamless-immutable 中使用了 JavaScript Object 提供的方法 freeze