如何判断一个对象的某个属性是可写的?

更新日期: 2019-09-01 阅读: 3.2k 标签: 属性

这是一个咋一听好像很简单,但是实际上却没那么简单,而且是很有趣的问题。我们先来看一下什么情况下一个对象的属性是可写的。

“属性可写”这个概念并没有严谨的定义,我们这里先来规定一下。属性可写,是指满足如下条件:

对于任意对象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这样的静态类型语言在编译时检查,才是比较好的方案~

来源:https://mp.weixin.qq.com/s/tEupyvUsJLBnHKGq_Nqw3w
原创: 月影前端,公众号:前端冷知识

本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://fly63.com/article/detial/5073

相关推荐

html中的marquee属性

该标签不是HTML3.2的一部分,并且只支持MSIE3以后内核,所以如果你使用非IE内核浏览器(如:Netscape)可能无法看到下面一些很有意思的效果,该标签是个容器标签

vue里的$refs属性

vuejs的极大程度的帮助减少了对dom的操作,他主要通过添加ref属性,但是当获取this.$refs属性时,稍有不注意就会输出undefined导致我们对dom节点的操作报错。this.$refs.xxx为undefined的几种情况记录:

css的overflow属性

事实上我挺长一段时间都没弄清楚overflow:scroll与overflow:auto的差别,今天测试了一下,总算是明白了。visible: 不剪切内容。hidden: 将超出对象尺寸的内容进行裁剪,将不出现滚动条。scroll: 将超出对象尺寸的内容进行裁剪,并以滚动条的方式显示超出的内容。

css使用到的border边框属性

border 在一个声明中设置所有的边框属性。 border-bottom在一个声明中设置所有的下边框属性。border-bottom-color设置下边框的颜色。border-bottom-style设置下边框的样式。

Cookie 的 SameSite 属性

Chrome 51 开始,浏览器的 Cookie 新增加了一个 SameSite 属性,用来防止 CSRF 攻击和用户追踪。Cookie 往往用来存储用户的身份信息,恶意网站可以设法伪造带有正确 Cookie 的 HTTP 请求,这就是 CSRF 攻击。

React 也能“用上” computed属性

初次见到计算属性一词,是在 Vue 官方文档 《计算属性和侦听器》 一节中,文章中是这样描述计算属性的:模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的。在模板中放入太多的逻辑会让模板过重且难以维护。

css常用属性

text-align 属性规定元素中的文本的水平对齐方式。属性值:none | center | left | right | justify;font-size表示设置字体大小,如果设置成inherit表示继承父元素的字体大小值。

JS、Jquery中判断checkbox是否选中

attr()与prop()如何选择:attr()方法返回被选元素的属性值。prop() 方法设置或返回被选元素的属性和值。当该方法用于返回属性值时,则返回第一个匹配元素的值。当该方法用于设置属性值时,则为匹配元素集合设置一个或多个属性/值对。

css z-index属性

z-index 仅适用于定位元素。即 postition 值为 relative, absolute 和 fixed 属性;堆叠顺序是当前元素位于 z 轴上的值。值越大表示元素越靠近屏幕,反之元素越远离屏幕在同一个堆叠上下文中, z-index 值越大,越靠近屏幕。

CSS中的cursor属性

css中的cursor这个属性是用来设置光标形状的。这个属性定义了鼠标指针放在一个元素边界范围内时所用的光标的形状。默认值:auto,继承性:yes,出现版本:css2

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!