前言: 这两个属性在学习前端的时候看到过,但是由于项目中没有用到过,所以一直没有细致的了解。今天 review 同事代码的时候,遇到了这个写法,看了半天也不知道如何处理。再不学习真的以后连别人的代码都不知道什么意思了。而后经过查阅 MDN 以后,颠覆了自己对 js 的基础知识 ---“对象(object)” 的认知,并由此深感自己的不足。故今天来做一个简单总结,讲给同样在学习路上的你。
tips: 如果你是 react 开发,你可以选择直接跳跃至标题二开始阅读。如果你是 vue 开发,我强烈建议你从标题一开始阅读,你会更加有代入感的阅读本文。
因为作者主要是 vue 开发,本文的由来就是阅读同事的 vue 代码有感而作,但是也请 react 开发的同学不要害怕,没有标题一也并不会影响你阅读本文的整体感受🎁。
一. 初次相遇的场景
- 第一次遇到这两个名词的场景,是在学习 Vue3 教程的过程中,在看到 computed 的用法时,看到了下面这样一段描述:
原文链接:vue3 Computed 讲解 - 回顾一下我们在 vue3 中 computed 常用的写法。我在项目中最常用的方法就是给 computed 传递一个回调函数, 这个回调函数返回值就是这个计算属性的值。
- 随着写的项目越来越多,我逐渐形成了一个惯性思维,好像 computed 就是这样“仅此而已”。其实不然,它还有更进阶的用法,接下来让我们继续慢慢理解。
- 相信大家一定理解下面的代码为什么会报错。
- 因为我们上面是 computed 的基础用法,这样用其实你只给这个 name 这个属性设置了 getter。这样就导致了你这个 name 属性只能读取,而不能改写(或者叫做重新赋值)。
- 听到这里,你可能和最开始的我一样困惑。什么 getter? 我连单词 get 都没看见,你就在这里自言自语 getter,别急,一步一个脚印慢慢来。
- 仔细看我们上面的写法,我们如果只给 computed 函数一个函数作为参数。如下图:
那么其实上面的写法等价于下面的写法(tips:暂时忽略报错,这里的报错不是重点) - 你可能更加好奇了,什么鬼,从哪里凭空冒出来一个 get? 为啥这样写就不能重新赋值了?在此之前你必须更加深入了解 object 这个类型。
二. object 属性的定义方法
- 这里我准备了一个空对象,现在我想让你给这个 obj 赋予一个叫做 name 的属性。值为字符串类型的 “韩振方”。你会怎么做?
- 我觉得你甚至不需要思考,条件反射的都可以写出下面的代码。
- 你要知道,其实这一步你是在完成一个对 obj 的属性描述过程。
- 让我们完整的回顾上面的过程:你刚刚给 obj 这个对象添加了一个属性叫做 ‘name’,并且这个 name 的 值(value) 是一个叫做 “韩振方” 的字符串。
- 接下来我将告诉你的是,在你
obj.name=“韩振方”
的时候,你其实间接的调用了 Object 原型身上的 defineProperty 方法。
三. Object.defineProperty
- 从 MDN 上查阅可知,这个函数有三个参数。
- 让我换一种方法,重新写
obj.name=“韩振方”
这段代码,那么它其实等价于Object.defineProperty(obj,"name",{value:"韩振方",...})
注意! 上面的代码不严谨,它省略了一部分内容。我只是想通过上面引出我们接下来要讲解十分重要的知识点属性描述符。 后面我会慢慢补充省略的内容。 - 由上面代码我们可以知道这个函数的基本用法,接收3个参数,第一个参数是要添加属性的对象(obj),第二个参数是要添加的属性名称(name),关键点是第三个属性,这个属性是一个对象类型的参数。我们的重点是搞清楚这第三个参数都有什么选项。
- 由于篇幅限制,在本文中,我们暂时忽略“enumerable”和“configurable”这两个属性。我们重点看下面这几个属性。
- 在讲解下面的知识之前,我想再强调一下,第三个参数属性描述符 是一个对象类型,
{}
它有且只有一些固定的键值对。它用来约束这个即将要定义的属性的一些行为。
四. value 和 writable
- value,我相信这个选项非常非常容易理解,就是你给这个属性即将赋予的值。
- 读懂了下面带红线的句子,你应该就明白了一个没有赋值的属性是为什么值是
undefined
的了吧? - writable 是否可写,这里可写说白了就是是否可以重新被赋值。
- 强调一下这里 writbale 默认值不是我们想象中的 true 而是 false。
- 回顾我们上面的代码。
由于我们没有定义 writable ,所以按道理来讲它是不可重新赋值的。 让我们验证一下:
不出所料,控制台报错了,并且报错信息和猜测的一样,不能给只读属性赋值。 - 让我们设置 writable 为 true 再重新尝试一下。
可以看到控制台的错误没有了,并且正确输出了修改过后的值。 - 别忘了,刚刚我们重新赋值的代码,
obj.name="小韩"
这一步本质上还是在重复使用 Object.defineProperty 这个函数。正好也对应了 MDN 的这段解释。
五. getter 和 setter
- 标题的内容终于到了,其实 getter 和 setter 并没有那么难理解。
- 首先让我们搞明白一个过程。下面的代码是在控制台输出 obj 的 name 属性的值。对吧?
- 其实你的这个动作 obj.(注意有个点),obj点 的过程是在“读取” obj 对象的 name 属性。注意这个 “读取” 的动作。这个读取其实就是对应了获取 value 的过程。
- 这个动作正好就是 getter 要做的行为。首先别看叫 getter 就很害怕,它其实就是属性描述符的一个属性 get 而已,仅此而已。只不过这个属性的值是一个函数。起了个外国人名字,加了个 er 叫起来顺口而已。
- 什么?有点绕?还不懂?一个普通对象,有一个属性,属性值是一个函数。像下面,一个对象 hanzhenfang ,有一个属性叫做 skill,值是一个函数,能够被执行,执行后在控制台输出一个哈哈。
怎么我这样写你就能明白,换个说法就不明白了呢? - 回到 getter,我想你可能马上想到 getter 的用法应该像下面这样。
对不起,这样是不允许的,因为 get 属性的返回值将会被作为属性值的读取结果给你。这样会造成编译器无法知道你的像obj.name
这样的属性值读取过程该返回给你哪一个值。 - 你只把 value 去掉也是不可以的,因为 writable 对应我们马上要讲的 setter。
- 所以正确的 gettr 用法是下面这样。
- 让我们不设置 setter,尝试把 name 修改回 韩振方 试一下。
有了上面的 writable 的经验,我们大概率要翻车。果然控制台报错了,“你不能给一个只有 getter 的对象属性重新赋值。” - 聪明的你一定想到了下面的结论,没错, getter 对应的是 value ,而setter 对应的正是 writable。
- setter 也是一个值为函数的属性,不过这个属性接收一个参数,这个参数正是赋值运算符右边的内容。(也就是等号右边的值)千万一定要仔细看我们下面的写法。
- 我们仅仅在 setter 函数的内部打印了一下新的值,而并没有对新的值做任何操作,那么其实我们 obj 的 name 属性仍为数字 10。
验证一下: - 为什么要这样做呢?我举个简单的例子,这样会给我们一个十分重要的中间处理步骤。假设我在给 obj 重新命名。因为我姓韩,你改的名字里姓氏最起码得是韩才可以通过吧?你直接改成吴彦祖那不乱拉套了?
可以看到我们可以在 setter 正确拦截错误的操作。 - 请原谅我啰嗦一大堆,因为我想如果像上面这样用实际例子演示可能会比 MDN 这样一段大白话更加通俗易懂。
六. 重新分析 computed
回过头再看我们标题一的问题就显得十分清晰了。
七. 思考题
getter 和 setter 有一个经典的错误使用案例。请分析为什么下面的代码会引起递归导致栈溢出?
控制台输出
如果你明白了上面代码报错的原因,我想你也就明白了 getter 和 setter 🎁。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。