谈论JavaScript对象——我的总结

前言

疑惑、怀疑与思考

JavaScript究竟是面向对象仍是基于对象?

与其它的语言相比,JavaScript老是显得不那么合群。好比:javascript

  • 不一样于其它的面向对象语言,JavaScript一直没有类的概念(ES6以前),ES6的到来也并无改变它是基于原型的本质,这点是最让开发人员困惑的地方
  • _proto_ 和 prototype 傻傻分不清
  • 对象能够是由 new 关键字实例化,也能够直接由花括号定义
  • JavaScript对象能够自由添加属性,而其余的语言不行

在被诟病和争论中,有人喊出JavaScript并不是“非面向对象”的语言,而是“基于对象”的语言。可是,对于如何定义“面向对象”和“基于对象”,基本上很难有人可以回答。html

JavaScript究竟是否须要模拟类class?

这须要明白JavaScript语言的设计思想,才能更清楚究竟是否须要模拟类,以及为何须要模拟类。在早期人们习惯于其余语言的面向对象编程方式,而对JavaScript感到困惑,并尝试用贴近类的方式去编程。java

溯源与再思考

什么是面向对象?

咱们先看看JavaScript对对象的定义:“语言和宿主的基础设施由对象来提供,而且 JavaScript 程序便是一系列互相通信的对象集合”。这里的意思根本不是表达弱化面向对象的意思,反而是表达对象对于语言的重要性。程序员

到底什么是面向对象?Objcet在英文中,是一切事物的总称,这和面向对象编程的抽象思惟有相通之处。中文翻译“对象”却没有这样的普适性。在不一样的编程语言中,设计者也利用各类不一样的语言特性来描述对象,最为成功的流派是使用“类”的方式来描述对象,这诞生了诸如C++、Java等流行的编程语言。编程

而JavaScript早年则选择了一个更为冷门的流派:原型。这是不合群的缘由之一。数组

然而不幸的是,由于一些公司政治缘由,JavaScript推出之时受管理层之命被要求模仿 Java,因此,JavaScript 创始人 Brendan Eich 在“原型运行时”的基础上引入了 new、this 等语言特性,使之“看起来更像 Java”。浏览器

所以,咱们至少须要明白一件事,咱们以前所熟知的“面向对象”的编程方式,实际上是“基于类”的面向对象,它并非面向对象的所有,确切地说,基于类只是面向对象编程的一个流派而已。而想要理解JavaScript对象,就必须清空咱们认识“基于类的面向对象”相关的概念,回到人类对对象的朴素认识和无关语言的基础理论,咱们就可以理解JavaScript面向对象设计的思路。app

什么是原型?什么是类?

“基于类”的编程提倡使用一个关注分类和类之间关系开发模型。在这类语言中,老是先有类,再从类去实例化一个对象。类与类之间又可能会造成继承、组合等关系。类又每每与语言的类型系统整合,造成必定编译时的能力。编程语言

与此相对,“基于原型”的编程看起来更为提倡程序员去关注一系列对象实例的行为,然后才去关心如何将这些对象,划分到最近的使用方式类似的原型对象,而不是将它们分红类。函数

基于原型和基于类都可以知足基本的复用和抽象需求,可是适用的场景不太相同。这就像专业人士可能喜欢在看到老虎的时候,喜欢用猫科豹属豹亚种来描述它,可是对一些不那么正式的场合,“大猫”可能更为接近直观的感觉一些。咱们的 JavaScript 并不是第一个使用原型的语言,在它以前,self、kevo 等语言已经开始使用原型来描述对象了。

JavaScript的原型与对象

JavaScript原型:

抛开模拟Java的复杂语法设施(new、Function Object、函数的prototype属性等),原型系统能够说至关简单,用两条能够归纳:

  • 若是全部对象都有私有字段 [[prototype]],就是对象的原型;
  • 读一个属性,若是对象自己没有,则会继续访问对象的原型,直到原型为空或者找到为止。

这个模型在之前的各个历史版本中并无大的改变,在ES6提供了一些列内置函数,能够更直接地访问操做原型:

  • Object.create 根据指定的原型建立新对象,原型能够是 null;
  • Object.getPrototypeOf 得到一个对象的原型;
  • Object.setPrototypeOf 设置一个对象的原型

利用这三个方法,咱们能够彻底抛开类的思惟,利用原型来实现抽象和复用。例如:

// 这段代码建立了一个“猫”对象,又根据猫作了一些修改建立了虎,以后咱们彻底能够用 Object.create 来建立另外的猫和虎对象,咱们能够经过“原始猫对象”和“原始虎对象”来控制全部猫和虎的行为
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();

这段代码建立了一个“猫”对象,又根据猫作了一些修改建立了虎,以后咱们彻底能够用 Object.create 来建立另外的猫和虎对象,咱们能够经过“原始猫对象”和“原始虎对象”来控制全部猫和虎的行为。可是,在更早的版本中,程序员只能经过 Java 风格的类接口来操纵原型运行时,能够说很是别扭。

JavaScript对象的特征:

  • 对象具备惟一标识性:这个标识不是看变量名,而是内存地址
  • 对象有状态
  • 对象具备行为
  • 对象具备动态性

关于惟一标识性,若是分别定义两个结构和值如出一辙的对象,他们两个是不相等的(a1 === a2为false)

关于状态和行为,不一样语言会有不一样的描述,Java中称他们为“属性”和“方法”,在JavaScript中,状态和行为统一抽象为“属性”。

前三个特征是任何面向对象语言都具有的,而JavaScript对象独有的特点是:对象具备高度的动态性,赋予了使用者在运行时为对象添改状态和行为的能力。

例如,下面的例子展现了向一个对象添加属性,这样操做彻底OK:

let o = { a: 1 };
o.b = 2;
console.log(o.a, o.b); //1 2

同时,JavaScript的属性被设计成比别的语言更加复杂的形式,它提供了数据属性(描述属性)和访问器属性两类属性描述符(能够理解为针对属性的属性,这两类属性描述符不能同时存在,只能选取一种)

数据属性在很早的版本中实现,访问器属性在ES6新增。详情可见下方总结,简单来讲就是:数据属性能够规定某属性的值、是否可写、是否可枚举、是否能被修改配置(包含是否能删除);访问器属性则主要定义了访问时的行为和结果。

既然ES6有了class,咱们是否是能够抛弃原型了?

如今能够回答JavaScript是否须要模拟类这个问题了。其实,JavaScript本不须要模拟类,可是由于你们习惯于类的编程方式(以及一些其余缘由,总结在下方),ES6正式开始使用class关键字,new + function的怪异搭配能够彻底抛弃了。咱们推荐在任何场景都使用ES6的语法来定义类。但在这里须要说明,class关键字的使用,其本质仍是基于原型,class extends 只是语法糖,彻底不存在抛弃原型一说。

如今,咱们能够总结一下为何JavaScript对象让人困惑了:

1.大部分面向对象语言都是基于类的流派,而基于原型的比较小众。

  基于原型本是一个优秀的抽象对象的形式,可是“基于类”的面向对象已经先入为主成为大部分人的思惟,在缺少系统性学习的前提下,尝试用基于类的思想去理解并掌握基于原型的处理方式,只会让人更加怀疑和困惑。

  其实,不止有人对基于原型有疑问,也有人对基于类表达过疑惑,只是对于大部分人来讲质疑一个如此成功的流派显得多余,慢慢地认为面向对象理所应当就是这样。

2.早期因为公司政治缘由,模仿java的一些语法和方式,不只怪异,更加深了人们的困惑。

全面认识JavaScript对象

JavaScript对象分类

JavaScript 对象并不是只有一种,好比在浏览器环境中咱们没法单纯依靠js代码实现 div 对象,只能靠 document.createElement 来建立,这说明了 JavaScript 的对象机制并不是简单的属性集合 + 原型。

JavaScript 中的对象能够分为如下几类,这也与 JavaScript 语言的组成有关:

  • 宿主对象(host Objects):由 JavaScript 宿主环境提供的对象,它们的行为彻底由宿主环境决定。
  • 内置对象(Built-in Objects):由 JavaScript 语言提供的对象。
    • 固有对象(Intrinsic Objects):由标准规定,随着 JavaScript 运行时自动建立的对象
    • 原生对象(Native Objects):能够由用户经过内置构造器建立的对象
    • 普通对象(Ordinary Objects):由 {} 语句、Object 构造器或者 class 关键字定义的对象,它可以被原型继承

宿主对象

在浏览器环境中,咱们知道全局对象是 window ,window 上又有不少属性,这里的属性一部分来自 JavaScript 语言,一部分来自浏览器环境。JavaScript 标准中规定了全局对象属性,来自浏览器宿主部分的能够理解为 W3C 的 HTML 标准(或非标准)的API,例如DOM、BOM。

固有对象

固有对象是由标准规定,随着 JavaScript 运行时而自动建立的对象实例。这些对象在任何JavaScript代码执行以前就已经被建立出来了,他们一般扮演相似基础库的角色,例如 global 对象(浏览器环境中是 window 对象)、JSON、Math、Number等。

ECMA标准提供了一份并不全面的固有对象表 ECMA

原生对象

可以经过语言自己的构造器建立的对象称做原生对象。在JavaScript标准中,提供了30多个构造器,以下:

几乎全部这些构造器的能力都是没法用纯JavaScript代码实现的,它们也没法用 class/extend 语法来继承。咱们能够认为,全部这些原生对象都是为了特定能力或者性能,设计出来的“特权对象”。

ES6以来的扩展(普通对象)

1.简洁的表示方法

let name = "...", age = 20;
let obj = { name: name, age: age, func: function () { ... } }; // ES5表示
let obj = { name, age, func() { ... } }; // ES6表示

2.属性名表达式

使用方括号,属性名称能够用表达式表示,也能够用变量名。

let name = "abc", obj = {};
obj[name] = "123";    // obj :{ abc: "123" }
obj["h" + "ello"] = "aa";  // obj:{ abc: "123", "hello": "aa" }

3.方法的 name 属性

函数有 name 属性,返回函数名。对象内的方法也是函数,也有 name 属性。

const person = {
  sayName() {
    console.log('hello!');
  },
};

person.sayName.name   // "sayName"

4.属性描述符

对象内的每一个属性都有一个属性描述符,分为两类:数据属性(也叫描述属性)、访问器属性。二者不能一块儿用。

数据属性

咱们经过 Object.getOwnPropertyDescriptor() 方法来获取某个对象的某个属性的描述符。

let obj = { a: "hello" };
const descriptor = Object.getOwnPropertyDescriptor(obj, 'a');
console.log(descriptor);
{
    value: "hello"
    writable: true
    enumerable: true
    configurable: true
}

能够看到,属性描述符(数据属性)由四个值组成:

  • value  该属性的值
  • writable  属性是否可写,若是为false就成为了只读属性。默认为true
  • enumerable  属性是否可枚举,默认为true
  • configurable  属性是否可配置。为true表示此属性的属性描述符能够被修改,属性也能够被删除,false则不可删除不可修改。默认为true

如何设置属性描述符呢?经过 Object.defineProperty()/Object.defineProperties() 方法:

Object.defineProperty(obj, prop, descriptor) 在对象上定义/修改一个属性,并返回该对象
例:
    let obj = {};
    let des = Object.defineProperty(obj, "a", {value: 123, writable: false});
    obj.a // 123
    console.log(des)  // {value: 123, writable: false,enumerable: true,configurable: true} // enumerable和configurable默认为true

Object.defineProperties(obj, props) 在对象上定义/修改多个属性,并返回该对象
例:
    let obj = {};
    let des = Object.defineProperty(obj, {
        a: {value: 123, writable: true},
        b: {value: 456, writable: true},
    });

访问器属性

访问器属性一样有四个:value、writable、get、set

其中最有用的是 getter/setter 函数,当经过对象取值/赋值时,会触发对应的函数。例如:

let obj = {
    _name: "hello",
    get name() { return this._name },
    set name(value) { this._name = value },
};
obj.name // "hello"
obj.name = "haha";
obj.name // "haha"

须要注意,当定义了 setter/getter 函数后,name属性真实存在,但在取值/赋值函数内部没法获取到同名属性 name ,也就是说,不能将 getter/setter 函数名和属性名相同,这点与 Proxy 不一样。在上面的例子中,当访问 name 属性时实际访问的是 _name 属性。

另外,上面提到,不能同时使用两种属性描述符,不然会报错,如:

let obj = {};
Object.defineProperty(obj, "a", {
  get : function(){
    return bValue;
  },
  set : function(newValue){
    bValue = newValue;
  },
  writable: true,  // 该属性只能用于数据描述符
});
// throws a TypeError: value appears only in data descriptors, get appears only in accessor descriptors

5.super关键字

咱们知道,this 关键字老是指向函数所在的当前对象,ES6新增了一个相似的关键字 super,指向当前对象的原型对象。

const proto = {
  foo: 'hello'
};

const obj = {
  foo: 'world',
  find() {
    return super.foo;
  }
};

Object.setPrototypeOf(obj, proto);
obj.find() // "hello" obj对象经过super关键字引用了原型对象的foo属性

注意:当super关键字表示原型对象时,只能用在对象的方法之中,用在其余地方都会报错。

6.遍历

遍历涉及到属性是否可枚举、是不是自身的属性、键是不是Symbol等问题。

如下四个操做没法遍历对象的不可枚举属性:

for ... in循环:只遍历对象自身的和继承的可枚举属性(不包含Symbol属性)
Object.keys():返回自身可枚举属性的键(不包含Symbol属性)
JSON.stringify():只序列化自身的可枚举属性(不包含Symbol属性)
Object.assign():只拷贝自身的可枚举属性(包含Symbol属性)

另外还有三种操做能够遍历不可枚举属性:

Object.getOwnPropertyNames():返回自身的全部属性(不含Symbol属性)
Object.getOwnPropertySymbols():返回自身全部的Symbol属性
Reflect.ownKeys():返回自身的全部属性(包含Symbol)

以上7种方法,除了JSON.stringify()和assign(),另外五种遍历,遵循一样的次序规则:

  • 首先遍历全部数值键,按照数值升序排列。
  • 其次遍历全部字符串键,按照加入时间升序排列。
  • 最后遍历全部 Symbol 键,按照加入时间升序排列。

7.扩展方法

一览表:

Object.is()        判断两个值是否相同,与===基本一致,能够说是对其的完善
Object.assign()      复制、合并对象,返回新对象。(1.只会复制可枚举属性2.属性都是浅拷贝)
Object.getOwnPropertyDescriptor()    返回某对象的某个自有属性的描述属性
Object.getPrototypeOf()      返回指定对象的原型对象
Object.setPrototypeOf()      设置指定对象的原型对象
Object.keys()          返回一个对象的全部可枚举属性名称
Object.values()         返回一个对象的全部可枚举属性的值
Object.entries()        返回一个对象的全部可枚举属性的键值对数组(能够看作是上面两个方法的结合)
Object.fromEntries()    entries()的逆操做,用于将一个键值对数组还原为对象,所以特别适合将Map结构转为对象

详情见这篇博客 JavaScript字符串、数组、对象方法总结

Class及继承

class关键字的使用正是迎合了“模拟类”的需求,但不改变其基于原型的本质,class及extends只是语法糖。详见 ECMAScript新语法、特性总结

Proxy

Proxy使得咱们拥有强大的对象操做能力。Proxy英文意思为“代理”,表示它能够代理某些操做。Proxy 在目标对象前架设一层拦截,外界对该对象的访问,都必须先通过这层拦截,它提供了一种机制,能够对外界的访问进行过滤和改写。这等同于在语言层面作出修改,属于一种“元编程”(meta programmin),即对编程语言进行编程。

const proxy = new Proxy(target, handler);  // 生成 Proxy 实例

栗子(拦截读取操做):

var person = {
  name: "张三"
};
 
var proxy = new Proxy(person, {
  get: function(target, propKey, receiver) {
    if (property in target) {
      return target[property];
    } else {
      throw new ReferenceError("不存在的");
    }
  },
  set: function(target, propKey, value, receiver) {
    console.log("setter", target, propKey, value);
    retrun target[propKey] = value;
  }
});
 
proxy.name // "张三"
proxy.age // 抛出一个错误
proxy.name = "李四"
proxy.name // "李四"

Proxy支持的拦截操做一览表,一共13种:

get(target, propKey, receiver):拦截对象属性的读取
set(target, propKey, value, receiver):拦截对象属性的设置,返回一个布尔值。
has(target, propKey):拦截propKey in proxy的操做,返回一个布尔值。
deleteProperty(target, propKey):拦截delete proxy[propKey]的操做,返回一个布尔值。
ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象全部自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。若是目标对象是函数,那么还有两种额外操做能够拦截。
apply(target, object, args):拦截 Proxy 实例做为函数调用的操做,好比proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
construct(target, args):拦截 Proxy 实例做为构造函数调用的操做(new命令),好比new proxy(...args)。

Reflect

Reflect 对象与 Proxy 对象同样,也是 ES6 为了操做对象而提供的新的 API。

Reflect 的做用:

  • 将 Object 语言内部的方法拿出来放到 Reflect 对象上,即从 Reflect 对象上拿 Ojbect 对象内部方法。现阶段这些方法同时存在 Object 和 Reflect 上,将来新的方法将只部署在 Reflect 上。
  • 原 Object 方法报错的状况,在 Reflect 上会返回 false。使得代码运行更稳定。
  • 让 Object 操做都变成函数行为。原 Object 操做有一些是命令式,好比 in 和 delete,Reflect 用 has() 和 deleteProperty() 替代。
  • Reflect 对象上的方法与 Proxy 行为一一对应,只要是 Proxy 上的方法就会对应地出如今Reflect上。这可使得两种对象相互配合完成默认的行为,做为修改行为的基础。即,无论 Proxy 如何修改默认行为,你总能够在 Reflect 上获取默认行为。

辅助说明示例:

2.某些Object方法调用可能会抛出异常,在Reflect上会返回false
// 老写法
try {
  Object.defineProperty(target, property, attributes);
  // success
} catch (e) {
  // failure
}

// 新写法
if (Reflect.defineProperty(target, property, attributes)) {
  // success
} else {
  // failure
}
3.将命令式操做改为函数行为
// 老写法
'assign' in Object // true

// 新写法
Reflect.has(Object, 'assign') // true
4.与Proxy配合,获取对象的一些默认行为
var loggedObj = new Proxy(obj, {
  get(target, name) {
    console.log('get', target, name);
    return Reflect.get(target, name);
  },
  deleteProperty(target, name) {
    console.log('delete' + name);
    return Reflect.deleteProperty(target, name);
  },
  has(target, name) {
    console.log('has' + name);
    return Reflect.has(target, name);
  }
});

Reflect对象方法

一共有13个静态方法:

Reflect.get(target, name, receiver)           查找并返回target对象的name属性,若是没有该属性,则返回undefined。
Reflect.set(target, name, value, receiver)    设置target对象的name属性等于value
Reflect.defineProperty(target, name, desc)    基本等同于Object.defineProperty,用来为对象定义属性。将来,后者会被逐渐废除,请从如今开始就使用Reflect.defineProperty代替它。
Reflect.deleteProperty(target, name)          等同于delete obj[name],删除对象的属性
Reflect.has(target, name)                     对应name in obj里面的in运算符,判断属性是否存在于对象中
Reflect.construct(target, args)               等同于new target(...args),这提供了一种不使用new,来调用构造函数的方法。
Reflect.ownKeys(target)                       用于返回对象的全部属性,基本等同于Object.getOwnPropertyNames与Object.getOwnPropertySymbols之和。
Reflect.isExtensible(target)                  对应Object.isExtensible,返回一个布尔值,表示当前对象是否可扩展。
Reflect.preventExtensions(target)             对应Object.preventExtensions方法,用于让一个对象变为不可扩展。它返回一个布尔值,表示是否操做成功。
Reflect.getOwnPropertyDescriptor(target, name) 基本等同于Object.getOwnPropertyDescriptor,用于获得指定属性的描述对象,未来会替代掉后者。
Reflect.getPrototypeOf(target)                用于读取对象的__proto__属性,对应Object.getPrototypeOf(obj)。
Reflect.setPrototypeOf(target, prototype)     用于设置目标对象的原型(prototype),对应Object.setPrototypeOf(obj, newProto)方法,返回布尔值,表示是否设置成功。
Reflect.apply(target, thisArg, args)          用于绑定this对象后执行给定函数

应用场景举例:

  • 能够实现观察者模式,即观察数据的变化,一旦发生变化,自动执行对应的函数。
相关文章
相关标签/搜索