这是一个咋一听好像很简单,但是实际上却没那么简单,而且是很有趣的问题。

我们先来看一下什么情况下一个对象的属性是可写的。

“属性可写”这个概念并没有严谨的定义,我们这里先来规定一下。

属性可写,是指满足如下条件:

对于任意对象object,该对象的a属性可写,是指如下代码成立:

const value = Symbol();

object.a = value;

console.assert(obj.a === value);

JavaScript有几种情况下,对象属性不可写。

?? 第一种情况,如果这个属性是accessor property,并且只有一个getter时,这个属性不可写。

const obj = {

  get a(){

    return 'a';

  }

};

console.log(obj.a); // a

obj.a = 'b';

console.log(obj.a); // a

?? 第二种情况,如果这个属性的Descriptor中设置了writable为false,这个属性不可写。

const obj = {};

Object.defineProperty(obj, 'a', {

  value: 'a',

  writable: false,

});

console.log(obj.a); // a

obj.a = 'b';

console.log(obj.a); // a

?? 第三种情况,目标对象被Object.freeze,实际上也是将对象上所有属性的writable设为了false:

const obj = {a: 'a'};

Object.freeze(obj);

console.log(obj.a); // a

obj.a = 'b';

console.log(obj.a); // a

那么了解了这些情况,我们就可以尝试写一个方法来判断对象属性是否可写了:

function isOwnPropertyWritable(obj, prop) {

  const des = Object.getOwnPropertyDescriptor(obj, prop);

  return des == null || des.writable || !!des.set;

}

上面这个方法可以简单判断一个对象自身的属性是否可写,判断逻辑也不复杂,先通过Object.getOwnPropertyDescriptor(obj, prop)方法获取对象自身属性的Descriptor,接下来有三种情况对象的这个属性可写:

  • 这个Descriptor不存在,表示对象上没有该属性,那么我们可以动态添加这个属性

  • 这个Descriptor存在,且writable为true,那么属性可写

  • 这个Descriptor存在,且拥有getter,那么属性可写

看似好像解决了这个问题,但是,实际上这个判断有很多问题。

首先,最大的问题是,这个方法只能判断对象自身的属性,如果对象原型和原型链上的属性,实际上getOwnPropertyDescriptor是访问不到的,我们看一个简单例子:

function isOwnPropertyWritable(obj, prop) {

  const des = Object.getOwnPropertyDescriptor(obj, prop);

  return des == null || des.writable || !!des.set;

}

class A {

  get a() {

    return 'a';

  }

}

const obj = new A();

console.log(isOwnPropertyWritable(obj, 'a')); // true

console.log(obj.a); // a

obj.a = 'b';

console.log(obj.a); // a

上面的代码,我们预期的isOwnPropertyWritable(obj, 'a')应该返回false,但实际上却是返回true,这是因为Object.getOwnPropertyDescriptor获取不到class中定义的getter,该getter实际上是在obj的原型上。

要解决这个问题,我们需要沿原型链递归判断属性:

function isPropertyWritable(obj, prop) {

  while(obj) {

    if(!isOwnPropertyWritable(obj, prop)) return false;

    obj = Object.getPrototypeOf(obj);

  }

  return true;

}

我们实现一个isPropertyWritable(obj, prop),不仅判断自身,也判断一下它的原型链。

这样我们就解决了继承属性的问题。

function isOwnPropertyWritable(obj, prop) {

  const des = Object.getOwnPropertyDescriptor(obj, prop);

  return des == null || des.writable || !!des.set;

}

function isPropertyWritable(obj, prop) {

  while(obj) {

    if(!isOwnPropertyWritable(obj, prop)) return false;

    obj = Object.getPrototypeOf(obj);

  }

  return true;

}

class A {

  get a() {

    return 'a';

  }

}

class B extends A {

}

const a = new A();

const b = new B();

console.log(isPropertyWritable(a, 'a')); // false

console.log(isPropertyWritable(b, 'a')); // false

但是实际上这样实现还是有缺陷,我们其实还少了几个情况。

首先,我们处理原始类型,比如现在下面的代码会有问题:

const obj = 1;

obj.a = 'a';

console.log(isPropertyWritable(obj, 'a')); // true

console.log(obj.a); // undefined

所以我们要修改一下isOwnPropertyWritable的实现:

function isOwnPropertyWritable(obj, prop) {

  if(obj == null) return false;

  const type = typeof obj;

  if(type !== 'object' && type !== 'function') return false;

  const des = Object.getOwnPropertyDescriptor(obj, prop);

  return des == null || des.writable || !!des.set;

}

然后,其实还有一些case,比如:

function isOwnPropertyWritable(obj, prop) {

  if(obj == null) return false;

  const type = typeof obj;

  if(type !== 'object' && type !== 'function') return false;

  const des = Object.getOwnPropertyDescriptor(obj, prop);

  return des == null || des.writable || !!des.set;

}

function isPropertyWritable(obj, prop) {

  // noprotected

  while(obj) {

    if(!isOwnPropertyWritable(obj, prop)) return false;

    obj = Object.getPrototypeOf(obj);

  }

  return true;

}

const obj = {};

Object.seal(obj);

console.log(isPropertyWritable(obj, 'a')); // true

obj.a = 'b';

console.log(obj.a); // undefined

我们还需要考虑seal的情况。

?? Object.seal 方法封闭一个对象,阻止添加新属性并将所有现有属性标记为不可配置。

所以对这种情况我们也要加以判断:

function isOwnPropertyWritable(obj, prop) {

  if(obj == null) return false;

  const type = typeof obj;

  if(type !== 'object' && type !== 'function') return false;

  if(!(prop in obj) && Object.isSealed(obj)) return false;

  const des = Object.getOwnPropertyDescriptor(obj, prop);

  return des == null || des.writable || !!des.set;

}

好了,那最后得到的版本就是这样的:

function isOwnPropertyWritable(obj, prop) {

  // 判断 null 和 undefined

  if(obj == null) return false;

  // 判断其他原始类型

  const type = typeof obj;

  if(type !== 'object' && type !== 'function') return false;

  // 判断sealed的新增属性

  if(!(prop in obj) && Object.isSealed(obj)) return false;

  // 判断属性描述符

  const des = Object.getOwnPropertyDescriptor(obj, prop);

  return des == null || des.writable || !!des.set;

}

function isPropertyWritable(obj, prop) {

  while(obj) {

    if(!isOwnPropertyWritable(obj, prop)) return false;

    obj = Object.getPrototypeOf(obj);

  }

  return true;

}

这样就100%没问题了吗?

也不是,严格来说,我们还是可以trick,比如给对象故意设一个setter:

function isOwnPropertyWritable(obj, prop) {

  // 判断 null 和 undefined

  if(obj == null) return false;

  // 判断其他原始类型

  const type = typeof obj;

  if(type !== 'object' && type !== 'function') return false;

  // 判断sealed的新增属性

  if(!(prop in obj) && Object.isSealed(obj)) return false;

  // 判断属性描述符

  const des = Object.getOwnPropertyDescriptor(obj, prop);

  return des == null || des.writable || !!des.set;

}

function isPropertyWritable(obj, prop) {

  while(obj) {

    if(!isOwnPropertyWritable(obj, prop)) return false;

    obj = Object.getPrototypeOf(obj);

  }

  return true;

}

const obj = {

  get a() {

    return 'a';

  },

  set a(v) {

    // do nothing

  }

}

console.log(isPropertyWritable(obj, 'a')); // true

obj.a = 'b';

console.log(obj.a); // a

你可能会说,这种trick太无聊了,但是事实上类似下面的代码还是有可能写出来的:

const obj = {

  name: 'a',

  get a() {

    return this.name;

  },

  set a(v) {

    this.name = v;

  }

};

Object.freeze(obj);

console.log(isPropertyWritable(obj, 'a'));

当然要解决这个问题也不是不可以,还要加上一个判断:

function isOwnPropertyWritable(obj, prop) {

  // 判断 null 和 undefined

  if(obj == null) return false;

  // 判断其他原始类型

  const type = typeof obj;

  if(type !== 'object' && type !== 'function') return false;

  // 判断是否被冻结

  if(Object.isFrozen(obj)) return false;

  // 判断sealed的新增属性

  if(!(prop in obj) && Object.isSealed(obj)) return false;

  // 判断属性描述符

  const des = Object.getOwnPropertyDescriptor(obj, prop);

  return des == null || des.writable || !!des.set;

}

所以,要考虑的情况着实不少,也不知道还有没有没考虑周全的。

有可能还真得换一个思路,从定义入手:

function isPropertyWritable(obj, prop) {

  const value = obj[prop];

  const sym = Symbol();

  try {

    obj[prop] = sym;

  } catch(ex) {

    // 解决在严格模式下报错问题

    return false;

  }

  const isWritable = obj[prop] === sym;

  obj[prop] = value; // 恢复原来的值

  return isWritable;

}

这样就解决了问题,唯一的问题是对属性做了两次赋值操作,不过应该也没有太大的关系。

补充:经过大家讨论,上面这个思路也不行,如果属性的setter中执行一些操作,会有很大的问题,比如我们observe一些对象,用这个方法因为写入了两次,可能会触发两次change事件。。。

所以基本上运行时判断某个属性可写,没有特别好的手段,也许只能使用TypeScript这样的静态类型语言在编译时检查,才是比较好的方案~

好了,关于判断对象属性是否可写的方法,你还有什么问题,欢迎在issue中讨论。

关于奇舞周刊

《奇舞周刊》是360公司专业前端团队「奇舞团」运营的前端技术社区。关注公众号后,直接发送链接到后台即可给我们投稿。

一个对象的属性_【前端冷知识】如何判断一个对象的某个属性是可写的?相关推荐

  1. extjs中滚动条属性_前端学习随笔6 盒模型及相关属性

    注:测试浏览器版本号--chrome 75.0.3770.80:opera 60.0.3255.109:firefox 67.0:ie 11. 一 心得体会 最大的收获是第一次知道了outline(轮 ...

  2. 前端里的button怎么去除点击自带边框_前端不为人知的一面--前端冷知识集锦

    前端已经被玩儿坏了!像console.log()可以向控制台输出图片等炫酷的玩意已经不是什么新闻了,像用||操作符给变量赋默认值也是人尽皆知的旧闻了,今天看到Quora上一个帖子,瞬间又GET了好多前 ...

  3. 这些鲜为人知的前端冷知识,你都GET了吗?

    来源:猴哥说前端‍‍‍‍‍‍ 背景 最近公司项目不多,比较清闲,划水摸鱼混迹于各大技术博客平台,瞬间又GET了好多前端技能,一些属于技巧,一些则是闻所未闻的冷知识,一时间还消化不过来,不由的发出一声感 ...

  4. 前端wxml取后台js变量值_这些鲜为人知的前端冷知识,你都GET了吗?

    背景 最近公司项目不多,比较清闲,划水摸鱼混迹于各大技术博客平台,瞬间又GET了好多前端技能,一些属于技巧,一些则是闻所未闻的冷知识,一时间还消化不过来,不由的发出一声感叹! 前端可真是博大精深 于是 ...

  5. excel判断字符串包含另一个字符串_【前端冷知识】如何正确判断一个字符串是数值?...

    在网页中,我们从用户输入的内容中获取的值通常是字符串,但是有时候我们希望用户输入的内容一定要能转成数值: <input id="userInput"> userInpu ...

  6. 前端不为人知的一面--前端冷知识集锦

    转自:http://www.cnblogs.com/Wayou/p/things_you_dont_know_about_frontend.html 前端已经被玩儿坏了!像console.log()可 ...

  7. display属性_前端基础:Grid 布局教程,重新复习grid布局的容器和项目属性

    链接:http://www.ruanyifeng.com/blog/2019/03/grid-layout-tutorial.html 一.概述 它将网页划分成一个个网格,可以任意组合不同的网格,做出 ...

  8. css border 虚线间距_【前端冷知识】CSS如何实现虚线框动画

    我们知道,CSS支持将元素的border属性设为虚线,例如: <h1>君喻学堂h1> h1 {   border: dashed 1px; } 但是,CSS的虚线样式是固定的,如果我 ...

  9. canvas刷新_【前端冷知识】Canvas 滤镜的性能优化

    最近几天没有及时更新,是因为这几天在忙一个项目mesh.js,这个项目是一个基于Canvas2D和WebGL的跨平台图形系统,提供底层的高性能API,同时也将是未来新版SpriteJS的底层渲染引擎. ...

最新文章

  1. RDKit | 分子的多种构象
  2. IO多路复用之poll
  3. ASP.Net请求小周期
  4. 【推荐系统】推荐系统冷启动问题
  5. springBoot的模版引擎
  6. 看图了解RocksDB
  7. sqlserver命令行修改用户登录密码
  8. java 将 ResultSet 转化为 json格式
  9. cp105b linux 驱动,cp105b驱动下载-富士施乐cp105b驱动下载v2.6.15.0 官方最新版-西西软件下载...
  10. 如何将一个服务器加入域控中,Windows Server如何创建域并加入域
  11. 利用android实现汇率计算器,利用python编写一个汇率计算器
  12. Oracle 中LONG RAW BLOB CLOB类型介绍
  13. 宿小程民序的开发有哪些功能
  14. Windows Server 2008 R2 桌面化
  15. k3显示远程服务器未打开,k3客户端远程服务器链接
  16. 我知道你不想跳槽,但你不该拒绝面试机会
  17. 阿里P8架构师深度概述互联网分布式架构
  18. 2022 SWPU NSS新生赛|MISC知识点
  19. 服务器缺少字体文件导致下载文件乱码解决方案
  20. 打造开源、开放的生态系统,KubeSphere“三步走”布局云原生

热门文章

  1. WordCount代码详解
  2. Python编程基础:第十九节 索引Index Operator
  3. AI新浪潮:截止2022年,全球74%的计算将来自端侧
  4. Java 编程语言中很少被人了解的特性-statement label
  5. Redis 如何分布式,来看京东金融的设计与实践
  6. spring-data-redis 使用过程中需要注意的地方
  7. mysql 压力测试之批量插入自增字段不连续问题
  8. mysql --The MEMORY Storage Engine--官方文档
  9. 【MarkDown】:MarkDown编辑器
  10. python评分卡建模-卡方分箱(2)之代码实现