转载烦请注明原文链接:

https://github.com/Xing-Chuan/blog/blob/master/JavaScript/JavaScript之Object拆解.md

最近把研究 Object 的体会总结了一下, 有 Object 相关联的属性、方法和 ES6 后新 Api .

属性类型

JavaScript 中有两种数据类型: 数据属性和访问器属性

数据属性

数据属性有以下几个描述行为的属性:

  • Configurable 描述这个属性是否可被 delete 删除, 默认为 true
  • Enumerable 描述这个属性是否可被枚举, 默认为 true
  • writable 描述这个属性是否可被修改, 默认为 true
  • value 描述这个属性的值

如果想要修改这些系统默认属性, 可以通过 ES5 的方法 Object.defineProperty(obj, property, option).

注意:

  • 可以多次调用 Object.defineProperty() 修改属性, 但将 Configurable 设置为 false 是不可逆的.
  • 调用 Object.defineProperty() 修改属性方法时, 如果不指定这些描述属性, 则默认值都会设置为 false.

访问器属性

访问器有以下几个属性:

  • Configurable 描述这个属性是否可被 delete 删除, 默认为 true
  • Enumerable 描述这个属性是否可被枚举, 默认为 true
  • get 在读取属性时调用的函数, 默认为 undefined
  • set 在设置属性时调用的函数, 默认为 undefined
let book = { _year: 2004, edition: 1 };

/*
* 第一个参数: 对象
* 第二个参数: 对象的属性
* 第三个参数: 需要设置的描述属性
*/ Object.defineProperty(book, 'year', {
get: function() { return this._year; },
set: function(newValue) {
if (newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
}
}); book.year = 2009;
console.log(book.edition); // 6

注意:

  • get 和 set 必须同时设置, 否则会出现不能读或不能写的情况

如果要一次修改多个参数的描述属性, 可以使用 Object.defineProperties()

let book = {};

Object.defineProperties(book, {
_year: {
value: 2004
},
edition: {
value: 1
}
});

构造函数

构造函数也是普通的函数, 如果没有通过 new 操作符生成新的实例对象, 那构造函数就是一个普通的函数.

为了区分构造函数于普通函数, 我们通常用大驼峰命名法给构造函数命名.

String、Array 等几乎所有对象都是 Object 的实例, Object 就是一个构造函数.

new Array() instanceof Object // true
new String() instanceof Object // true
new Function() instanceof Object // true
function Info() {
this.name = 'XingChuan';
this.age = '10';
} let info1 = new Info();
let info2 = new Info();
console.log(info1.name); // XingChuan
console.log(info1.age); // 10
console.log(info2.name); // XingChuan
console.log(info2.age); // 10

原型对象

既然谈到构造函数就要谈到原型了, 每个函数都有一个原型(prototype)属性, 原型存在的意义就是, 原型上的属性和方法由所有构造函数的实例对象所共享, 如果修改了原型对象上的方法, 那所有实例都会实时更改.

我们平时使用 Array 和 String 的方法, 都是在其原型对象上, 所有我们创建的所有数组和字符串都可以享有这些方法.

原型链

在 chrome, firefox, safari 中, 有一个浏览器私有属性 __proto__ 指向构造函数的 prototype, __proto__ 并不是官方属性, 为了兼容性考虑, 开发中最好不要使用 __proto__.

下面我们来具象化一下原型链的构成:

函数的原型链

构造函数也是普通的函数, 只是我们拿来生成实例, 所以才有这个称谓, 而所有的函数都是 Function 的实例.

function info() {

}
// 原型链
info.__proto__ => Function.prototype => Object.prototype

所有的函数都是 Function 的实例, 而 Function 是 Object 的实例, 所以有了这条原型链.

内置对象的原型链

很有意思的是, Object、Array、String、Function 都是函数, 所以他们都是 Function 的实例, Function 比较特殊, 它也是自身的实例.

Math 是个例外, 它并不是一个函数, 而是一个对象.

console.log(Array instanceof Function) // true
console.log(String instanceof Function) // true
console.log(Object instanceof Function) // true
console.log(Function instanceof Function) // true
console.log(Math instanceof Function) // false
console.log(Math.__proto__ === Object.prototype); // true
// 原型链
Array.__proto__ => Function.prototype => Object.protytype

实例对象的原型链

function Info() {
}
Info.prototype.name = 'Xingchuan';
Info.prototype.age = 10;
Info.prototype.showName = function() {
console.log(this.name);
};
let info1 = new Info();
info1.showName() // XingChuan
let info2 = new Info();
info2.name = 'test';
info2.showName() // test
// 原型链
info1.__proto__ => Info.prototype => Object.prototype

实例对象的 __proto__ 会指向构造函数的 prototype, 所有的原型对象都是 Object.prototype 的实例, 所以构成了这一条原型链.

注意:

  • 原型链的访问是有就近原则的, 如果像上面实例中已经有 name 属性, 则不会继续访问 prototype 上的 name 属性, 如果实例中没有这个属性, 则会按照原型链一直找下去, 如果没有就是 undefined.

元素也是 Object 的实例

console.dir(document.getElementsByTagName('span')[0]);
// span元素 => HTMLSpanElement => HTMLElement => Element => Node => EventTarget => Object.prototype
console.dir(document.getElementsByTagName('span')[0] instanceof Object);
// true

元素的原型链很长, 不过可以看到元素也是 Object 的实例.

小结

  • 原型链有就近原则, 如果在实例上访问到某属性, 就不会继续原型链寻找该属性了
  • 原型是可以重新指定的
  • prototype 的 constructor 指向 构造函数
  • 实例的 __proto__ 指向构造函数的 prototype
  • 所有函数的 __proto__ 指向 Function.prototype
  • 函数的 prototype 都是基于 Object.prototype
  • Function 也是自己的实例
  • 一切起源于 Object.prototype
  • 每个原型都有 constructor 属性, 指向原型所属的函数, 用字面量对象改为原型的引用, 会丢失 constructor 这个属性

来张示意图结尾( 侵删 ):

this 的指向

<JavaScript忍者禁术> 一书中对 this 指向的问题有比较详细的描写, 推荐看看.

在讨论 this 指向的问题之前, 先要明确一下, 在严格模式下, 未指定环境对象而调用函数,则 this 值不会转型为 window.

除非明确把函数添加到某个对象或者调用 apply()或 call(),否则 this 值将是 undefined.

  • 普通函数中的 this
let x = 1;
function show() {
console.log(this.x);
}
show(); // 1

普通函数中的 this 指向 window

  • 构造函数中的 this
function Info(){
  this.x = 1;
}
let info1 = new Info();
console.log(info1.x); // 1

构造函数中的 this 指向 new 出来的实例对象, new 这个操作符会改变 this 的指向.

  • 对象方法中的 this
let x = 2;
let obj = {
x: 1,
y: function() {
console.log(this.x);
}
};
obj.y(); // 1 对象方法中的 this 指向调用它的对象.
  • call、apply、bind(ES5) 中的 this

call、apply、bind(ES5) 的作用就是改变 this 的指向, this 会指向第一个参数.

如果 call、apply、bind(ES5) 调用方法时没有传参, 默认 this 指向 window.

bind 只会改变 this 的指向, 并不会执行方法, call 和 apply 则会改变指向时也执行方法.

  • 事件函数中的 this

事件函数中的 this 指向绑定事件的元素.

  • 箭头函数中的 this

箭头函数是 ES6 中新增的方法, 不同于其他情况中的 this 在调用时才决定指向, 箭头函数 this 指向定义时的外围, 在不确认指向的情况下, 请慎用.

ECMAScript 6 入门对箭头函数的使用限制做了说明:
(1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
(2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用Rest参数代替。
(4)不可以使用yield命令,因此箭头函数不能用作Generator函数。

数据类型判断

在开发中, 我们可以用 typeof 来判断类型, 但有很大的局限性.

typeof 1
// "number"
typeof '1'
// "string"
typeof true
// "boolean"
typeof undefined
// "undefined"
typeof null
// "object"
typeof (function(){})
// "function"
typeof []
// "object"
typeof {}
// "object"

typeof 只对一些简单数据类型有效, 为了可以判断各种内置对象, 我们需要采取一些 手段 , 使用 Object 原型上的 toString 方法.

Object.prototype.toString.call(1);
// "[object Number]"
Object.prototype.toString.call('1');
// "[object String]"
Object.prototype.toString.call(true);
// "[object Boolean]"
Object.prototype.toString.call(function() {});
// "[object Function]"
Object.prototype.toString.call(null);
// "[object Null]"
Object.prototype.toString.call(undefined);
// "[object Undefined]"
Object.prototype.toString.call(new Date());
// "[object Date]"
Object.prototype.toString.call(Math);
// "[object Math]"
Object.prototype.toString.call([]);
// "[object Array]"
Object.prototype.toString.call({});
// "[object Object]"

可以全部搞定了.

ES6 之后的新特性

对象属性的简洁写法

简洁的写法可以减少代码量也可以更加优雅, 但代码是给计算机看的, 同时也是给人看的, 容易发生歧义的地方一定要注意.

  • 属性的简写

如果属性名与属性值相同, 可以忽略不写.

属性值是字符串时不可简写.

// old
let name = 'XingChuan';
let obj = {
name: name
};
// new
let name = 'XingChuan';
let obj = {
name
};
// error
let name = 'XingChuan';
let obj = {
name:'name'
};
  • 函数的简写
// old
let obj = {
show: function() {
console.log('show');
}
};
// new
let obj = {
show() {
console.log('show');
}
};

字面量形式定义对象

ES5只支持这种字面量定义:

let obj = {
name: 'XingChuan',
age: 10
};

ES6支持这种写法:

let obj ={
[name]: 'XingChuan',
['a' + 'ge']: 10
};
// 作为属性名的表达式会自动 toString() , 应避免使用对象作为表达式, 因为 String({}) === '[object Object]'

注意:

  • 属性名表达式与简洁表示法不能同时使用, 会报错
  • 属性名表达式为对象时属性名会重复的问题

Object.is('','') 同值相等API

Object.is() 基本等同于 === , 除却两点:

+0 === -0 // true
NaN === NaN // false Object.is(+0, -0) // false
Object.is(NaN, NaN) // true

Object.assign(target, source1, source2) 复制可枚举属性

let target = {
info: {
name: 'name1'
}
};
let source1 ={
info: {
name: 'name2',
age: 30
}
};
let source2 ={
info: {
name: 'name3'
}
}; Object.assign(target, source1, source2);
// { info: {name: 'name3'}}
//
// Object.assign 是浅拷贝, 如果属性的值是对象, 就会添加新的引用, 而不是在原有地址上添加属性.
// target会自动转换为对象, 所以不能为 null 或 undefined , 会报错
// source为 null 或 undefined 时, 因为无法转换为对象, 会跳过, 但不会报错
// 若 source 为字符串, 会以数组的形式复制到 target 中
//

属性的可枚举性

每个对象属性都有一个描述对象 Description , 可以控制是否可被枚举, 数据属性的其中之一.

let obj = {
name: 'XingChuan'
};
Object.getOwnPropertyDescriptor(obj,'name'); // {
// configurable: true,
// enumerable: true, // 如果可枚举为 false , 某些操作会忽略掉这个属性
// value: "XingChuan",
// writable: true
// }

ES5 中有 3 个属性会忽略 enumerablefalse 的属性:

  • for...in 循环
  • Object.keys()
  • JSON.stringify()

ES6 新增的 Object.assign() 也会忽略描述中不可枚举的属性.

数组中的 length 属性不会被 for...in 获取就是因为不可枚举的描述.

Object.getOwnPropertyDescriptor([1,2,3],'length');

// {
// configurable: false,
// enumerable: false,
// value: 3,
// writable: true
// }

另外, ES6 中也规定了 Class 原型上的方法是不可枚举的.

属性的遍历

  1. for...in

for...in 循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)

  1. Object.keys(obj)

Object.keys返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)

  1. Object.getOwnPropertyNames(obj)

Object.getOwnPropertyNames返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)

  1. Object.getOwnPropertySymbols(obj)

Object.getOwnPropertySymbols返回一个数组,包含对象自身的所有 Symbol 属性

  1. Reflect.ownKeys(obj)

Reflect.ownKeys返回一个数组,包含对象自身的所有属性,不管属性名是 Symbol 或字符串,也不管是否可枚举

以上 5 种方法在遍历顺序上, 遵循以下 3 条规则:

  • 首先遍历所有属性名为数值的属性,按照数字排序
  • 其次遍历所有属性名为字符串的属性,按照生成时间排序
  • 最后遍历所有属性名为 Symbol 值的属性,按照生成时间排序

__proto__, Object.getPrototypeOf(), Object.setPrototypeOf()

__proto__

__proto__ 指向当前实例的原型对象, 其没有被 ES6 列为正式 API, 但因为被浏览器厂商广泛使用, 被收入附录.

某些浏览器厂商同样指向原型对象, 可能是另一种命名方式, 所以为了兼容性考虑, 最好不要通过它去操作原型.

Object.setPrototypeOf()

Object.setPrototypeOf() 是 ES6 设置原型对象的方法

let obj = {
x: 10
};
let option = {
x: 20,
y: 30,
z: 40
};
Object.setPrototypeOf(obj, option); obj.x // 10
obj.y // 30
obj.z // 40 // 因原型链访问顺序的优先级, obj.x 为 10 而不是 20, 如 obj 不存在 x 的属性, obj.x 就会为 20.

Object.getPrototypeOf()

Object.getPrototypeOf(obj) 是 ES6 返回原型对象的方法

Object.keys(), Object.values(), Object.entries()

Object.keys() 是 ES5 中遍历属性的方法, ES6 新增了 Object.values(), Object.entries().

  • Object.values

返回对象自身的(不包含继承的), 可枚举的键值

  • Object.entries()

返回对象自身的(不包含继承的), 可枚举的键值对数组

对象的拓展运算符

ES8 中将数组的拓展运算符引入到了对象中.

解构

let {a, b, ...x} = {a: 1, b:2, c: 3, d: 4};
console.log(x);
// {c:3,d:4}

注意:

  1. 拓展运算符会复制对象所有未读取的键值对, 所以右侧必须是对象或可转换为对象, 不能为 null 或 undefined
  2. 拓展运算符必须置为最后一个位置, 否则会报错
  3. 拓展运算符不会复制原型上的方法

克隆对象

let x = {name: 'XingChuan', age: 88};
let cloneObj = { ...x };

合并对象

let x = {name: 'XingChuan', age: 88};
let y = {job: 'developer'};
let cloneObj = { ...x, ...y };

拓展运算符表达式

let obj = { ...{x > 1 ? {a: 1} : {} } };

扩展运算符的参数对象之中,如果有取值函数get,这个函数是会执行的.

let runtimeError = {
...a,
...{
get x() {
throws new Error('thrown now');
}
}
};

Object.getOwnPropertyDescriptors()

ES5 中 Object.getOwnPropertyDescriptor(obj, property) 可以获取对象属性的描述对象.

ES8 中 新增了 Object.getOwnPropertyDescriptors(obj) 可以获取对象所有属性的描述对象, 描述对象包括 get 和 set 属性.

Null 传导运算符

我们要读取对象的一个属性或调用其方法, 为了不报错, 应该先判断对象是否存在, 然后再读取其属性.

如果我们想读取 obj.info.xingchuan.name, 安全的写法应该是下面这样

let name = obj && obj.info && obj.info.xingchuan && obj.info.xingchuan.name || 'default';

现在提案中引入了 Null 传导运算符, 简化了写法, 可以写为下面这种方式.

let name = obj ?. info ?. xingchuan ?. name || 'default';

参考资料

  • MDN
  • JavaScript高级程序设计
  • ECMAScript6入门
  • JavaScript忍者禁术

JavaScript之Object拆解的更多相关文章

  1. 从头开始学JavaScript (十一)——Object类型

    原文:从头开始学JavaScript (十一)--Object类型 一.object类型 一个object就是一系列属性的集合,一个属性包含一个名字(属性名)和一个值(属性值). object对于在应 ...

  2. JavaScript中Object的总结

    基于原型继承,动态对象扩展,闭包,JavaScript已经成为当今世界上最灵活和富有表现力的编程语言之一. 这里有一个很重要的概念需要特别指出:在JavaScript中,包括所有的函数,数组,键值对和 ...

  3. JavaScript Math Object 数字

    JavaScript Math Object Math Object The Math object allows you to perform mathematical tasks. Math is ...

  4. JavaScript中Object.prototype.toString方法的原理

    在JavaScript中,想要判断某个对象值属于哪种内置类型,最靠谱的做法就是通过Object.prototype.toString方法. ? 1 2 var arr = []; console.lo ...

  5. 【WIP】客户端JavaScript Web Object

    创建: 2017/10/11   更新: 2017/10/14 标题加上[WIP],增加[TODO] 更新: 2018/01/22 更改标题 [客户端JavaScript Web Object, UR ...

  6. JavaScript中object和Object有什么区别

    JavaScript中object和Object有什么区别,为什么用typeof检测对象,返回object,而用instanceof 必须要接Object呢 ————————————————————— ...

  7. javascript nested object merge

    javascript nested object merge deep copy Object reference type function namespace(oNamespace, sPacka ...

  8. Javascript判断object还是list/array的类型(包含javascript的数据类型研究)

    前提:先研究javascript中的变量有几种,参考: http://www.w3school.com.cn/js/js_datatypes.asp http://glzaction.iteye.co ...

  9. javascript的 Object 和 Function

    一. javascript 的 内置对象: Object 和 Function javascript所有东西,包括 Function 都是对象 . Array  其实是一个 Function 类型的对 ...

随机推荐

  1. RabbitMQ插件安装

    RabbitMQ的有些插件没有集成在初始的安装中,它们需要额外安装,这些文件的后缀为.ez,安装时需要将.ez文件拷贝到安装的插件目录.以下是不同系统中默认安装的插件目录路径: 插件目录 Linux ...

  2. winform利用委托delegate进行窗体间通信,相同标题已经存在??

    前段时间学习委托,感觉很模糊的样子,也做过许多实例,但是项目中一直没有用到,今天在项目中遇到一个很简单的例子,现在拿出来,做一个简单的记录. 要求:将弹出框里勾选的内容返回到主面板上. 工具:委托. ...

  3. LRU算法总结

    LRU算法总结 无论是哪一层次的缓存都面临一个同样的问题:当容量有限的缓存的空闲空间全部用完后,又有新的内容需要添加进缓存时,如何挑选并舍弃原有的部分内容,从而腾出空间放入这些新的内容.解决这个问题的 ...

  4. java使用Junit工具进行单元测试

    目录 1.类的定义: 2.Junit工具的使用: 3.对该类进行单元测试并查看结果: 4.记录各个阶段的时间 5.将过程记录在个人博客上(github地址) 1.类的定义:类是同一事物的总称,类是封装 ...

  5. [UWP]浅谈按钮设计

    一时兴起想谈谈UWP按钮的设计. 按钮是UI中最重要的元素之一,可能也是用得最多的交互元素.好的按钮设计可以有效提高用户体验,构造让人眼前一亮的UI.而且按钮通常不会影响布局,小小的按钮无论怎么改也不 ...

  6. 【hibernate 初探】之 关系映射,ORM

    从整理上讲,一个ORM框架(以hibernate为例)所涉及内容无非就是,如何映射,如何检索,还有事务处理.所以从这三方面入手,基本上可以保证将hibernate可以用到自己的项目之中.所以我先说一下 ...

  7. xhr.readyState的就绪状态

    0:初始化,XMLHttpRequest对象还没有完成初始化 1:载入,XMLHttpRequest对象开始发送请求 2:载入完成,XMLHttpRequest对象的请求发送完成 3:解析,XMLHt ...

  8. ps命令学习笔记

    最近在看linux优化大师这本书,第2章祥细讲解了分析系统用的工具,这里把一些内容整理出来,以便加深印像. 当进行系统分析时,ps命令显示有关选择的活跃进程的信息.ps命令提供当前已存在的进程列表,和 ...

  9. Oracle查询多行数据合并成一行数据

    例如: select base_id, translate (ltrim (text1, '/'), '*/', '*,') xmmc,translate (ltrim (text2, '/'), ' ...

  10. Bash : 索引数组

    Bash 提供了两种类型的数组,分别是索引数组(indexed array)和关联数组(associative array).本文主要介绍索引数组的基本用法. 索引数组的基本特点 Bash 提供的数组 ...