与其它的语言相比,JavaScript老是显得不那么合群。好比:javascript
在被诟病和争论中,有人喊出JavaScript并不是“非面向对象”的语言,而是“基于对象”的语言。可是,对于如何定义“面向对象”和“基于对象”,基本上很难有人可以回答。html
这须要明白JavaScript语言的设计思想,才能更清楚究竟是否须要模拟类,以及为何须要模拟类。在早期人们习惯于其余语言的面向对象编程方式,而对JavaScript感到困惑,并尝试用贴近类的方式去编程。java
咱们先看看JavaScript对对象的定义:“语言和宿主的基础设施由对象来提供,而且 JavaScript 程序便是一系列互相通信的对象集合”。这里的意思根本不是表达弱化面向对象的意思,反而是表达对象对于语言的重要性。程序员
到底什么是面向对象?Objcet在英文中,是一切事物的总称,这和面向对象编程的抽象思惟有相通之处。中文翻译“对象”却没有这样的普适性。在不一样的编程语言中,设计者也利用各类不一样的语言特性来描述对象,最为成功的流派是使用“类”的方式来描述对象,这诞生了诸如C++、Java等流行的编程语言。编程
而JavaScript早年则选择了一个更为冷门的流派:原型。这是不合群的缘由之一。数组
然而不幸的是,由于一些公司政治缘由,JavaScript推出之时受管理层之命被要求模仿 Java,因此,JavaScript 创始人 Brendan Eich 在“原型运行时”的基础上引入了 new、this 等语言特性,使之“看起来更像 Java”。浏览器
所以,咱们至少须要明白一件事,咱们以前所熟知的“面向对象”的编程方式,实际上是“基于类”的面向对象,它并非面向对象的所有,确切地说,基于类只是面向对象编程的一个流派而已。而想要理解JavaScript对象,就必须清空咱们认识“基于类的面向对象”相关的概念,回到人类对对象的朴素认识和无关语言的基础理论,咱们就可以理解JavaScript面向对象设计的思路。app
“基于类”的编程提倡使用一个关注分类和类之间关系开发模型。在这类语言中,老是先有类,再从类去实例化一个对象。类与类之间又可能会造成继承、组合等关系。类又每每与语言的类型系统整合,造成必定编译时的能力。编程语言
与此相对,“基于原型”的编程看起来更为提倡程序员去关注一系列对象实例的行为,然后才去关心如何将这些对象,划分到最近的使用方式类似的原型对象,而不是将它们分红类。函数
基于原型和基于类都可以知足基本的复用和抽象需求,可是适用的场景不太相同。这就像专业人士可能喜欢在看到老虎的时候,喜欢用猫科豹属豹亚种来描述它,可是对一些不那么正式的场合,“大猫”可能更为接近直观的感觉一些。咱们的 JavaScript 并不是第一个使用原型的语言,在它以前,self、kevo 等语言已经开始使用原型来描述对象了。
抛开模拟Java的复杂语法设施(new、Function Object、函数的prototype属性等),原型系统能够说至关简单,用两条能够归纳:
这个模型在之前的各个历史版本中并无大的改变,在ES6提供了一些列内置函数,能够更直接地访问操做原型:
利用这三个方法,咱们能够彻底抛开类的思惟,利用原型来实现抽象和复用。例如:
// 这段代码建立了一个“猫”对象,又根据猫作了一些修改建立了虎,以后咱们彻底能够用 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 风格的类接口来操纵原型运行时,能够说很是别扭。
关于惟一标识性,若是分别定义两个结构和值如出一辙的对象,他们两个是不相等的(a1 === a2为false)
关于状态和行为,不一样语言会有不一样的描述,Java中称他们为“属性”和“方法”,在JavaScript中,状态和行为统一抽象为“属性”。
前三个特征是任何面向对象语言都具有的,而JavaScript对象独有的特点是:对象具备高度的动态性,赋予了使用者在运行时为对象添改状态和行为的能力。
例如,下面的例子展现了向一个对象添加属性,这样操做彻底OK:
let o = { a: 1 }; o.b = 2; console.log(o.a, o.b); //1 2
同时,JavaScript的属性被设计成比别的语言更加复杂的形式,它提供了数据属性(描述属性)和访问器属性两类属性描述符(能够理解为针对属性的属性,这两类属性描述符不能同时存在,只能选取一种)。
数据属性在很早的版本中实现,访问器属性在ES6新增。详情可见下方总结,简单来讲就是:数据属性能够规定某属性的值、是否可写、是否可枚举、是否能被修改配置(包含是否能删除);访问器属性则主要定义了访问时的行为和结果。
如今能够回答JavaScript是否须要模拟类这个问题了。其实,JavaScript本不须要模拟类,可是由于你们习惯于类的编程方式(以及一些其余缘由,总结在下方),ES6正式开始使用class关键字,new + function的怪异搭配能够彻底抛弃了。咱们推荐在任何场景都使用ES6的语法来定义类。但在这里须要说明,class关键字的使用,其本质仍是基于原型,class extends 只是语法糖,彻底不存在抛弃原型一说。
1.大部分面向对象语言都是基于类的流派,而基于原型的比较小众。
基于原型本是一个优秀的抽象对象的形式,可是“基于类”的面向对象已经先入为主成为大部分人的思惟,在缺少系统性学习的前提下,尝试用基于类的思想去理解并掌握基于原型的处理方式,只会让人更加怀疑和困惑。
其实,不止有人对基于原型有疑问,也有人对基于类表达过疑惑,只是对于大部分人来讲质疑一个如此成功的流派显得多余,慢慢地认为面向对象理所应当就是这样。
2.早期因为公司政治缘由,模仿java的一些语法和方式,不只怪异,更加深了人们的困惑。
JavaScript 对象并不是只有一种,好比在浏览器环境中咱们没法单纯依靠js代码实现 div 对象,只能靠 document.createElement 来建立,这说明了 JavaScript 的对象机制并不是简单的属性集合 + 原型。
JavaScript 中的对象能够分为如下几类,这也与 JavaScript 语言的组成有关:
在浏览器环境中,咱们知道全局对象是 window ,window 上又有不少属性,这里的属性一部分来自 JavaScript 语言,一部分来自浏览器环境。JavaScript 标准中规定了全局对象属性,来自浏览器宿主部分的能够理解为 W3C 的 HTML 标准(或非标准)的API,例如DOM、BOM。
固有对象是由标准规定,随着 JavaScript 运行时而自动建立的对象实例。这些对象在任何JavaScript代码执行以前就已经被建立出来了,他们一般扮演相似基础库的角色,例如 global 对象(浏览器环境中是 window 对象)、JSON、Math、Number等。
ECMA标准提供了一份并不全面的固有对象表 ECMA
可以经过语言自己的构造器建立的对象称做原生对象。在JavaScript标准中,提供了30多个构造器,以下:
几乎全部这些构造器的能力都是没法用纯JavaScript代码实现的,它们也没法用 class/extend 语法来继承。咱们能够认为,全部这些原生对象都是为了特定能力或者性能,设计出来的“特权对象”。
let name = "...", age = 20; let obj = { name: name, age: age, func: function () { ... } }; // ES5表示 let obj = { name, age, func() { ... } }; // ES6表示
使用方括号,属性名称能够用表达式表示,也能够用变量名。
let name = "abc", obj = {}; obj[name] = "123"; // obj :{ abc: "123" } obj["h" + "ello"] = "aa"; // obj:{ abc: "123", "hello": "aa" }
函数有 name 属性,返回函数名。对象内的方法也是函数,也有 name 属性。
const person = { sayName() { console.log('hello!'); }, }; person.sayName.name // "sayName"
对象内的每一个属性都有一个属性描述符,分为两类:数据属性(也叫描述属性)、访问器属性。二者不能一块儿用。
咱们经过 Object.getOwnPropertyDescriptor() 方法来获取某个对象的某个属性的描述符。
let obj = { a: "hello" }; const descriptor = Object.getOwnPropertyDescriptor(obj, 'a'); console.log(descriptor); { value: "hello" writable: true enumerable: true configurable: 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
咱们知道,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关键字表示原型对象时,只能用在对象的方法之中,用在其余地方都会报错。
遍历涉及到属性是否可枚举、是不是自身的属性、键是不是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(),另外五种遍历,遵循一样的次序规则:
一览表:
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及extends只是语法糖。详见 ECMAScript新语法、特性总结
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 对象与 Proxy 对象同样,也是 ES6 为了操做对象而提供的新的 API。
辅助说明示例:
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); } });
一共有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对象后执行给定函数
应用场景举例: