林秀栋的技术博客

js中的对象描述符(数据属性和访问器属性)

文章来源 文章来源

数据属性和访问器属性

创建一个对象:

let person = {
  name: "Nicholas",
  _job: "Software Engineer",
  sayName: function () {
    alert(this.name);
  },
  get job() {
    return this._job;
  },
  set job(newJob) {
    this._job = newJob;
  },
};

在这个对象中,我们定义了一个 name 属性和一个_job 属性;至于以 set 和 get 开头的两处代码,他们共同定义了一个属性 job。

JavaScript 中的对象有两种不同类型的属性:数据属性和访问器属性。

name 和_job 是数据属性,job 是访问器。数据属性和访问器属性的最大的不同就在于:当访问一个访问器属性时,得到 get 后面函数的返回值;给一个访问器属性赋值时,执行的是 set 后面的函数

console.info(person.job); //Software Engineer
person.job = "Coder";
console.info(person.job); //Coder
console.info(person._job); //Coder

每个对象的属性在创建时都带有一些特征值(可以通过 Object.getOwnPropertyDescriptors 或 Object.getOwnPropertyDescriptor 得到),JavaScript 通过这些特征值来定义它们的行为。

var descriptors= Object.getOwnPropertyDescriptors(person)
//descriptors的内容如下;
{
    job:{enumerable: true, configurable: true, get: ƒ, set: ƒ}
    name:{value: "Nicholas", writable: true, enumerable: true, configurable: true}
    sayName:{writable: true, enumerable: true, configurable: true, value: ƒ}
    _job:{value: "Coder", writable: true, enumerable: true, configurable: true}
}

可以看到

数据属性的描述符的有四个属性分别是:

  1. value:包含这个属性的数据值。读取属性的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。默认为 undefined.
  2. writable:表示能否修改属性的值。是一个 bool 值,默认为 true
  3. enumerable:属性是否可枚举,即能否通过 for-in 循环返回属性。是一个 bool 值,默认为 true
  4. configrable:属性是否可配置。即属性能否通过 delete 删除,能否修改属性的特性,或者能否把属性修改为访问器属性。是一个 bool 值,默认为 true

访问器属性的描述符的有四个属性分别是:

  1. get:在读取属性时调用的函数。默认值为 undefined。
  2. set:在写入属性时调用的函数。默认值为 undefined。
  3. enumerable:属性是否可枚举,即能否通过 for-in 循环返回属性。是一个 bool 值,默认为 true
  4. configrable:属性是否可配置。即属性能否通过 delete 删除,能否修改属性的特性,或者能否把属性修改为数据属性。是一个 bool 值,默认为 true

要修改属性默认的特性,必须使用 Object.defineProperty 或 Object.defineProperties。

数据属性:

var person = {};
//除了configrable之外,其他三个属性相互之间不会影响,读者可以自己测试

console.info(
  "---------------------writable start--------------------------------"
);
Object.defineProperty(person, "name", {
  writable: false,
  enumerable: true,
  configurable: true,
  value: "Nicholas",
});
console.info(person.name); //"Nicholas"
person.name = "Greg";
//writable为false,属性不可修改
console.info(person.name); //"Nicholas"

//writable为false,但configrable为true,我们可以重新配置属性描述符,
Object.defineProperty(person, "name", {
  writable: false,
  enumerable: true,
  configurable: true,
  value: "John",
});
console.info(person.name); //John

delete person.name;
//configrable为true,属性可以被删除
console.info(person.name); //undefined
console.info(
  "---------------------writable end--------------------------------"
);

console.info(
  "---------------------enumerable start--------------------------------"
);
var person = {};
Object.defineProperty(person, "name", {
  writable: false,
  enumerable: true,
  configurable: true,
  value: "Nicholas",
});

//enumerable为true属性可枚举
for (var prop in person) {
  console.info(prop); //name
}

Object.defineProperty(person, "name", {
  writable: false,
  enumerable: false,
  configurable: true,
  value: "Nicholas",
});

//enumerable为false属性不可枚举,循环体不执行
for (var prop in person) {
  console.info(prop); //
}
console.info(
  "---------------------enumerable end--------------------------------"
);

console.info(
  "---------------------configurable start--------------------------------"
);
var person = {};
Object.defineProperty(person, "name", {
  writable: true,
  enumerable: true,
  configurable: false,
  value: "Nicholas",
});
//configurable为false,writable为true,属性仍然可修改
person.name = "John";
console.info(person.name); //John

//configurable为false,writable为true,仍然可以通过配置的方式改变属性值
Object.defineProperty(person, "name", {
  writable: true,
  enumerable: true,
  configurable: false,
  value: "Nicholas",
});
console.info(person.name);

//configurable为false,enumerable为ture,属性可枚举
for (var prop in person) {
  console.info(prop); //name
}

//configurable为false,我们仍然可以把writable属性由true改为false
Object.defineProperty(person, "name", {
  writable: false,
  enumerable: true,
  configurable: false,
  value: "Nicholas",
});
console.info(Object.getOwnPropertyDescriptor(person, "name")); //{value: "Nicholas", writable: false, enumerable: true, configurable: false}

//configurable为false,writable为false,不能通过配置改变value的值
try {
  Object.defineProperty(person, "name", {
    writable: false,
    enumerable: true,
    configurable: false,
    value: "John",
  });
} catch (error) {
  console.info("value change error");
  console.info(Object.getOwnPropertyDescriptor(person, "name")); //{value: "Nicholas", writable: false, enumerable: true, configurable: false}
}

//configurable为false,但是不能把writable属性由false改为true
try {
  Object.defineProperty(person, "name", {
    writable: true,
    enumerable: true,
    configurable: false,
    value: "Nicholas",
  });
} catch (error) {
  console.info("writable false to true error");
  console.info(Object.getOwnPropertyDescriptor(person, "name")); //{value: "Nicholas", writable: false, enumerable: true, configurable: false}
}

//configurable为false,不能改变enumerable的值
try {
  Object.defineProperty(person, "name", {
    writable: false,
    enumerable: false,
    configurable: false,
    value: "Nicholas",
  });
} catch (error) {
  console.info("enumerable change error");
  console.info(Object.getOwnPropertyDescriptor(person, "name")); //{value: "Nicholas", writable: false, enumerable: true, configurable: false}
}

var person = {};
Object.defineProperty(person, "name", {
  writable: true,
  enumerable: true,
  configurable: true,
  value: "Nicholas",
});

//configurable为true,可以把数据属性修改为访问器属性
try {
  Object.defineProperty(person, "name", {
    get: function () {
      return "Nicholas";
    },
    enumerable: true,
    configurable: false,
  });
} catch (error) {
  console.info("get error");
}
console.info(Object.getOwnPropertyDescriptor(person, "name")); //{set: undefined, enumerable: true, configurable: false, get: ƒ}

var person = {};
Object.defineProperty(person, "name", {
  writable: true,
  enumerable: true,
  configurable: false,
  value: "Nicholas",
});
//configurable为false,不可以把数据属性修改为访问器属性
try {
  Object.defineProperty(person, "name", {
    get: function () {
      return "Nicholas";
    },
    enumerable: true,
    configurable: false,
  });
} catch (error) {
  console.info("get error");
}
console.info(Object.getOwnPropertyDescriptor(person, "name")); //{value: "Nicholas", writable: true, enumerable: true, configurable: false}

console.info(
  "---------------------configurable end--------------------------------"
);

访问器属性

//访问器属性

console.info("------------------------------------------------------");
var person = { _name: "Nicholas" };
Object.defineProperty(person, "name", {
  get: function () {
    console.info("get 被调用");
    return this._name;
  },
  set: function (newName) {
    console.info("set 被调用");
    this._name = newName;
  },
  enumerable: true,
  configurable: true,
});
person.name; //get 被调用
person.name = "John"; //set 被调用

console.info("------------------------------------------------------");
console.info(
  "----------------------不设set 开始--------------------------------"
);

var person = { _name: "Nicholas" };

Object.defineProperty(person, "name", {
  get: function () {
    console.info("get 被调用");
    return this._name;
  },
  enumerable: true,
  configurable: true,
});
person.name; //get 被调用
person.name = "John"; //没有设置set,什么也没发生
console.info(person.name); //Nicholas,

console.info(
  "----------------------不设set 结束--------------------------------"
);

console.info(
  "----------------------不设get 开始--------------------------------"
);

var person = { _name: "Nicholas" };

Object.defineProperty(person, "name", {
  set: function (newName) {
    console.info("set 被调用");
    this._name = newName;
  },
  enumerable: true,
  configurable: true,
});
console.info(person.name); //没有get,得到 undefined
console.info(person._name); //Nicholas
person.name = "John"; //set 被调用
console.info(person._name); //John,通过set,_name的值被改变

console.info(
  "----------------------不设get 结束--------------------------------"
);

console.info(
  "----------------------不设get set开始--------------------------------"
);
//虽然不报错,但是这个属性没有任何意义
var person = { _name: "Nicholas" };
Object.defineProperty(person, "name", {
  enumerable: true,
  configurable: true,
});
console.info(
  "----------------------不设get 结束--------------------------------"
);

console.info(
  "----------------------enumerable 开始--------------------------------"
);
var person = { _name: "Nicholas" };
Object.defineProperty(person, "name", {
  get: function () {
    console.info("get 被调用");
    return this._name;
  },
  set: function (newName) {
    console.info("set 被调用");
    this._name = newName;
  },
  enumerable: true,
  configurable: true,
});
for (var prop in person) {
  console.info(prop); //_name,name
}

Object.defineProperty(person, "name", {
  get: function () {
    console.info("get 被调用");
    return this._name;
  },
  set: function (newName) {
    console.info("set 被调用");
    this._name = newName;
  },
  enumerable: false,
  configurable: true,
});
for (var prop in person) {
  console.info(prop); //_name
}
console.info(
  "----------------------enumerable 结束--------------------------------"
);

console.info(
  "----------------------configurable 开始--------------------------------"
);
var person = { _name: "Nicholas" };
Object.defineProperty(person, "name", {
  get: function () {
    console.info("get 被调用");
    return this._name;
  },
  set: function (newName) {
    console.info("set 被调用");
    this._name = newName;
  },
  enumerable: true,
  configurable: false,
});

person.name; //get 被调用
person.name = "John"; //set 被调用
console.info(person.name); //John

//报错
try {
  Object.defineProperty(person, "name", {
    get: function () {
      console.info("get 被调用");
      return this._name;
    },
    set: function (newName) {
      console.info("set 被调用");
      this._name = newName;
    },
    enumerable: true,
    configurable: false,
  });
} catch (e) {
  console.info("不能重新定义name的属性标识符");
}

//报错
try {
  Object.defineProperty(person, "name", {
    value: "123",
    writable: true,
    enumerable: true,
    configurable: false,
  });
} catch (e) {
  console.info("不能重新定义name的属性标识符");
}

console.info(
  "----------------------configurable 结束--------------------------------"
);

总结

  1. writable 为 true,属性值就可以修改,无论是通过.运算符还是通过 Object.defineProperty 方法;
  2. writable 为 false,不能通过.运算符修改属性。但是在 configurable 为 true 的情况下可以通过通过 Object.defineProperty 方法重新设置 value 值,从而修改属性值;
  3. 只要 enumerable 为 true,属性就可枚举,为 false 则不可枚举;
  4. configurable 为 true 的情况下,可以对属性描述符对象进行任何修改;
  5. configurable 为 fasle 的情况下,可以通过 Object.defineProperty 把 writable 改为 true,在 writabe 为 true 的情况下,可以修改 value 的值。
  6. configurable 为 false 的情况下,除第 5 条的所述的情况外,不能通过 Object.defineProperty 修改属性的任何特性值。

不可变对象

ES5 定义了三个方法 Object.preventExtensions(不可扩展)、Object.seal(密封)、Object.freeze(冻结)分别定义了不同级别的可扩展性。

不可扩展

Object.preventExtensions() 可以使一个对象不可再添加新的属性,参数为目标对象,返回修改后的对象。

var obj = Object.preventExtensions({});

// 直接定义新的属性会报错
Object.defineProperty(obj, "content", {
  value: "hello",
}); // TypeError: Cannot define property:p, object is not extensible.

// 非严格模式下通过点符号添加不会报错,但会静默失败,原对象仍然没有 content 属性
obj.content = "hello";
obj.content; // undefined

对应的,Object.isExtensible() 可以判断一个对象是否可扩展,即是否可以添加新的属性。参数是目标对象。

密封

Object.seal() 可以使一个对象无法添加新属性的同时,也无法删除旧属性。参数是目标对象,返回修改后的对象。

其本质是通过修改属性的 configurable 为 false 来实现的。在属性描述对象里讲到,configurable 为 false 时,其他配置不可改变,writable 只能 true 变 false,且属性无法被删除。而由于只要 writable 或 configurable 其中之一为 true,则 value 可改,所以密封之后的对象还是可以改属性值的。

var obj = { content: "hello" };
Object.getOwnPropertyDescriptor(obj, "content");
// Object {
//   value: "hello",
//   writable: true,
//   enumerable: true,
//   configurable: true
// }
Object.seal(obj);
Object.getOwnPropertyDescriptor(obj, "content"); // seal 后 configurable 变为 false
// Object {
//   value: "hello",
//   writable: true,
//   enumerable: true,
//   configurable: false
// }

对应的,Object.isSealed() 可以检测一个对象是否密封,即是否可以增删属性。参数是目标对象,返回布尔值。

注意 seal 后对象的 isExtensible() 也随之改变

冻结

Object.freeze() 可以使对象一个对象不能再添加新属性,也不可以删除旧属性,且不能修改属性的值。参数是目标对象,返回修改后的对象。

var obj = Object.freeze({ name: "example" });

// 直接定义新的属性会报错
Object.defineProperty(obj, "content", {
  value: "hello",
}); // TypeError: Cannot define property:p, object is not extensible.

// 非严格模式下通过点符号添加不会报错,但会静默失败,原对象仍然没有 content 属性
obj.content = "hello";
obj.content; // undefined

delete obj.name; // 删除失败,返回 false

obj.name = "hello";
obj.name; // 仍然是 "example"

对应的,Object.isFrozen() 可以检测一个对象是否冻结,即是否可以增删改。参数是目标对象,返回布尔值。

注意 freeze 后对象的 isExtensible() 和 isSealed()

注意要点

无论是不可扩展,密封,还是冻结,都是浅层控制的,即只控制对象本身属性的增删改。如果对象属性是一个引用类型,则仍然可增删改。

var obj = Object.freeze({
  content: { name: "example" },
});
obj.content = new Object();
obj.content; // {name: "example"},content 本身不可改
obj.content.name = "test";
obj.content; // {name: "test"},但 content 的属性仍可改,因为冻结的是 obj 而不是 obj.content

所以需要循环处理所有情况

function deepFreeze(obj) {
  var propNames = Object.getOwnPropertyNames(obj);
  propNames.forEach(function (name) {
    var prop = obj[name];
    if (typeof prop == "object" && prop !== null) {
      deepFreeze(prop);
    }
  });
  return Object.freeze(obj);
}

由于每个对象都有一个属性 __proto__,该属性的值是该对象的原型对象,也是引用类型,由于冻结是浅层的所以原型对象并不会被连着冻结,仍然可以通过给对象的原型对象加属性达到给当前对象新增属性的效果。所以如果想进一步冻结还需要把原型对象也冻结上。

var obj = Object.freeze({});
obj.content = "hello";
obj.content; // undefined,增加失败

var proto = Object.getPrototypeOf(obj);
proto.content = "hello";
obj.content; // "hello",增加成功

Object.freeze(proto);
proto.name = "example";
obj.name; // undefined,冻结原型之后增加失败

get、set 与继承

function Person() {}

var p = Person.prototype;
p._name = "John";
Object.defineProperty(p, "name", {
  get: function () {
    return this._name;
  },
  set: function (newName) {
    this._name = newName;
  },
  enumerable: true,
  configurable: true,
});

var person = new Person();
console.info(person.name); //John
console.info(person.hasOwnProperty("_name")); //false

//虽然name属性的set get方法是定义在原型中的
//但是通过person调用时,它们的this属性会指向person
//所以通过person.name设置属性时,执行set方法中的this._name=newName时,
//会给person对象添加_name属性,并把newName赋给person新建的属性_name;
person.name = "Nicholas";
console.info(person.name); //Nicholas
console.info(p._name); //John
console.info(person.hasOwnProperty("name")); //false
console.info(person.hasOwnProperty("_name")); //true