Skip to content

ECMAScript 总结与对象的属性特性(Day4 2/4) ​

本页对应 Day04_ES6&Git,共 6 段视频,约 46 分钟。

学习目标 ​

  • 能够使用 Object.getOwnPropertyDescriptor() 和 Object.getOwnPropertyDescriptors() 查看对象自身属性的 value、writable、enumerable、configurable、get、set 等特性,并区分数据属性与访问器属性。
  • 能够使用 Object.defineProperty() 修改或新增单个属性的特性,并验证不可枚举属性不出现在 for...in 中、不可写属性赋值无效、不可配置属性不能被 delete 删除。
  • 能够使用 Object.defineProperties() 一次性为对象添加多个属性,并分别设置数据属性和访问器属性的特性。
  • 能够使用 Object.create(proto, descriptors) 创建指定原型的新对象,并在第二个参数中为新增属性设置特性。
  • 能够在 class 中通过 get/set 定义访问器属性,并配合私有或内部数据属性实现赋值校验与读取处理。

知识点 ​

从 for...in 少了一个 length 说起 ​

为什么需要:对象属性不只是“键值对”,还带有可枚举、可配置、可写等特性;不理解这些特性,就解释不了 for...in 为什么看不到数组的 length。

怎么想:先用 for...in 遍历普通对象和数组,对比输出;再单独查看 length 的描述信息,发现它的 enumerable 是 false。

怎么做:

js
const arr = [100, 200];
arr.firstName = '高';
arr.lastName = '乐';

const user = {
  name: '曹操',
  age: 18, // 示例值
  address: '上海',
  say() {}
};

for (const key in user) {
  console.log(key, user[key]);
}

for (const key in arr) {
  console.log(key, arr[key]);
}

console.log(Object.getOwnPropertyDescriptor(arr, 'length'));

调试时会发现:user 的 name、age、address、say 都能被 for...in 枚举;arr 能枚举 0、1、firstName、lastName,却没有 length。原因是 length 被设置成了不可枚举。这里说的是 for...in 的可枚举,不是 for...of。

易错点:for...in 会受原型链上可枚举属性影响;length 不可枚举,不代表它不存在。

用 getOwnPropertyDescriptor 查看属性特性 ​

为什么需要:属性的特性默认看不见,必须用方法读取,才能知道一个属性能不能枚举、能不能删除、能不能改。

怎么想:单个属性用 Object.getOwnPropertyDescriptor(obj, '属性名');一次看全部用 Object.getOwnPropertyDescriptors(obj)。它只取对象自身属性,不查原型链。

怎么做:

js
console.log(Object.getOwnPropertyDescriptor(user, 'address'));
console.log(Object.getOwnPropertyDescriptor(user, 'say'));
console.log(Object.getOwnPropertyDescriptors(arr));

普通数据属性会看到 value、writable、enumerable、configurable。默认添加的属性通常是可写、可枚举、可配置的。arr.length 则是 writable: true,但 enumerable: false、configurable: false。

易错点:想看原型上的属性,要先把原型对象拿出来再查;不要以为 getOwnPropertyDescriptor 会沿原型链查找。

数据属性:用 defineProperty 改特性 ​

为什么需要:有时属性要只读、不可删、不可枚举,默认的“都能改”不够用。

怎么想:数据属性有四个特性:configurable 决定能否删除/改特性,enumerable 决定能否被 for...in 枚举,writable 决定能否改值,value 是值。Object.defineProperty 对已存在属性是修改,对不存在属性是添加。

怎么做:

js
const person = { name: '高小乐', address: '上海' };

Object.defineProperty(person, 'address', {
  configurable: false,
  enumerable: false,
  writable: false,
  value: '北京'
});

person.address = '深圳'; // 改不了
delete person.address;   // 删不掉
for (const key in person) console.log(key); // 不出现 address

再添加 age 时,可以设置成不可写、不可删但可枚举,验证 person.age = 200 不生效,而 for...in 能看到它。

易错点:configurable: false 后不能再改该属性特性;defineProperty 新属性未写的特性默认是 false,和普通赋值添加属性不同。

访问器属性:get/set 做拦截 ​

为什么需要:赋值和读取时可能需要处理,例如 window.name 赋对象会自动转成字符串,这就是访问器属性在起作用。

怎么想:访问器属性没有 value/writable,而是有 get/set。读取时调用 get,赋值时调用 set,并把赋的值传给 set。

怎么做:

js
const p = { name: '高小乐', _age: 0 };

Object.defineProperty(p, 'age', {
  enumerable: true,
  configurable: true,
  get() {
    return this._age;
  },
  set(value) {
    value = +value;
    if (value >= 0 && value <= 200) {
      this._age = value;
    }
  }
});

p.age = 200; // 生效
p.age = 400; // 不生效

调试时在 get 里打印 this,会发现它是 p;在 set 里打印参数,会发现参数就是赋进来的值。window.name 的 descriptor 会出现 get/set,所以它能做转换处理。

易错点:set 里要写回内部属性;不要用箭头函数导致 this 不对;get 不要返回固定值冒充真实数据。

defineProperties 与 Object.create ​

为什么需要:一次添加多个属性并设置特性,或创建指定原型的新对象。

怎么想:Object.defineProperties(obj, 描述集合) 批量定义;Object.create(原型, 描述集合) 创建新对象并定义属性。

怎么做:

js
const u = {};
Object.defineProperties(u, {
  address: { value: '上海', enumerable: false, configurable: false, writable: false },
  age: {
    enumerable: false,
    configurable: true,
    get() { return this._age; },
    set(value) { value = +value; if (value >= 0 && value <= 200) this._age = value; }
  }
});

const fakeArr = Object.create(Array.prototype, {
  0: { value: '北京', enumerable: false, configurable: false, writable: false },
  1: { value: '上海', enumerable: true, configurable: true, writable: true }
});

易错点:set 用普通函数;Object.create 出来的对象只是原型指向数组原型,不要和真正数组字面量混用。

class 中定义访问器属性 ​

为什么需要:类实例也需要读写拦截,配合私有字段更自然。

怎么想:class 里普通字段是数据属性,get/set 定义的是访问器属性;set 必须接收参数。

怎么做:

js
class Product {
  name = '高小乐';
  address = '上海';
  #h = 0;

  get age() {
    return this.#h;
  }

  set age(value) {
    value = +value;
    if (value >= 0 && value <= 200) {
      this.#h = value;
    }
  }
}

const prod = new Product();
prod.age = 124; // 生效
prod.age = 400; // 不生效

易错点:set 忘记写参数会报错;私有字段要先初始化;class 中 get/set 不是普通方法调用,而是属性访问时自动触发。

为什么属性也有“特性” ​

先看一个现象:数组明明有 length,但 for...in 遍历时却看不到它。

js
const arr = [100, 200];
arr.firstName = '高';
arr.lastName = '乐';

for (const key in arr) {
  console.log(key, arr[key]);
}
// 0 100
// 1 200
// firstName 高
// lastName 乐
// 没有 length

为什么需要理解这件事?因为对象属性不只有“键”和“值”,它背后还有一组描述信息,决定这个属性能不能枚举、能不能改、能不能删。怎么想?不要先背概念,先从现象反推:length 能读,但 for...in 不出来,说明“存在”不等于“可枚举”。怎么做?用 Object.getOwnPropertyDescriptor(obj, key) 读取某个自有属性的描述符。

js
console.log(Object.getOwnPropertyDescriptor(arr, 'length'));

易错点:getOwnPropertyDescriptor 只看对象自身属性,不查原型链;原型上的属性要先拿到原型对象再查。

读取描述符:看清默认行为 ​

普通添加的属性,默认通常可枚举、可配置、可写。比如:

js
const user = {
  name: '曹操',
  age: 20,
  address: '上海',
  say() {}
};

console.log(Object.getOwnPropertyDescriptor(user, 'address'));
// { value: '上海', writable: true, enumerable: true, configurable: true }

这里要纠正几个同音字:转写稿里的“可辩利”是“可枚举”,“和谐性/可写性”对应 writable,“可配置”对应 configurable,“制毒”是“只读”。怎么想?描述符返回一个对象,数据属性里会看到 value、writable、enumerable、configurable。易错点:不要把它和 for...of 混在一起,这里说的“可枚举”主要影响 for...in 和 Object.keys 等枚举行为。

数据属性:值、可写、可枚举、可配置 ​

为什么需要 Object.defineProperty?因为直接赋值只能改值,改不了“能不能删、能不能枚举、能不能写”。怎么做?

js
const person = {
  name: '高小乐',
  address: '上海'
};

Object.defineProperty(person, 'address', {
  configurable: false,
  enumerable: false,
  writable: false,
  value: '北京'
});

person.address = '深圳';
console.log(person.address); // 北京
delete person.address;       // 删不掉

调试时可以看到:值已被改成“北京”;再赋“深圳”无效,因为 writable:false;delete 无效,因为 configurable:false;for...in 也遍历不到,因为 enumerable:false。易错点:一旦把 configurable 设为 false,后续再想通过 defineProperty 改回可配置通常会报错;另外,defineProperty 对已有属性是修改,对不存在的属性是添加。

访问器属性:get 和 set ​

为什么需要访问器属性?因为有时赋值不是“直接塞进去”,而是要先校验、转换。比如 window.name 被赋一个对象,读出来却变成字符串,这说明它内部走了 set 处理。访问器属性没有 value/writable,而是有 get/set。

js
const person = { name: '高小乐' };
person._age = 0;

Object.defineProperty(person, 'age', {
  configurable: true,
  enumerable: true,
  get() {
    return this._age;
  },
  set(val) {
    val = Number(val);
    if (val >= 0 && val <= 200) {
      this._age = val;
    }
  }
});

person.age = 200;
console.log(person.age); // 200
person.age = 400;
console.log(person.age); // 200

怎么想?读 person.age 调 get,写 person.age = ... 调 set,传入值就是 set 的形参。易错点:set 不要用箭头函数,箭头函数没有自己的 this,拿不到当前实例;内部常配一个 _age 这类普通数据属性保存真实值。

批量定义与 Object.create ​

为什么还需要批量方法?因为一次只定义一个属性太啰嗦。Object.defineProperties 可以一次添加/修改多个属性;Object.create 的第二个参数也能在创建新对象时直接设置属性特性。

js
const user = {};

Object.defineProperties(user, {
  address: {
    configurable: false,
    enumerable: false,
    writable: false,
    value: '上海'
  },
  age: {
    configurable: true,
    enumerable: true,
    get() {
      return this._age;
    },
    set(val) {
      val = Number(val);
      if (val >= 0 && val <= 200) this._age = val;
    }
  }
});

易错点:第二个参数对象的每个键都是属性名,值才是描述符对象;访问器属性不要再写 value/writable。

class 中定义访问器 ​

为什么 class 里也要讲?因为实例属性也可以用 get/set 包一层校验,并配合私有字段。

js
class Product {
  #age = 0;
  constructor() {
    this.name = '高';
    this.address = '上海';
  }
  get age() {
    return this.#age;
  }
  set age(val) {
    val = Number(val);
    if (val >= 0 && val <= 200) {
      this.#age = val;
    }
  }
}

const p = new Product();
p.age = 124;
console.log(p.age); // 124
p.age = 400;
console.log(p.age); // 124

易错点:get age() 和 set age(val) 定义的是访问器属性,不是普通方法;#age 是私有字段,外部不能直接访问,正好配合访问器做受控读写。

先厘清本节主线 ​

本节标题虽沿用“Git 课堂笔记”,但转写内容实际围绕 JavaScript 对象属性的特性展开:为什么 for...in 遍历数组时看不到 length?为什么给 window.name 赋一个对象,最后却变成字符串?这些现象都指向同一件事——属性不只是“键值对”,它还有可枚举、可配置、可写等特性。学习路径是:先观察现象,再用描述符查看特性,最后用 defineProperty、defineProperties、Object.create 和 class 去控制特性。

用现象引出属性特性 ​

为什么需要:只把对象理解成“属性名 + 值”不够,遇到“遍历不到”“赋值不生效”“自动转字符串”就会解释不了。怎么想:先写最小例子观察。怎么做:

js
const arr = [100, 200];
arr.firstName = '高';
arr.lastName = '乐';

const user = {
  name: '曹操',
  age: 18,
  address: '上海',
  say() {}
};

for (const key in user) {
  console.log(key, user[key]);
}

for (const key in arr) {
  console.log(key, arr[key]);
}
console.log(arr);

调试时会发现:user 的 name、age、address、say 都能被 for...in 枚举;数组能枚举 0、1、firstName、lastName,却看不到 length。原因是 length 不可枚举。注意:这里的“可枚举”指 for...in,不是 for...of;原型上的属性不是自身属性,本来就不该被当作自身属性遍历。

另一个现象是 window.name:给它赋对象,读出来却变成字符串。调试时可查看它的描述符,会发现它不是普通数据属性,而是带 get/set 的访问器属性,赋值时 setter 对值做了处理。

查看属性描述符 ​

为什么需要:要控制特性,先要看见特性。怎么想:用静态方法读取“自身属性”的描述符。怎么做:

js
Object.getOwnPropertyDescriptor(user, 'address');
Object.getOwnPropertyDescriptors(user);
Object.getOwnPropertyDescriptor(arr, 'length');

getOwnPropertyDescriptor 返回单个属性的描述信息;getOwnPropertyDescriptors 返回所有自身属性的描述符。数据属性里常见 value、writable、enumerable、configurable;访问器属性里则是 get、set、enumerable、configurable。易错点:它只取自身属性,原型链上的属性要拿原型对象再查;访问器属性没有 value/writable,不要混用。

数据属性与 defineProperty ​

为什么需要:有时要禁止删除、禁止改写、禁止遍历。怎么想:数据属性有四个特性:configurable 决定能否 delete、能否再修改特性;enumerable 决定能否被 for...in 枚举;writable 决定能否写入;value 是值。怎么做:

js
const person = { name: '高小乐', address: '上海' };

Object.defineProperty(person, 'address', {
  configurable: false,
  enumerable: false,
  writable: false,
  value: '北京'
});

person.address = '深圳'; // 不生效
delete person.address;   // 删不掉

也可以用它添加新属性并设置特性。易错点:writable:false 后普通赋值不生效;configurable:false 后不能删除,也不能随意再改特性;enumerable:false 后 for...in 看不到它。

访问器属性 get/set ​

为什么需要:赋值和读取时可能要校验、转换、联动内部值,类似 window.name 的处理。怎么想:访问器属性不直接存值,而是一对函数:读取走 get,写入走 set,set 的参数就是赋进来的值。怎么做:

js
const person = { name: '高小乐', address: '上海' };
person._age = 0;

Object.defineProperty(person, 'age', {
  enumerable: true,
  configurable: true,
  get() {
    return this._age;
  },
  set(val) {
    val = Number(val);
    if (val >= 0 && val <= 200) {
      this._age = val;
    }
  }
});

person.age = 200;
person.age = 400; // 不生效

调试时在 getter 里打印 this,会发现它就是 person;在 setter 里打印参数,能确认 val 是赋进来的值。易错点:getter 要 return;setter 不写内部属性时,赋值看似执行了但值没保存;需要 this 时不要随手用箭头函数。

批量定义与 Object.create ​

为什么需要:一次添加多个属性、创建新对象并指定原型和特性。怎么做:

js
const user = { name: '高小乐', address: '上海' };
user._age = 0;

Object.defineProperties(user, {
  address: {
    enumerable: false,
    configurable: false,
    writable: false,
    value: '上海'
  },
  age: {
    enumerable: false,
    configurable: true,
    get() { return this._age; },
    set(val) {
      val = Number(val);
      if (val >= 0 && val <= 200) this._age = val;
    }
  }
});

const obj = Object.create(Array.prototype, {
  0: { value: '北京', enumerable: false, configurable: false },
  1: { value: '上海', enumerable: true, configurable: true }
});

易错点:defineProperties 第二个参数是“属性名 → 描述符对象”的映射;Object.create 第一个参数是新对象的原型,第二个参数才是属性描述符。

class 中定义访问器属性 ​

为什么需要:用 class 创建实例时,也能在读取/赋值时加一层处理。怎么做:

js
class Product {
  name = '高小乐';
  address = '上海';
  #age = 0;

  get age() {
    return this.#age;
  }

  set age(val) {
    val = Number(val);
    if (val >= 0 && val <= 200) {
      this.#age = val;
    }
  }
}

const p = new Product();
p.age = 124;
p.age = 400; // 不生效

普通字段 name、address 是数据属性;get age/set age 定义的是访问器属性,配合私有字段 #age 很合适。易错点:get/set 要成对理解;set 必须接参数;class 里的访问器属性是给实例用的,不是给类本身用的。

本节要解决的问题:为什么需要 Git ​

先明确本节笔记的核心结论:Git 是一款开源免费的分布式版本控制系统,由 Linux 之父 Linus Torvalds(林纳斯·托瓦兹)为了方便管理 Linux 代码而开发。

为什么需要它?因为写代码不是“一次写完就结束”的事情。同一份代码会不断被修改、试错、推翻、重做;多人参与时,还会出现“谁改了哪里”“改坏了能不能退回去”“哪些人能提交、哪些人只能看”等问题。仅靠手动复制文件夹,很快就会变成“项目最终版”“项目最终版2”“项目真的最终版”这样的混乱局面。Git 要解决的就是这类问题:把代码的每次变化管理起来,让修改可追踪、可回退、可协作。

这里的学习顺序应当是:先知道 Git 是什么、能做什么,再进入具体命令。不要一上来死记命令,否则容易变成“会敲但不知道为什么敲”。

怎么想:版本控制系统与“分布式” ​

“版本控制系统”可以理解为专门记录文件变化历史的工具。它的关注点不是“当前文件长什么样”,而是“文件从哪个状态变到了哪个状态”。这正是后面能实现版本回退的基础。

“分布式”是 Git 的重要特点。笔记只给出了这个定性,没有展开命令细节,因此这里不额外编造操作流程。你只需要先建立一个基本认识:Git 不是单纯把文件传到某个中心位置就结束,而是围绕版本历史进行管理。理解这一点,后面学习仓库、提交、分支等内容时,才不会把 Git 当成普通网盘。

为什么先强调“开源免费”?因为它降低了学习和使用门槛,也是它被广泛采用的重要原因。为什么强调“Linux 之父开发”?这是 Git 的出身背景:它一开始就是为了管理大型代码项目而生,目标非常明确——解决真实开发中的版本管理问题。

怎么做:认识 Git 的四个功能 ​

笔记列出了 Git 可以实现的功能,可以逐条理解:

  1. 代码备份:不是简单复制一份文件,而是把代码的历史状态保存下来。这样即使当前改坏了,也有历史版本可查。
  2. 版本回退:当新修改出现问题,可以回到之前的版本。这是“版本控制”最直观的价值之一。
  3. 多人协作:多人同时开发时,需要有一种机制管理各自的修改,避免互相覆盖。Git 的协作能力正是建立在其版本管理能力之上的。
  4. 权限控制:在团队中,不同成员对代码的读写、提交权限可能不同,Git 相关平台和流程可以配合实现权限管理。

这四点不需要死背,可以按“个人开发”和“团队开发”来记:前两点主要解决个人修改的安全感,后两点主要解决团队协作的秩序问题。

怎么做:获取 Git 与查文档 ​

笔记给出了两个地址,建议直接保存:

Git 工具下载地址:

https://git-scm.com/

Git 官方文档地址:

https://git-scm.com/book/zh/v2

下载地址用于获取 Git 工具;官方文档用于查证概念和命令。学习时不要只依赖二手笔记,遇到“某个说法是否准确”“某个命令参数是什么”时,应回到官方文档核对。尤其是后面进入具体操作阶段,文档比记忆更可靠。

易错点与学习建议 ​

第一,不要把“版本控制”理解成“文件备份”。备份只是结果之一,Git 的核心是管理版本历史。

第二,不要把“分布式”当成一句口号背下来。本节只需要先记住这个定性,具体含义要结合后续仓库操作再理解,避免现在过度展开。

第三,不要跳过“Git 能做什么”直接背命令。先知道代码备份、版本回退、多人协作、权限控制分别对应什么问题,后面学习命令时才知道每个命令服务于哪个目标。

第四,下载和文档地址要以官方站点为准,不要随意使用来源不明的安装包。

关于转写稿的说明 ​

本节提供的转写稿内容实际在讲 JavaScript 对象属性的特性,包括数据属性、访问器属性、Object.defineProperty、Object.defineProperties、Object.create 以及 class 中定义访问器属性等内容,与“Git 介绍”这一节笔记不匹配。为避免把无关内容硬塞进 Git 教程、造成知识错误,本教程未采用该转写稿中的推导与调试过程,仅依据本节笔记扩写。后续如果补充 Git 命令操作,应再提供对应的 Git 转写稿,才能按要求补出推导和调试细节。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>数据属性特性</title>
</head>
<body>
<script>
  const person = {
    name: '高小乐',
    address: '上海'
  };

  // 修改 address 属性特性,并把值改为“北京”
  Object.defineProperty(person, 'address', {
    configurable: false,
    enumerable: false,
    writable: false,
    value: '北京'
  });

  // 添加 age 属性并设置特性
  Object.defineProperty(person, 'age', {
    configurable: false,
    enumerable: true,
    writable: false,
    value: 190
  });

  console.log(person.address); // 北京
  person.address = '深圳';      // 不可写,赋值无效
  console.log(person.address); // 北京
  console.log(delete person.address); // false,不可配置

  person.age = 200;            // 不可写,赋值无效
  console.log(person.age);     // 190

  for (const key in person) {
    console.log(key, person[key]);
  }
</script>
</body>
</html>

要点解释:

  • 关键行:Object.defineProperty(person, 'address', {...}) 可以修改已有属性特性,也可以添加新属性并设置特性。
  • 关键行:writable: false 表示属性值不可写,非严格模式下赋值会静默失败。
  • 关键行:enumerable: false 影响 for...in 枚举,address 不会出现在遍历结果中。
  • 关键行:configurable: false 表示不能用 delete 删除,也不能再随意修改该属性特性。
  • 易错点:configurable、enumerable、writable、value 拼写和大小写要正确。
  • 易错点:不可写属性赋值不会报错但不会生效;delete 不可配置属性返回 false。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>访问器属性</title>
</head>
<body>
<script>
  const person = {
    name: '高小乐',
    address: '上海',
    _age: 0
  };

  Object.defineProperty(person, 'age', {
    configurable: true,
    enumerable: true,
    get() {
      console.log('get 中的 this:', this);
      return this._age;
    },
    set(val) {
      val = +val; // 先转成数字
      if (val >= 0 && val <= 200) {
        this._age = val;
      }
    }
  });

  console.log(person.age); // 0
  person.age = 200;
  console.log(person.age); // 200
  person.age = 400;        // 超出范围,不写入
  console.log(person.age); // 200
  person.age = '128';      // 转成数字 128
  console.log(person.age); // 128
  person.age = [1, 2];     // +[1, 2] 为 NaN,不写入
  console.log(person.age); // 128
</script>
</body>
</html>

要点解释:

  • 关键行:访问器属性使用 get / set,不再使用 value / writable。
  • 关键行:读取 person.age 时调用 get,返回 this._age;写入时调用 set,传入的值由参数 val 接收。
  • 关键行:val = +val 先把传入值转成数字,再做 0 ~ 200 范围判断。
  • 关键行:_age 是普通数据属性,用来和访问器属性 age 配合存储内部值。
  • 易错点:get 中要 return,否则读取到 undefined。
  • 易错点:set 必须写参数,否则拿不到赋进来的值。
  • 易错点:不要在 get / set 中使用箭头函数,否则 this 可能不是当前对象。
  • 易错点:+[1, 2] 结果是 NaN,条件不成立,所以不会写入。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>class 中定义访问器属性</title>
</head>
<body>
<script>
  class Product {
    name = '高小乐';
    address = '上海';
    #age = 0;

    get age() {
      return this.#age;
    }

    set age(val) {
      val = +val;
      if (val >= 0 && val <= 200) {
        this.#age = val;
      }
    }
  }

  const p = new Product();
  console.log(p.age); // 0
  p.age = 124;
  console.log(p.age); // 124
  p.age = 400;        // 超出范围,不写入
  console.log(p.age); // 124
</script>
</body>
</html>

要点解释:

  • 关键行:class 中可以直接定义 get age() 和 set age(val),给实例提供访问器属性。
  • 关键行:#age = 0 是私有字段,内部存储真实值,外部不能直接访问 p.#age。
  • 关键行:set age(val) 中同样先 val = +val,再做 0 ~ 200 范围校验。
  • 关键行:name、address 是普通实例数据属性,age 是访问器属性。
  • 易错点:私有字段 #age 必须在类体中声明,不能直接在外面写 p.#age。
  • 易错点:set 方法必须带参数,否则无法接收赋值。
  • 易错点:访问器属性要通过 p.age 读写,不要直接操作私有字段。

常见错误 ​

  1. 现象:用 for...in 遍历数组看不到 length,误判数组属性异常。原因:length 是不可枚举属性。修法:用 Object.getOwnPropertyDescriptor(arr, 'length') 验证;遍历数组优先用 for...of、forEach 或普通 for。
  2. 现象:给 window.name 赋对象,读出来却是字符串。原因:window.name 是访问器属性,setter 内部做了转换。修法:先查描述符确认 get/set,不要假设赋值就是原样存储。
  3. 现象:用 Object.defineProperty 新建属性后,属性不能改、不能删、也遍历不到。原因:新建属性时未指定的特性默认是 false。修法:明确写出 writable、enumerable、configurable,或先普通赋值再调整特性。
  4. 现象:在 set age(val) { this.age = val } 中赋值导致栈溢出。原因:setter 内又触发同名访问器,形成递归。修法:用内部字段存储,如 _age 或 #age,get/set 只操作内部字段。
  5. 现象:get/set 里的 this 不是实例,读写失败。原因:用箭头函数定义访问器,箭头函数没有自己的 this。修法:改用普通函数或 class 方法定义 get/set。
  6. 现象:delete obj.prop 返回 false 或严格模式报错,属性删不掉。原因:该属性 configurable: false。修法:创建时设 configurable: true,或不要依赖 delete 删除不可配置属性。
  7. 现象:用 Object.getOwnPropertyDescriptor(obj, 'x') 查不到原型链上的 x。原因:该方法只读取对象自身属性。修法:显式传入原型对象查询,或沿原型链逐层检查。

练习 ​

  1. 给定一个数组和若干自定义属性,分别用 for...in、Object.keys、Object.getOwnPropertyDescriptor(s) 输出结果,并解释差异;至少验证 length 和自定义属性的可枚举性。
  2. 用 Object.defineProperty 给对象添加一个属性:只读、不可枚举、不可配置,并写出验证代码,分别验证赋值、delete、for...in 三种行为。
  3. 实现 age 访问器属性:setter 要求先转数字,且只接受 0~200;getter 返回内部值。分别用 Object.defineProperty 和 class 两种写法完成。
  4. 用 Object.create 创建一个以数组原型为原型的新对象,添加索引属性 0/1 和一个访问器属性,并验证输出结果与属性特性。
  5. 给出下面错误代码的修复方案并说明触发点:set age(v) { this.age = v };再给一个用箭头函数写 get/set 导致 this 错误的案例。

原视频对照 ​

#视频时长
1对象的属性的特性15:23
2数据属性07:35
3访问器属性09:11
4添加多个属性并设置属性的特性04:04
5创建新对象添加属性并设置特性04:38
6class中定义访问器属性05:13

小结 ​

  1. 属性行为由描述符控制:数据属性关注 value/writable/enumerable/configurable,访问器属性关注 get/set/enumerable/configurable。
  2. for...in 只列出可枚举属性名,数组的 length 默认不可枚举,因此不会出现在结果中。
  3. Object.defineProperty/defineProperties 适合精确控制属性特性;新建属性时未指定的特性默认为 false。
  4. 访问器属性把读写变成函数调用,适合做校验、转换和派生值,不应直接读写同名属性。
  5. getter/setter 内应配合内部字段或私有字段;不要用箭头函数定义访问器。
  6. class 中的 get/set 定义在原型上、服务实例,常与私有字段配合实现受控访问。
  7. Object.create 可指定新对象原型,并可通过第二参数同时定义属性及其特性。
  8. 属性描述符查询默认只针对自身属性,原型链上的属性需要显式处理。