Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
akira-cn opened this issue Aug 27, 2019 · 1 comment
Open

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

akira-cn opened this issue Aug 27, 2019 · 1 comment

Comments

@akira-cn
Copy link
Owner

akira-cn commented Aug 27, 2019

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

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

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

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

对于任意对象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中讨论。

@Liugq5713
Copy link

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

这边有个typo,应该是拥有setter,那么属性可写

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants