前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版

Object.defineProperty详解与Vue2响应式原理

首页2018-12-23 09:40:12Front-End
JavaScriptObject.defineProperty响应式原理

来自网络

有次接手一份老代码,配置对象上的某个字段赋值怎么都不生效,console.log 打出来还是旧值,也不报错。找了很久才发现前面有人用 Object.defineProperty 把它定成了 writable: false,非严格模式下这种赋值是静默失败的,连个警告都没有。

Object.defineProperty 平时写业务确实用得少,但它是 JS 对象模型的底层入口。Object.freeze 怎么冻住对象、class 的方法为什么 for...in 遍历不到、Vue 2 的响应式是怎么做的,答案全在属性描述符这套机制里。这篇把四个数据描述符和存取器描述符逐个过一遍,再用它解释一遍 Vue 2 的响应式和它的短板。

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • 属性描述符解决了什么问题
  • value、writable、enumerable、configurable 各自管什么
  • 用 defineProperty 定义属性时那个「默认全是 false」的坑
  • 严格模式和非严格模式下违规操作的不同表现
  • get/set 存取器描述符的用法和它的互斥规则
  • Vue 2 的响应式为什么必须用它,以及三个绕不过去的短板
  • Proxy 是怎么把这些短板补上的
  • 配套的几个 API,getOwnPropertyDescriptor、defineProperties、freeze

# 一、简介

先说兼容性,在 IE8 下 Object.defineProperty 只能在 DOM 对象上使用,尝试在原生的对象上使用会报错。这条限制今天已经没有实际意义了,主流浏览器和 Node 都完整支持它,写它只是为了让你在读老代码里那些奇怪的兼容分支时知道来由。

定义对象可以使用构造函数或字面量的形式。

var obj = new Object;  //obj = {}
obj.name = "张三";  //添加描述
obj.say = function(){};  //添加行为
@前端进阶之旅: 代码已经复制到剪贴板

除了以上添加属性的方式,还可以使用 Object.defineProperty 定义新属性或修改原有的属性。

两者的差别不只是写法。用 obj.name = '张三' 这种赋值方式创建的属性,是可写、可枚举、可配置的,也就是一个「什么都能干」的普通属性。而 Object.defineProperty 允许你把这三个开关单独关掉,这才是它存在的理由。

# 二、Object.defineProperty()

# 2.1 定义

Object.defineProperty(obj, prop, descriptor)
@前端进阶之旅: 代码已经复制到剪贴板

参数说明:

  • obj 必需,目标对象
  • prop 必需,需定义或修改的属性的名字
  • descriptor 必需,目标属性所拥有的特性

返回值是传入函数的对象,即第一个参数 obj。注意它是原地修改,返回的就是那个对象本身,不是副本。

针对属性我们可以设置一些特性,比如是否只读不可以写,是否可以被 for...in 或 Object.keys() 遍历。

给对象的属性添加特性描述,目前提供两种形式,数据描述和存取器描述。这两种形式是互斥的,同一个属性只能选一种。

# 2.2 数据描述

当修改或定义对象的某个属性的时候,给这个属性添加一些特性。

var obj = {
    test:"hello"
}
//对象已有的属性添加特性描述
Object.defineProperty(obj,"test",{
    configurable:true | false,
    enumerable:true | false,
    value:任意类型的值,
    writable:true | false
});
//对象新添加的属性的特性描述
Object.defineProperty(obj,"newKey",{
    configurable:true | false,
    enumerable:true | false,
    value:任意类型的值,
    writable:true | false
});
@前端进阶之旅: 代码已经复制到剪贴板

数据描述中的属性都是可选的,来看一下设置每一个属性的作用。

# 2.2.1 value

属性对应的值,可以是任意类型的值,默认为 undefined。

var obj = {}
//第一种情况:不设置value属性
Object.defineProperty(obj,"newKey",{

});
console.log( obj.newKey );  //undefined
------------------------------
//第二种情况:设置value属性
Object.defineProperty(obj,"newKey",{
    value:"hello"
});
console.log( obj.newKey );  //hello
@前端进阶之旅: 代码已经复制到剪贴板

上面这两段用分隔线隔开了,是因为它们必须分别运行。第一种情况执行完之后 newKey 的 configurable 是 false,同一个 obj 上再调一次 defineProperty 会直接抛错。这个细节后面讲 configurable 的时候还会再遇到一次。

# 2.2.2 writable

属性的值是否可以被重写。设置为 true 可以被重写,设置为 false 不能被重写,默认为 false。

var obj = {}
//第一种情况:writable设置为false,不能重写。
Object.defineProperty(obj,"newKey",{
    value:"hello",
    writable:false
});
//更改newKey的值
obj.newKey = "change value";
console.log( obj.newKey );  //hello

//第二种情况:writable设置为true,可以重写
Object.defineProperty(obj,"newKey",{
    value:"hello",
    writable:true
});
//更改newKey的值
obj.newKey = "change value";
console.log( obj.newKey );  //change value
@前端进阶之旅: 代码已经复制到剪贴板

第一种情况里那次赋值就是我开头踩的坑。非严格模式下它悄无声息地失败,什么都不发生。开了严格模式('use strict')或者在 ES 模块里(模块默认严格模式),同样的代码会抛 TypeError: Cannot assign to read only property。

这个差别在实践中很重要。现在用打包工具的项目源码基本都是 ESM,天生严格模式,所以你反而更容易第一时间看到报错。真正难查的是那些跑在非严格模式下的老脚本。

# 2.2.3 enumerable

此属性是否可以被枚举(使用 for...in 或 Object.keys())。设置为 true 可以被枚举,设置为 false 不能被枚举,默认为 false。

var obj = {}
//第一种情况:enumerable设置为false,不能被枚举。
Object.defineProperty(obj,"newKey",{
    value:"hello",
    writable:false,
    enumerable:false
});

//枚举对象的属性
for( var attr in obj ){
    console.log( attr );  
}
//第二种情况:enumerable设置为true,可以被枚举。
Object.defineProperty(obj,"newKey",{
    value:"hello",
    writable:false,
    enumerable:true
});

//枚举对象的属性
for( var attr in obj ){
    console.log( attr );  //newKey
}
@前端进阶之旅: 代码已经复制到剪贴板

不可枚举不等于不存在。enumerable: false 的属性,for...in、Object.keys、JSON.stringify、展开运算符都拿不到它,但 obj.newKey 直接访问是有值的,Object.getOwnPropertyNames(obj) 也能列出来,'newKey' in obj 同样返回 true。

这个特性在语言内部用得很多。Array.prototype.push、Object.prototype.toString 这些内置方法全是不可枚举的,否则你 for...in 一个数组会把所有原型方法都遍历出来。class 里定义的方法默认也是不可枚举的,而 ES5 时代手动挂在 prototype 上的函数是可枚举的,这是两者一个容易被忽略的差别。

# 2.2.4 configurable

是否可以删除目标属性,或是否可以再次修改属性的特性(writable、configurable、enumerable)。设置为 true 可以被删除或可以重新设置特性,设置为 false 不能被删除也不可以重新设置特性,默认为 false。

这个属性起到两个作用:

  • 目标属性是否可以使用 delete 删除
  • 目标属性是否可以再次设置特性
//-----------------测试目标属性是否能被删除------------------------
var obj = {}
//第一种情况:configurable设置为false,不能被删除。
Object.defineProperty(obj,"newKey",{
    value:"hello",
    writable:false,
    enumerable:false,
    configurable:false
});
//删除属性
delete obj.newKey;
console.log( obj.newKey ); //hello

//第二种情况:configurable设置为true,可以被删除。
Object.defineProperty(obj,"newKey",{
    value:"hello",
    writable:false,
    enumerable:false,
    configurable:true
});
//删除属性
delete obj.newKey;
console.log( obj.newKey ); //undefined
@前端进阶之旅: 代码已经复制到剪贴板

这两段代码同样要分开跑,原文没说清楚这点。放在一起顺序执行的话,第二次 defineProperty 会因为上一次设了 configurable: false 而直接抛 TypeError: Cannot redefine property,根本走不到 delete 那行。

delete 失败的表现也分模式。非严格模式下 delete obj.newKey 返回 false 然后什么都不发生,严格模式下直接抛 TypeError。

fe
  • 一、简介
  • 二、Object.defineProperty()
    • 2.1 定义
    • 2.2 数据描述
      • 2.2.1 value
      • 2.2.2 writable
      • 2.2.3 enumerable
      • 2.2.4 configurable
    • 2.3 默认值这个坑
    • 2.4 存取器描述
      • 2.4.1 定义
      • 2.4.2 getter/setter
  • 三、Vue 2 的响应式就是这么做的
    • 3.1 三个绕不过去的短板
    • 3.2 Proxy 是怎么补上的
  • 四、配套的几个 API
  • 总结
  • 参考

← nodejs系列之Koa2 洋葱模型与中间件实战JavaScript复制粘贴与剪贴板操作详解 →