博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
你应该知道的JS —— 对象
阅读量:6654 次
发布时间:2019-06-25

本文共 8696 字,大约阅读时间需要 28 分钟。

对象的隐式转换

当对象之间相加obj1 + obj12或相减obj1 - obj2或者alert(obj)会发生什么?。 当某个对象出现在了需要原始类型才能进行操作的上下文时,JavaScript 会将对象转换成原始类型。在进行转换中的对象有有特殊的方法

  • 对于对象而言,不存在布尔转换,因为所有对象在上下文中布尔值都为ture,所以只有数字和字符串转换
  • 数字转换发生在减去对象或者应用数学函数时,例如,Date对象可以被减去,结果是date1 - data2两个日期之间的时间差
  • 字符串转换,当我们alert(obj)在类似的上下文中输出对象时,通常会发生这种情况
ToPrimitive

当我们在需要原始类型的上下文中使用对象时,例如在alert或者数学运算中,使用ToPrimitive算法将其转换为原始类型值。该算法允许我们使用特殊的对象方法自定义转换 根据上下文,转换具有所谓的提示

  • string 当一个操作期望一个字符串时,对于对象到字符串的转换,如alert
alert(obj);// 或者使用对象来作为属性anotherObj[obj] = 123;复制代码
  • number 当一个操作需要数字时,用于对象到数学的转换。例如
let num = Number(obj);let n = +objlet delta = date1 - date2;let greater = user1 > user2;复制代码
  • default 在少数情况下发生,当操作不确定期望的类型时。+这种运算符即可以进行字符串拼接也可以进行数学运算,所以字符串和数字都可以。或者当一个对象与字符串,数字或符号来判断是否相等时
let total = car1 + car2;if(user == 1) { ... };复制代码

大于小于运算符<>可以同时处理字符串和数字。不过,他使用number提示,而不是default提示,这是历史原因 在JavaScript中,除了一个特例(Date对象),其他的内置对象都按照与default相同的方式实现转换number

为了进行转换, JavaScript会尝试查找并调用三个对象方法

  1. 调用obj[Symbol.toPrimitive](init) 如果方法存在
  2. 否则,如果提示是string
    • 尝试obj.toString()obj.valueOf()
  3. 否则, 如果提示是numberdefault
    • 尝试obj.valueOf()obj.toString()
Symbol.toPrimitive

例子如下:

let user = {  name: 'john',  money: 1000,  [Symbol.toPrimitive](hint){    console.log(hint);    return hint === 'string' ? this.name : this.money  }}console.log(`${user}`);// string// joinconsole.log(+user);// Number// 1000console.log(user === 1000);// default// true复制代码
toString()和valueOf()

toString()以及valueOf从远古时代到来,他们不是符号,而是常规字符串命名的方法。他们提供了一种替代老式的方式来实现转换。 如果没有Symbol.toPrimitive那么JavaScript会尝试查找他们并按顺序尝试:

  • toString -> valueOf 为字符串提示
  • valueOf -> toString除此以外
let user = {  name: 'john',  money: 1000,  toString(){  ``return this.name;  },  valueOf(){    return this.money;  }}console.log(`${user}` + 1); // john1console.log(+user); // 1000console.log(user == 1000); // true复制代码

最后附上一张JavaScript原始类型转换表

对象的遍历

for...in循环

为了遍历对象的所有键,存在一个特殊的循环形式: for...in

let user = {  name: 'john',  age: 30,  isAdmin: true}for(let key in user){  // keys  alert(key); // name, age, isAdmin  alert(user[key]); // john, 20, true}复制代码

对象遍历的顺序并不是按照添加顺序创建的,而是具有一定规则的, 先是整数属性排序,其他的则以创建顺序出现

let codes = {  "49": "Germany",  "41": "Switzerland",  "44": "Great Britain",  // ...  "1": "USA"}for(let code in codes){  console.log(code); // 1 , 41, 44, 49;}复制代码

使用for...in遍历对象是无法直接获取属性值,因为他实际上遍历的是对象中所有可枚举属性,你需要手动获取属性值。而在ES6中我们可以借助for...ofIterator来直接获取属性值。 简单介绍下Iterator,在ES6中新添了MapSet,加上原有的数据结构, 用户还可以组合使用他们,因此需要统一的接口机制,来处理不同的数据结构。遍历器(Iterator)就是这样一种结构,只要在数据结构中部署它,就可以完成遍历操作 Iteerator的遍历过程是这样的

  1. 创建一个指针对象,指向当前数据结构的起始位置
  2. 第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员
  3. 第二次调用指针对象的next方法,指针就指向数据结构第二个成员
  4. 不断调用指针对象的next方法,直到他指向数据结构的结束位置 每一次调用next方法就会泛函一个包含valuedone两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。 当我们使用for...of循环遍历某种数据结构时,该循环会自动去寻找Iterator接口。 结合这两点我们可以创建对象的遍历器接口
let obj = {      a: 1,      b: 2    }Object.defineProperty(obj, Symbol.iterator, {  value(){    var o = this;    var idx = 0;    var k = Object.keys(o);    return {      next(){        return {          value: o[k[idx++]],          done: (idx > k.length)        }      }    }  }})复制代码

对象的克隆

对象与原始类型之间的根本区别之一是他们他们通过引用存储和复制 原始类型值: string, number, boolean被分配/复制为整体值 例如:

let message = "hello";let pharse = message;复制代码

因此我们有两个独立的变量,每个变量都存储字符串hello

对象不是这样的 对象存储的是其值的内存地址,而非值本身

let user = {  name: 'john'}复制代码

当一个变量被赋值为对象时,赋值的是其值的内存地址(引用),而不是对象本身 我们可以将一个对象想象成一个橱柜,那么变量就是橱柜的钥匙,赋值变量就会赋值钥匙,但不是橱柜本身

let user = { name: "John" };let admin = user;复制代码

现在我们有两个变量,每个变量都引用同一个对象

我们可以使用任何变量来访问控制橱柜并修改其内容

let user = { name: 'john' };let admin = user;admin.name = 'pete';alert(user.name); // 'pete'复制代码

只有两个对象是同一个对象时,他们才是相等的

例如两个变量引用同一个对象,他们是相等的

let a = {};let b = a;console.log( a == b) // trueconsole.log( a === b) // true复制代码

这里两个独立的对象是不相等的,尽管他们都是空的

let a = {};let b = {};console.log( a == b); // false复制代码

因此复制一个对象变量会创建对同一个对象的引用。 但是如果我们需要复制一个对象呢?我们需要创建一个新对象并通过遍历他的属性来复制现有对象的结构 例如:

let user = {  name: 'john',  age: 30}let clone = {}; for(let key in user){  clone[key] = user[key];}clone.name = 'pete';console.log(user.name);复制代码

我们也可以使用Object.assgin方法,

let user = {  name: 'john',  age: 30}let clone = Object.assign({}, user);复制代码

到现在为止,我们认为所有的属性user都是原始类型的,但属性可以是对其他对象的引用,如何处理他们 例如这个

let user = {  name: 'john',  sizes: {    height: 182,    width: 50  }};let clone = Object.assign({}, user);console.log( user.sizes === clone.sizes );user.sizes.width++;console.log(clone.sizes.width) 51复制代码

为了解决这个问题,我们应该递归检查每个值的类型,如果他是一个对象,就复制他的结构,这就是所谓的深度克隆 简单的例子:

let user = {  name: 'john',  size: {    height: 182,    width: 50  }}let cloneobj = {};function clone(source, target){  let keys = Object.keys(source);  for(let key of keys){    if(typeof source[key] == 'object'){      target[key] = clone(source[key], {});    }else{      target[key] = source[key];    }  }  return target;}    clone(user, cloneobj);user.size.width++;console.log(cloneobj);复制代码

而在HTML5规范中提出了一种用于深层克隆的标准算法,用于处理上述情况和更复杂的情况,称为

关于结构化克隆的好处是在于他处理循环对象并支持大量内置类型,问题在于算法并不对用户直接暴露,只能作为API的一部分

MessageChannel

只要你调用postMessage结构化克隆算法就可以使用,我们可以创建一个MessageChannel并发送消息。在接收端,消息包含我们原始数据对象的结构化克隆

function strucuralClone(obj){  return new Promise(resolve => {    const {port1, port2} = new MessageChannel();    port2.onmessage = ev => resolve(ev.data);    port1.postMessage(obj);  })}const user = {  a: 1,  b: {    c: 2,  }}user.c = user;const clone = strucuralClone(user);clone.then( (result) => console.log(result))复制代码
History API

如果你曾经使用history.pushState(),那么您可以提供一个状态对象来保存URL。事实证明,这个状态对象在结构上被同步克隆。同时我们必须小心,不要混淆可能使用状态对象的任何程序逻辑,所以我们需要在完成克隆后恢复原始状态。为了防止发生任何事件,请使用history.replaceState()而不是history.pushState();

function strucuralClone(obj){  const oldState = history.state;  history.replaceState(obj, document.title);  const copy = history.state;  history.replaceState(oldState, document.title);  return copy;}const user = {  a: 1,  b: {    c: 2,  }}user.c = user;const clone = strucuralClone(user);console.log(clone)复制代码
Notification API
function strucuralClone(obj){  return new Notification('', {data: obj, silent: true});}const user = {  a: 1,  b: {    c: 2,  }}user.c = user;user.a = 2;const clone = strucuralClone(user);user.a = 3;console.log(clone)复制代码

结构化克隆优于JSON的地方

  • 结构化克隆可以复制RegExp对象
  • 结构化克隆可以复制Blob,File以及FileList对象
  • 结构化克隆可以复制ImageData对象。
  • 结构化克隆可以正确地复制有循环引用的对象

结构化克隆所不能做到的

  • Error以及Function对象是不能被结构化克隆算法复制的
  • 企图克隆DOM节点同样会抛出错误
  • 对象的某些特定参数也不会被保留
  • 原型链上的属性也不会被追踪以及复制

性能比较

这些克隆方式只是黑科技,在项目中还是乖乖用lodash提供的clone方法把。

对象的不可变性

有时候你会希望属性或者对象是不可改变的,在ES5中可以通过多种方法来实现

对象常量

结合writeable: falseconfigurable:false就可以创建一个真正的常量属性(不可修改, 重定义, 或者删除);

var myObject = {};Object.defineProperty(myObject, "freez_number", {  value: 42,  writable: false,  configurable: false})复制代码
禁止扩展

如果你想禁止一个对象添加新属性并且保留已有属性,可以使用Object.preventExtensions(..)

var myObject = {  a: 2}Object.preventExtensions(myObject);myObject.b = 3;myObject.b // undefiend复制代码
密封

Object.seal(..)会创建一个密封对象,这个方法实际上会在一个现有对象上调用Object.preventExtensions(..)并把所有属性标记为configurable: false 所以密封后不仅不能添加新属性也不能重新配置或者删除任何现有属性(虽然可以修改属性的值)

冻结

Object.freeze()会创建一个冻结对象,这个方法实际上会在一个现有对象上调用Object.seal() 并把所有数据访问属性标记为writeable: false这样就无法修改他们的值 这个方法是你可以应用在对象上的级别最高的不可变性,他会禁止对于对本身及其任意直接属性的修改。 重要的一点: 所有的方法创建的都是浅不可变性,也就是说,他们只会影响目标对象和他的直接属性。如果目标对象引用了其他对象(数组, 对象. 函数, 等) 其他对象的内容不受影响, 仍然是可变的

不过我们可以深度冻结一个对象,具体方法为,首先在这个对象上调用Object.freeze(..)然后遍历他引用的所有对象并在这些对象上调用Object.freeze(...),但是一定要小心,因为这样做有可能会在无意中冻结其他对象(共享对象)

为什么需要不可变性

下面的代码能够体现不可变性的重要性

var arr = [1, 2, 3];foo(arr);console.log(arr[0]);复制代码

从表面上讲,你可能认为arr[0]的值仍然为1但事实是否如此不得而知,因为foo(...)可能会改变那你传入其中的arr所引用的数组,所以我么需要上面的方法来让对象不可变

var arr = Object.freeze([1, 2, 3]);foo(arr)console.log(arr[0]);复制代码

可以非常确定arr[0]就是1 这是非常重要的,因为这可以使我们更容易的理解代码当我们将对象传递到我们看不到或者不能控制的地方,我们依然能够相信这个值不会改变

不可变性带来的性能问题

每当我们开始创建一个新值(数组,对象)取代修改已经存在的值时,很明显的问题是,性能上会有问题。 如果在你的程序中,只会发生一次或几次单一的状态变化,那么扔掉一个旧对象或旧数组完全没必要担心,性能损失会非常非常小————顶多几微妙。但是如果频繁的进行这样的操作,那么性能问题就需要考虑了。像数组这样的数据结构,我们期望除了能够保存其原始的数据,然后能追踪其每次改变并根据之前的版本创建一个分支 在内部,他可能就像一个对象引用的链表树,树中的每个节点都表示原始值的改变。

如果是开发的话,我们也可以使用Immutable.js这种成熟的库来进行开发

Getter 和 Setter

在ES5中可以使用gettersetter部分改写默认操作, 但是只能应用在单个属性上,无法应用在整个对象上(ES6中proxy的出现可以改写整个对象),getter是一个隐藏函数,会在获取属性值时调用,setter也是一个隐藏的函数,会在设置属性值时调用。 当你给一个属性定义getter, setter或者两者都有时,这个属性会被定义为访问描述符。对于访问描述符来说,javascript会忽略他们的valuewriteable特性,取而代之的是关心setget(还有configurable和enumerable)特性

let myObject = {  get a(){    return this._a;  }  set a(val){    this._a = val * 2;  }}myObject.a = 2;myObject.a; //4复制代码

存在性

看下面代码

var myObject = {  a: undefiend}myObject.a // undefiend;myObject.b // undefiend复制代码

这时我们可以看出,如myObject.a的属性访问返回值可能是undefiend,但是这个值有可能是属性中存储的undefiend,也可能是因为属性不存在所以返回undefined,那么怎么区别这两种对象呢

var myObject = {  a: 2}('a' in myObject);  // true('b' in myObject);  // falsemyObject.hasOwnProperty("a");  // true;myObject.hasOwnProperty("b");  // false复制代码

in操作符会检查属性是否在对象及其[[property]]原型链中,相比之下,hasOwnProperty(...)只会检查属性是否在myObject对象中,不会检查[[prototype]]链。

参考资料:

  • 你不知道的js

转载于:https://juejin.im/post/5abce8ac5188255c3200d0c5

你可能感兴趣的文章
Linux的五个查找命令:find,locate,whereis,which,type
查看>>
KK课表抓取教务系统
查看>>
mac上如何某端口号被哪些程序占用
查看>>
mac 随记
查看>>
易宝典文章——玩转Office 365中的Exchange Online服务 之二十四 配置垃圾邮件筛选器反垃圾邮件...
查看>>
读写者锁与生产者/消费者模式
查看>>
关于python中的if __name__=='__main__'语句问题
查看>>
节约时间的18种方法
查看>>
Debian下搭建zabbix监控
查看>>
病毒与***的查杀
查看>>
线程组
查看>>
涉密数据的处理
查看>>
我的友情链接
查看>>
【单机实现系列】通过scom2012对Hyper-V主机来监控和邮件报警②
查看>>
python简介
查看>>
python字典开发三级菜单
查看>>
.net Framework下载地址
查看>>
十三个经典算法集锦
查看>>
深圳偶遇
查看>>
给自己电脑安装SSD与加内存条
查看>>