XboxYan

XboxYan 查看完整档案

武汉编辑武汉工程大学  |  信息科学与技术 编辑阅文集团  |  前端设计 编辑 xy-ui.codelabo.cn/docs/ 编辑
编辑

偏用户体验的前端~

个人动态

XboxYan 发布了文章 · 1月7日

换一种方式来实现CSS评分组件

评分组件想必大家都碰到过了吧,纯 CSS 方式网上也有很多实现,一般都是通过 input[type="radio"] 实现,比如这个

大致原理如下

  1. 通过 flex-direction: row-reverse 或者其他手段(dirction:rtltransform:scaleX(-1)),将元素位置翻转
  2. 配合 :checked~ 选中视觉上的前面兄弟节点

实现非常精妙,无需 js 接入,兼容性也不错,不过这里还有两个瑕疵

  1. 视觉展示和页面结构不一致,比如给每个 input 添加 value 属性,需要倒序来加,不符合一般人的认知
<div class="raido-group">
    <input type="radio" name="rate" value="5">
    <input type="radio" name="rate" value="4">
    <input type="radio" name="rate" value="3">
    <input type="radio" name="rate" value="2">
    <input type="radio" name="rate" value="1">
</div>
  1. 当组件聚焦后,通过键盘左右键切换也是相反的,具体表现就是,按方向键 “右”,评分却减少,反之亦然

优化

上面提到的两个瑕疵其实都是元素位置翻转引起的,目的也是为了实现视觉上的前置兄弟节点选择器,有没有什么办法规避这个问题呢?答案就是重置,处理如下

  1. 默认情况下都是选中的样式
  2. 配合 :checked~ 选中后面的元素,样式设置为未选中的样式

具体实现如下,html 为正常结构

<div class="raido-group">
    <input type="radio" name="rate" value="1">
    <input type="radio" name="rate" value="2">
    <input type="radio" name="rate" value="3">
    <input type="radio" name="rate" value="4">
    <input type="radio" name="rate" value="5">
</div>

下面是关键样式

[type="radio"]{
    -webkit-appearance: none;
    width: 20px;
    height: 20px;
    -webkit-mask: url("data:image/svg+xml,%3Csvg width='12' height='11' viewBox='0 0 12 11' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6 0l1.693 3.67 4.013.476L8.74 6.89l.788 3.964L6 8.88l-3.527 1.974.788-3.964L.294 4.146l4.013-.476L6 0z' fill='%23F67600'/%3E%3C/svg%3E");
    -webkit-mask-size: 20px;
    -webkit-mask-repeat: no-repeat;
    -webkit-mask-position: center;
    background-color: coral; /*默认是选中的样式,橙色*/
    margin: 0;
    transition: .2s;
}

[type="radio"]:checked~[type="radio"]{
    background-color: #E8EAED; /*未选中的样式,灰色*/
}
这里的星星图形是通过 mask 实现的,根据实际需求也可采用传统背景图片的方式

可以看到优化后的方案代码更加精简了,html 也符合常规认知,键盘切换也正常了

当然这个实现也还是有点瑕疵的,由于默认是选中状态,所以即使没有任何 :checked, 也会是全部高亮(满分)的情况,不会有0分的情况,所以推荐默认把最后一项设置 checked 属性。当然实际场景也并不会有0分的选项,很多场景下都是默认满分,所以这个小问题无伤大雅~

半颗星的实现

在上面的基础上,半颗星也很容易,只需要使用 10 个 input,每个只占据一半的空间即可

  1. 10个input
  2. 每个input宽度为之前的一半
  3. 配合 nth-child(odd)nth-child(even)设置显示区域

具体实现如下

<div class="raido-group rate-half">
    <input type="radio" name="rate-half" value="0.5">
    <input type="radio" name="rate-half" value="1">
    <input type="radio" name="rate-half" value="1.5">
    <input type="radio" name="rate-half" value="2">
    <input type="radio" name="rate-half" value="2.5">
    <input type="radio" name="rate-half" value="3">
    <input type="radio" name="rate-half" value="3.5">
    <input type="radio" name="rate-half" value="4">
    <input type="radio" name="rate-half" value="4.5">
    <input type="radio" name="rate-half" value="5">
</div>

下面是关键样式

.rate-half [type="radio"]{
    width: 10px; /* 宽度设置一半 */
}
.rate-half [type="radio"]:nth-child(odd){
    -webkit-mask-position: left; /* 设置星星的显示区域 */
}
.rate-half [type="radio"]:nth-child(even){
    -webkit-mask-position: right;
}

以上源码可查看 codepen

更好的实现

上面的实现已经非常完美了,无需 js,兼容性也不错,但是还是有些需要改进的地方,比如

  1. html 修改起来比较麻烦,从 5 分制改成 10 分制需要改动每一个 inputvalue 属性
  2. 星星的数量修改起来比较麻烦,从 5 颗星改成 10 颗星,需要手动添加新的 input 节点
  3. js 获取当前值相对比较麻烦,需要使用 document.quertSelector([type="radio"][name="rate"]:checked).value,不太符合语意,新手可能不太熟悉

那么,还有没有其他方式可以实现类似的效果呢?可以这样想一下,评分组件本质上也是一个表单输入组件,不考虑样式的话,普通的 input 也完全可以实现同样的输入功能,只是需要手动键盘输入而已。那有没有不需要手动输入的只需要简单拖拽就可以完成表单输入的呢?答案就是 input range范围选择器

<input type="range">

范围选择器有一些默认值

  1. 最小值 min0
  2. 最大值 max100
  3. 默认值 value 为区域范围的一半,50
  4. 默认步长 step1

现在和上面的评分组件对照起来,就可以很容易得出下面的结构

<input type="range" max="5" value="0">

这是一个可以输入 0~5 整数的组件,不能有小数出现,功能已经完全满足了

input range 的 shadow dom

现在可以开始改造样式了,在开始之前,可以在 Chrome 中开启 shadow-dom 的显示,方式为在控制台右上角 setting > preferences > Elements,勾选 Show user agent shadow DOM (已经勾选过的可以忽略)

这时便可很清楚的看到 input range 的内部结构

这里一共有三层,分别对应的选择器如下

  1. 容器 input[type="range" i]::-webkit-slider-container
  2. 轨道 input[type="range" i]::-webkit-slider-runnable-track
  3. 滑块 input[type="range" i]::-webkit-slider-thumb

input range 自定义样式

很多时候默认样式不是我们需要的,而且也不易修改,比如 Chrome 中 input range 的已滑动蓝色区域,这部分是无法修改的(Firefox 可以,这里只针对于 Chrome,其他浏览器原理类似),首先要做的就是重置

input[type="range"]{
    -webkit-appearance: none;
}

然后,由于滑块也是占据空间的,为了消除这个影响,可以将宽度设置为 0,当然也需要重置默认样式

input[type="range" i]::-webkit-slider-thumb {
    -webkit-appearance: none;
    width: 0;
}

由于 Chrome 没有已滑动区域和未滑动区域的区分,这里采用足够大的 box-shadow 来覆盖实现

input[type="range" i]::-webkit-slider-thumb {
    box-shadow: 999px 0px 0px 999px #E8EAED; /*可以实现一个右侧的足够大的阴影*/
}

通过以上几步可以实现一个这样的效果,可以先停下来体会一下?

最后一步,加上 mask 遮罩,实现镂空效果

input[type="range" i]::-webkit-slider-runnable-track {
    -webkit-mask: url("data:image/svg+xml,%3Csvg width='12' height='11' viewBox='0 0 12 11' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6 0l1.693 3.67 4.013.476L8.74 6.89l.788 3.964L6 8.88l-3.527 1.974.788-3.964L.294 4.146l4.013-.476L6 0z' fill='%23F67600'/%3E%3C/svg%3E"); /*星星图案*/
    -webkit-mask-size: 20px;
    -webkit-mask-repeat: repeat-x;
}

用一张动图描述一下效果

以上源码可查看 codepen

相关扩展

那么,现在来扩展一下,比如实现一个 10 颗星,总分 100 分的评分组件,支持半颗星,实现如下

<input type="range" max="100" value="0" step="5">

还需要修改一下宽度

input[type="range"]{
    width: 200px; /*每个星星的尺寸为20 * 20,10颗星宽度就是200*/
}

这样就得到了一个 10 颗心的评分组件

有时候,如果需要仅仅作为展示,比如一些电影评分,添加 disabled 就可以了~

<input type="range" value="0" step="0.5" disabled>

Firefox 的兼容

Firefox 也可以采用类似的原理,只需要换上对应的选择器即可,如下

  1. 容器 input[type=range](没有单独的容器,用最外层代替)
  2. 轨道 input[type=range]::-moz-range-track
  3. 滑块 input[type=range]::-moz-range-thumb

但是,Firefox 还有一个表示进度的选择器 ::-moz-range-progress,这样就可以不用 box-shadow 来遮盖了,所以,针对 Firefox 的另一种实现方式如下

input[type=range]{
    -webkit-mask: url("data:image/svg+xml,%3Csvg width='12' height='11' viewBox='0 0 12 11' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6 0l1.693 3.67 4.013.476L8.74 6.89l.788 3.964L6 8.88l-3.527 1.974.788-3.964L.294 4.146l4.013-.476L6 0z' fill='%23F67600'/%3E%3C/svg%3E");
    -webkit-mask-size: 20px;
    -webkit-mask-repeat: repeat-x;
    height: 20px;
}
input[type=range]::-moz-range-track{
    background: #E8EAED;
    height: inherit;
}
input[type=range]::-moz-range-progress {
    background: coral;
    height: inherit;
}
input[type=range]::-moz-range-thumb {
    width: 0;
    opacity: 0;
}

动图演示如下

可以看到这里是单独的两层,和 Chrome 用阴影遮挡可不一样,因此可以实现更加灵活的效果,比如不依赖 mask 遮罩,分别设置不同的背景,下面是示意代码

input[type=range]::-moz-range-track{
    background: 未选中的星星;
    height: inherit;
}
input[type=range]::-moz-range-progress {
    background: 选中的星星;
    height: inherit;
}

这样,在 Firefox 中这样的效果也能实现(前提是上面的可以遮挡住下面的)

最后

首先说说这个方式的优点

  1. html 结构非常简单,就一个 input 元素
  2. 属性修改非常方便,只用设置 stepmax等相关属性即可
  3. 语义非常友好,默认表单提交也天然支持
  4. 当然键盘切换也是没有问题
  5. 移动端支持友好,可以滑动选择
  6. js 获取评分也符合常规表单,只需要 input.value 即可

当然,还是有一些局限

  1. IE 支持不够友好,理论上也是可以兼容的,尝试了一下在滑动的过程中会闪烁,体验不佳
  2. 没有 hover 样式

所以,如果你的项目不需要关照 IE ,大可以使用 input range 的方式~

查看原文

赞 16 收藏 10 评论 3

XboxYan 发布了文章 · 1月7日

如何优雅的获取Form表单数据?

先看一个简单的例子

比如这里有一个简单的 form 表单

<form id="form">
    <input name="user" type="text">
    <div>
        <input type="radio" name="A" value="r1">r1
        <input type="radio" name="A" value="r2">r2
        <input type="radio" name="A" value="r3">r3
        <input type="radio" name="A" value="r4">r4
    </div>
    <div>
        <input type="checkbox" name="B" value="c1">c1
        <input type="checkbox" name="B" value="c2">c2
        <input type="checkbox" name="B" value="c3">c3
        <input type="checkbox" name="B" value="c4">c4
    </div>
    <button type="submit">login</button>
    <button type="reset">reset</button>
</form>

页面如下

image.png

现在,随便输入点内容

image.png

如果提交,表单会触发默认行为,直接以get方式提交到当前页

image.png

可以看到这些参数正是我们输入的参数。

表单的默认行为

通常我们都会把这些默认行为阻止掉,毕竟都不希望在提交表单会跳转到另一个页面,或者刷新整个页面

form.addEventListener("submit", (ev) => {
    ev.preventDefault();
    // 自己发请求
})

那么,我需要如何拿到这些提交的值呢?

表单提交的数据类型

根据数据类型来划分,通常使用的表单提交数据格式大概有以下几种

  1. formdata格式,需要通过 new FormData 创建
  2. json格式,形如{ a:1,b:2,c=3 }
  3. url字符串拼接,形如 a=1&b=2&c=3

1.formdata

通常 formdata 的使用方式如下

var formData = new FormData();
formData.append('username', 'xboxyan');
formData.append('password', '123456');

但是对于 form 来说,大可以不必采用这样的方式,直接通过以下方式获取

var formData = new FormData(form); // form为表单对象

所以,针对表单容器就不要再用 div 了!

为了更加方便的使用,这里简单扩展一下 form 属性

/* formdata */
Object.defineProperty(HTMLFormElement.prototype, 'formdata', {
    get() {
        return new FormData(this);
    }
})

这样就可以通过form.formdata来获取了

image.png

FormData对象的值是不可见的,需要通过get方式才能看到

image.png

关于 FormData 的更多操作可参考 https://developer.mozilla.org...

2.jsondata

json 格式也是非常常用的一种数据传递方式了,不过原生 form 并没有直接获取的方式,只能自己封装了。

这里可以直接在上面 FormData 的基础上遍历一次即可,由于有些值会有多个,比如上面的多选框,这里直接转成了数组,具体实现如下

/* jsondata */
Object.defineProperty(HTMLFormElement.prototype, 'jsondata', {
    get() {
        const jsondata = {}
        const formdata = new FormData(this);
        formdata.forEach((value, key) => {
            if (!jsondata[key]) {
                jsondata[key] = formdata.getAll(key).length > 1 ? formdata.getAll(key) : formdata.get(key);
            }
        });
        return jsondata;
    }
})

这样就可以通过form.jsondata来获取了

image.png

3.urldata

还有一种方式在get请求中会用到,就是 url拼接方式,这个原生也没有直接的方案,可以借助上面jsondata转换一下,这里做了一个小改动,比如默认的多选是b=1&b=2&b=3&c=1,这里改造成了b=1,2,3&c=1,具体实现如下

/* urldata */
Object.defineProperty(HTMLFormElement.prototype, 'urldata', {
    get() {
        const urldata = [];
        Object.entries(this.jsondata).forEach(([key, value]) => {
            urldata.push(key + '=' + (value.join ? value.join() : value))
        })
        return urldata.join('&');
    }
})

这样就可以通过form.urldata来获取了

image.png

总结

通过以上3个方法,基本可以满足日常开发的绝大部分需求,前提是 html 足够规范,表单就必须是 form,单选就必须是 input radio等等,很多前端同学或者说很多框架都喜欢用 div 来模拟,以至于这些原生特性就直接被抛弃掉了,岂不可惜?

最后,附上以上全部源码

/* jsondata */
Object.defineProperty(HTMLFormElement.prototype, 'jsondata', {
    get() {
        const jsondata = {}
        const formdata = new FormData(this);
        formdata.forEach((value, key) => {
            if (!jsondata[key]) {
                jsondata[key] = formdata.getAll(key).length > 1 ? formdata.getAll(key) : formdata.get(key);
            }
        });
        return jsondata;
    }
})
/* formdata */
Object.defineProperty(HTMLFormElement.prototype, 'formdata', {
    get() {
        return new FormData(this);
    }
})
/* urldata */
Object.defineProperty(HTMLFormElement.prototype, 'urldata', {
    get() {
        const urldata = [];
        Object.entries(this.jsondata).forEach(([key, value]) => {
            urldata.push(key + '=' + (value.join ? value.join() : value))
        })
        return urldata.join('&');
    }
})
查看原文

赞 12 收藏 9 评论 0

XboxYan 回答了问题 · 2020-11-11

如何让背景色,铺满整个屏幕,并且高度随内容自适应

假设页面上页面上有一个.box

html,body{
    height: 100%;
    margin: 0;
}

.box{
    height: 100%;
    background: red;
}

关注 4 回答 4

XboxYan 回答了问题 · 2020-11-11

解决有没有封装了indexedDB的js库啊,急需!

看看这个,yux-storage,阅文集团荣誉出品

  1. 使用类似 localStorage API, 无需考虑 IndexedDB 的复杂概念,上手无压力。
  2. 支持回调和 Promise 两种方式,各凭所愿。
  3. 非常轻量,100 行左右的源码,压缩后更小。

以下是继承 IndexedDB 的特点

  1. 可以存储多种类型的数据,而不仅仅是字符串。
  2. 储存空间大,一般来说不少于 250MB,甚至没有上限。
  3. 异步操作,在进行大量数据存取时不会阻塞应用程序。

关注 8 回答 6

XboxYan 发布了文章 · 2020-11-06

使用svg描边来实现移动端1px

今天介绍一个通过svg来实现移动端1px效果的小技巧

svg的描边方式

通常我们在使用一些设计软件时,描边会有三种选择,分别是内描边居中描边外描边,比如 photoshop

image.png

那么,svg 中的描边是哪种方式呢?

答案是居中描边,并且无法更改,如下

<svg height="100px" viewBox="0 0 100 100">
    <rect x='10' y='10' width='40' height='40' fill='none' stroke-width='10' stroke='red' />
</svg>

image.png

可以看到,rect 的描边是居中的,两边各是 5

0.5px的实现

根据上面的结论,如果 stroke-width1 时,那么就很轻松的被分成了两边各 0.5,然后把外侧的部分截断就可以了

这里直接设置 rect 的宽高都为 100%,并且 svg 默认是超出隐藏的 (overflow:hidden),如下

<svg height="100px" viewBox="0 0 100 100">
    <rect width='100%' height='100%' fill='none' stroke-width='1' stroke='red' />
</svg>

这里对比一下1px的效果

<div style="box-sizing: border-box; width:100px;height:100px;border:1px solid red;"></div>

image.png

可以用手机访问或者扫描以下网址体验

https://codepen.io/xboxyan/pe...

image.png

svg作为背景使用

以上是直接使用 svg 标签,实际项目当然不能这样使用了,不过可以将svg 作为背景图片来使用,例如

div {
  background: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'><rect width='100%' height='100%' fill='none' stroke-width='1' stroke='red' /></svg>");
}

很简单,在 svg 前面添加一段 data:image/svg+xml, 表示这是一张 svg格式的图片,和 base64 的写法比较类似,后面 svg 的属性xmlns='http://www.w3.org/2000/svg'表示命名空间,暂时还不能去除,记住这层规律就可以了

当然在IE下,svg可能还需要转义才能正常显示,这里可以参考张鑫旭老师的这篇文章

下面来看几个案例

1. 1px分割线

这个在移动端特别常见,这里可以这样来实现,直接使用 line 画一条底部的线

p {
  background: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'><line x1='0' y1='100%' x2='100%' y2='100%' stroke='red' /></svg>");
}

效果如下

image.png

https://codepen.io/xboxyan/pe...

image.png

2. 1px边框

和分割线类似,只是用rect来画一个自适应的矩形,这里宽高均为 100%

div {
  background: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'><rect width='100%' height='100%' fill='none' stroke='red' /></svg>");
}

效果如下

image.png

https://codepen.io/xboxyan/pe...

image.png

3. 1px带圆角的边框

常规方式实现带圆角的边框就无能为力了,svg 可以很轻松的实现,借助rx 属性,还有元素自身的border-radius就可以了

div {
  border-radius:5px;
  background: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'><rect width='100%' height='100%' rx='5' fill='none' stroke='red' /></svg>");
}

效果如下

image.png

https://codepen.io/xboxyan/pe...

image.png

总结

以上就是几个svg实现边框的小技巧了,这里是用background-image模拟的,比使用伪元素要方便的多,同时也不占据空间,算是相对完美的解决方案了~

查看原文

赞 9 收藏 4 评论 0

XboxYan 回答了问题 · 2020-07-21

html页面能否获取form表单传入的参数?

method改成get或者去掉,默认情况下会直接携带参数到地址栏上

b.html?height=xxx&weight=xxx

关注 3 回答 1

XboxYan 赞了文章 · 2020-05-01

如何理解Object.defineProperty()?

几乎所有使用Vue的开发者都知道,Vue的双向绑定是通过Object.defineProperty()实现的,也知道在getter中收集依赖,在setter中通知更新。

那么除了知道getter和setter之外,Object.defineProperty()还有哪些值得我们去注意的地方呢?是不是有很多细节的东西不懂呢?

你可能会说,除了getter和setter之外,Object.defineProperty()还有value,writable,enumerable,configurable。

那么问题来了?

  • value或writable与getter,setter可以共存吗?与enumerable,configurable呢?
  • 概括讲下writable,enumerable,configurable分别是什么意思?
  • enumerable在Object.keys()和for...in以及展开操作符...是如何表现的?
  • configurable会限制哪些属性不可redefine?value会被限制吗?会限制属性的删除吗?
  • 通过obj.foo和Object.defineProperty(obj,foo)方式定义的属性有何区别?
  • data descriptor、accessor descriptor、shared descriptor是什么?

如果看了上面这些问题一脸懵逼,不要惊慌,我们先来看一道非常直观易懂的题目:

// 实现下面的逻辑
console.log(a+a+a); // 'abc'

题目看完了,带着问题开始阅读下面的内容吧。
如果能耐心看完的话对于个人的前端技术提升会非常大。
往近了说,不出意外上面这些问题全部可以迎刃而解,对于a+a+a题目的题解也会理解更加透彻。
往远了说,可以去看懂Vue源码相关的实现,以及看懂任何使用到Object.defineProperty()这个API的库的源码实现,甚至最后自己写个小轮子。

  • 初识Object.defineProperty()
  • 语法

    • 参数
    • 返回值
  • Object.defineProperty()概览

    • 基本知识点
    • data和accessor两种描述符
    • 描述符必须是data, accessor之一,不能同时具有两种特性
    • 如何区分data descriptor和accessor descriptor?
    • descriptor key概览

      - 共享descriptor key概览
      - data descriptor key概览
      - accessor descriptor key概览
    • 牢记属性不仅仅是descriptor自己的属性,还要考虑继承属性
    • 三个很基础但是很好的例子

      - 默认descriptor:不可写,不可枚举,不可配置
      - 重用同一对象记忆上一次的value值
      - 冻结Object.prototype
  • Object.defineProperty()详解

    • 创建一个property
    • 修改一个property

      • Writable attribute
      • Enumerable attribute

        • 知识点
        • 在for...in中如何表现?
        • 在Object.keys()中如何表现?
        • 在展开操作符...中如何表现?
        • 如何检测属性是否可以枚举?
      • Configurable attribute
    • 增加属性和默认值
    • 自定义setter和getter
    • properties的继承
  • 如何获取属性的descriptor?
  • console.log(a+a+a); // 'abc'题解

    • 解法1: Object.defineProperty() 外部变量
    • 解法1(优化版):Object.defineProperty() 内部变量
    • 解法2: Object.prototpye.valueOf()
    • 解法3:charCodeAt,charFromCode
    • 解法3(优化版一):内部变量this._count和_code
    • 解法3(优化版二):内部变量this._code
    • 题目扩展: 打印a...z
    • 题目扩展(优化版): 打印a...z

初识Object.defineProperty()

静态方法Object.defineProperty()会直接在一个对象上定义一个新的属性,或者修改对象上已经存在的属性,然后返回这个对象。

const obj = {};
Object.defineProperty(obj, 'prop', {
    value: 42,
    writable: true
});
console.log(obj); // {prop: 42}
obj.prop = 43; // {prop: 43}

语法

Object.defineProperty(obj, prop, descriptor)

参数

  • obj 需要定义属性的对象
  • prop 需要定义或者修改的property的名字或者Symbol
  • descriptor 定义和修改的property的描述符

返回值

返回传递进函数的对象。

Object.defineProperty()概览

  • 基本知识点
  • data和accessor两种描述符
  • 描述符必须是data, accessor之一,不能同时具有两种特性
  • 如何区分data descriptor和accessor descriptor?
  • descriptor key概览

    • 共享descriptor key概览
    • data descriptor key概览
    • accessor descriptor key概览
  • 牢记属性不仅仅是descriptor自己的属性,还要考虑继承属性
  • 三个很基础但是很好的例子

    • 默认descriptor:不可写,不可枚举,不可配置
    • 重用同一对象记忆上一次的value值
    • 冻结Object.prototype

基本知识点

  • Object.defineProperty()允许精准添加或者修改对象上的一个属性。
  • 通过const obj = {};obj.foo = 1这种赋值方式增添的属性,可以通过for...in或者Object.keys枚举,他的值可能发生改变,也可能被删除。
  • Object.defineProperty()允许对对象属性的默认方法做出改变。
  • 默认情况下通过Object.defineProperty()是immutable(不可变的)。不能通过delete obj.foo删除这个属性。
  • Object.defineProperty()具有data和accessor两种描述符,描述符生效时只能是其中之一,不能同时生效。
  • data和accessor两种描述符都是object,dataDescriptor = {value, writable},accessorDescriptor={get(){}, set(){}}
  • data和accessor有各自独有的key,它们也有共享的key。data accessor特有的key为value和writable,accessor descriptor特有的key为get和set。共享的key为configurable和enumerable。
  • 如果descriptor没有value, writable, get和set,会被当做一个data descriptor;如果同时有value或writable和get或set,异常会抛出。

data和accessor两种描述符

对象的属性descriptor描述符主要有两种:data descriptor和accessor descriptor。

data descriptor

数据描述符指的是value,writable,它可能是可写的,也可能是不可写的。

accessor descriptor

权限描述符指的是通过getter-setter函数get(),set()对property的描述。

描述符必须是data, accessor之一,不能同时具有两种特性

下面的代码会报错的原因破案了:只能是data,accessor 之一。

Object.defineProperty({this, 'a', {
    value: 'a', // data descriptor
    get(){
         // access descriptor
    }
})
// `Invalid property descriptor.Cannot both specify accessors and a value or writable attribue.`

如何区分data descriptor和accessor descriptor?

data accessor特有的key为value和writable。
accessor descriptor特有的key为get和set。

// 典型的data descriptor
Object.defineProperty({this, 'a', {
    value: 'a',
    writable: false
})
// 典型的accessor descriptor
Object.defineProperty({this, 'a', {
    get(){ ... }
    set(){ ... }
})

descriptor key概览

默认情况下是通过Object.defineProperty()定义属性的。

共享descriptor key概览
configurable
  • 默认值为false
  • 当且仅当属性的描述符类型可能发生变化以及属性描述符可能从对象上删除和这个属性相关联
  • configurable为false时,非data descriptor的属性不能被重定义,也就是说除value和writable之外的属性不能定义,而且特别要注意,value可以随意改,而writable仅能从true改为false。get(), set(), enumerable, configurable是都不能重新定义的。
  • 而且不能切换descriptor的类型:data descriptor和accessor descriptor
  • configurable 不仅仅影响属性的修改,还影响到了属性的删除。configurable为false时delete obj.o失效

为什么configurable设置为false时要这样设计?

  • 提升对象属性可控性
  • 提升安全性

这是因为get(), set(), enumerable, configurable是权限相关的属性,为了避免引起不必要的bug。
很多库的作者不允许自己修改这个属性,让它保持在一种可控的状态,从而代码按照自己的预期去运行。而且这样做也更加安全。

enumerable
  • 默认值为false
  • 当且仅当对象的属性枚举展示时会和这个属性相关联
data descriptor key概览
value
  • 默认值为undefined
  • 属性相关联的value
  • 可以是任何JavaScript值 number,object,function等等
writable
  • 默认是false
  • 当且仅当通过赋值操作符赋值时会和这个属性相关联
accessor descriptor key概览
get
  • 默认值为undefined
  • 作为属性的getter服务于属性,如果没有getter的话,get为undefined。
  • 当property被访问时,这个函数会在不传参的情况下调用然后,并将this设置为访问属性的对象(this由于继承可能不是定义属性的对象。)
  • 返回值会作为property的value。
set
  • 默认值为undefined
  • 作为属性的setter服务于属性,如果没有setter的话,set为undefined。
  • 当属性重新赋值时,函数在传递一个参数的情况下调用,并将这个集合设置为属性赋值的对象。

牢记属性不仅仅是descriptor自己的属性,还要考虑继承属性

为确保保留了这些默认值:

  • 可以freeze Object.prototype
  • 或者Object.create(null)

三个很基础但是很好的例子

默认descriptor:不可写,不可枚举,不可配置
var obj = {};
var descriptor = Object.create(null); // no inherited properties
descriptor.value = 'static';

// not enumerable, not configurable, not writable as defaults
Object.defineProperty(obj, 'key', descriptor);

// being explicit
Object.defineProperty(obj, 'key', {
  enumerable: false,
  configurable: false,
  writable: false,
  value: 'static'
});
重用同一对象记忆上一次的value值
function withValue(value) {
  var d = withValue.d || (
    // 记忆住上一次的值
    withValue.d = {
      enumerable: false,
      writable: false,
      configurable: false,
      value: value
    }
  );

  // 避免重复赋值
  if (d.value !== value) d.value = value;
  return d;
}
Object.defineProperty(obj, 'key', withValue('static'));
冻结Object.prototype
Object.freeze(Object.prototype)

Object.defineProperty()详解

创建一个property

属性如果在对象上不存在的话,Object.defineProperty()会创建一个新的属性。
可以省略很多描述符中字段,并且输入这些字段的默认值。

// 创建对象
var o = {};
// 定义属性a并且传入data descriptor
Object.defineProperty(o, 'a', {
    value: 37,
    writable: true,
    enumerable: true,
    configurable: true,
})
// 定义属性b并且传入accessor descriptor
// 伪造value(好处是更细粒度的value控制):外部变量和get()
// 伪造writable(好处是更细粒度的writable控制):外部变量和set()
// 在这个例子中,o.b的值与bValue做了强关联。bValue是什么值,o.b就是什么值。除非o.b被重新定义
var bValue = 38;
Object.defineProperty(o, 'b', {
    get() { return bValue },
    set(newValue) { bValue = newVlaue },
    enumerable: true,
    configurable: true,
})
// 不可以同时混合定义两者
Object.defineProperty(o, 'conflict', {
    value: 'a',
    get() { return 'a' }
})
// 报错:Cannot both specify accessors and a value or writable
// 重新解读报错:Cannot both specify accessors descriptor and data descriptor(a value or writable)

修改一个property

  • 当一个属性在对象中存在时,Object.defineProperty()可以根据descriptor中的值和对象返回值的配置尝试修改这个属性。
  • 如果旧的descriptor有configurable属性,并且设置为false,意思是”不可配置“。

    • 意味着不能修改任意共享descriptor和accessor descriptor的属性的值
    • 可以重定义data descriptor:value任意变,writable只能从true变为false(不能从false改为true)。
    • 而且不能切换descriptor的类型:data descriptor和accessor descriptor
    • 违反规则报错:Cannot redefine property: xxx;符合规则和没有修改属性的话不报错。
Writable attribute

当writable设置为false时,属性是不可写的,意味着无法重新赋值。

  • 非严格模式不会报错,只是赋值失败
  • 严格模式会报错Cannot assign to read only property 'b' of object '#<Object>'
// 非严格模式不会报错,只是赋值失败
var o = {};
Object.defineProperty(o, 'a', {
  value: 37,
  writable: false
});
console.log(o.a); // logs 37
o.a = 25; // 不会报错
// (只会在strict mode报错,或者值没改变也不会报错)
console.log(o.a); // logs 37. 重新赋值没有生效

// 严格模式会报错
// strict mode
(function() {
  'use strict';
  var o = {};
  Object.defineProperty(o, 'b', {
    value: 2,
    writable: false
  });
  o.b = 3; // 抛出Cannot assign to read only property 'b' of object '#<Object>'
  return o.b; // 2
}());
Enumerable attribute
  • 知识点
  • 在for...in中如何表现?
  • 在Object.keys()中如何表现?
  • 在展开操作符...中如何表现?
  • 如何检测属性是否可以枚举?
知识点
  • enumerable属性定义了属性是否可以被Object.assign()或者spread(...) pick到。
  • 对于非symbol的属性,它还会影响到for...in和Object.keys()对属性的pick。
  • 可以用obj.propertyIsEnumerable(prop)检测属性是否可遍历。
var o = {};
Object.defineProperty(o, 'a', {
  value: 1,
  enumerable: true
});
Object.defineProperty(o, 'b', {
  value: 2,
  enumerable: false
});
Object.defineProperty(o, 'c', {
  value: 3, // enumerable默认为false
}); 
o.d = 4; // enumerable默认为true
Object.defineProperty(o, Symbol.for('e'), {
  value: 5,
  enumerable: true
});
Object.defineProperty(o, Symbol.for('f'), {
  value: 6,
  enumerable: false
});
在for...in中如何表现?

只有'a'和'd'打印了出来。
enumerable为true的都能被解构出来,不包括Symbol。

for (var i in o) {
  console.log(i); // 'a','d'
}
在Object.keys()中如何表现?

只有'a'和'd'被搜集到。
enumerable为true的都能被解构出来,不包括Symbol。

Object.keys(o); // ['a', 'd']
在展开操作符...中如何表现?

enumerable为true的都能被解构出来,包括Symbol。

var p = { ...o }
p.a // 1
p.b // undefined
p.c // undefined
p.d // 4
p[Symbol.for('e')] // 5
p[Symbol.for('f')] // undefined
如何检测属性是否可以枚举?

可以用obj.propertyIsEnumerable(prop)检测属性是否可遍历

o.propertyIsEnumerable('a'); // true
o.propertyIsEnumerable('b'); // false
o.propertyIsEnumerable('c'); // false
o.propertyIsEnumerable('d'); // true
o.propertyIsEnumerable(Symbol.for('e')); // true
o.propertyIsEnumerable(Symbol.for('f')); // false
Configurable attribute

configurable属性控制属性是否可以被修改(除value和writable外),或者属性被删除。

var o = {};
Object.defineProperty(o, 'a', {
  get() { return 1; },
  configurable: false
});

Object.defineProperty(o, 'a', {
  configurable: true
}); // throws a TypeError
Object.defineProperty(o, 'a', {
  enumerable: true
}); // throws a TypeError
Object.defineProperty(o, 'a', {
  set() {}
}); // throws a TypeError (set初始值为undefined)
Object.defineProperty(o, 'a', {
  get() { return 1; }
}); // throws a TypeError
// (即使set没有变化)
Object.defineProperty(o, 'a', {
  value: 12
}); // throws a TypeError // ('value' can be changed when 'configurable' is false but not in this case due to 'get' accessor)

console.log(o.a); // logs 1
delete o.a; // 不能删除
console.log(o.a); // logs 1

增加属性和默认值

属性的默认值很值得思考一下。
通过点操作符.赋值和通过Object.defineProperty()是有区别的。

两种赋初始值方式的区别如下

  • 通过点操作符定义的属性,writable,configurable,enumerable值都为true,value为赋入的值
  • 通过Object.defineProperty只指定value的属性,writable,configurable,enumerable值都为false
通过点操作符定义的属性

通过点操作符定义的属性等价于Object.defineProperty的data descriptor和共享descriptor为true。

var o = {};
o.a = 1;
// 等价于
Object.defineProperty(o, 'a', {
  value: 1,
  writable: true,
  configurable: true,
  enumerable: true
});
通过Object.defineProperty只指定value的属性
Object.defineProperty(o, 'a', { value: 1 });
// 等价于
Object.defineProperty(o, 'a', {
  value: 1,
  writable: false,
  configurable: false,
  enumerable: false
});

自定义setter和getter

下面的例子展示了如何实现一个自存档的对象。
当temperature属性设置后,archive数组会打印。

  • 常见的一种gettter,setter使用方式
  • 这个getter和setter总是返回相同的值
常见的一种gettter,setter使用方式
function Archiver() {
    var temperature = null;
    var archive = [];
    Object.defineProperty(this, 'temperature', {
        get(){
            console.log('get!');
            return temperature;
        },
        set(value) {
            temperature = value;
            archive.push({ val: temperature });
        }
    });
    this.getArchive = function(){ return archive; };
}
var arc = new Archiver();
arc.temperature; // 'get'
arc.temperature = 11;
arc.temperature = 13;
arc.getArchive(); // [{val: 11}, {vale: 13}]
这个getter和setter总是返回相同的值
var pattern = {
    get() {
        return 'I always return this string, ' +
               'whatever you have assigned';
    },
    set() {
        this.myname = 'this is my name string';
    }
};

function TestDefineSetAndGet() {
    Object.defineProperty(this, 'myproperty', pattern);
}

var instance = new TestDefineSetAndGet();
instance.myproperty = 'test';
console.log(instance.myproperty);
// I always return this string, whatever you have assigned

console.log(instance.myname); // this is my name string

properties的继承

  • 如果一个accessor属性是继承的,它的get和set方法会在属性被访问时调用,并且在后代对象上被修改。如果这些方法使用变量来存储这个值,这个值会在所有对象间共享(即使使用new仍然会共享)
  • 如果一个value属性是继承的,它可以直接设置在对象上。但是,如果继承了一个不可写的值属性,它仍然会阻止修改对象上的属性。

主要为以下3个问题:

  • Object.defineProperty与prototype的问题
  • 如何解决 Object.defineProperty与prototype的问题
  • Object.defineProperty的writable和__proto__
Object.defineProperty与prototype的问题

这个例子展示了继承带来的问题:

function myclass() {
}

var value;
Object.defineProperty(myclass.prototype, "x", {
  get() {
    return value;
  },
  set(x) {
    value = x;
  }
});

var a = new myclass();
var b = new myclass();
a.x = 1;
console.log(b.x); // 1
如何解决 Object.defineProperty与prototype的问题

如何解决这个问题呢?
可以将值存储在另一个this属性上。这样使用new创建新实例时,可以为自己开辟单独的属性空间。
在get和set方法中,this指向使用、访问、修改属性的对象实例。

function myclass() {
}
Object.defineProperty(myclass.prototype, "x", {
  get() {
    return this._x;
  },
  set(x) {
    this._x = x; // 用this._x来存储value
  }
});

var a = new myclass();
var b = new myclass();
a.x = 1;
console.log(b.x); // 1
Object.defineProperty的writable和__proto__

下面的例子,点操作符赋值的属性可写,但是继承的myclass.prototype的初始值不会发生更改;不可写的属性不可写。

function myclass() {
}

myclass.prototype.x = 1;
Object.defineProperty(myclass.prototype, "y", {
  writable: false,
  value: 1
});

var a = new myclass();
a.x = 2;
console.log(a.x); // 2
console.log(myclass.prototype.x); // 1
a.y = 2; // Ignored, throws in strict mode
console.log(a.y); // 1
console.log(myclass.prototype.y); // 1

值得分析一波的截图:
image

  • __proto__是通过Object.defineProperty(foo.prototype)实现的继承
  • 如果属性的writable为true,会在__proto__的上一级创建新的属性

如何获取属性的descriptor?

Object.getOwnPropertyDescriptor(obj,prop)

使用示例:

var o = {};
Object.defineProperty(o, 'a', { value: 1 });
Object.getOwnPropertyDescriptor(o,'a')
// {
//     configurable: false
//     enumerable: false
//     value: 1
//     writable: false
// }

console.log(a+a+a); // 'abc'题解

/*
  console.log(a + a + a); // 打印'abc'
*/
  • 解法1: Object.defineProperty() 外部变量
  • 解法1(优化版):Object.defineProperty() 内部变量
  • 解法2: Object.prototpye.valueOf()
  • 解法3:charCodeAt,charFromCode
  • 解法3(优化版一):内部变量this._count和_code
  • 解法3(优化版二):内部变量this._code
  • 题目扩展: 打印a...z
  • 题目扩展(优化版): 打印a...z
/**
 * 解法1: Object.defineProperty() 外部变量
 */
let value = "a";
Object.defineProperty(this, "a", {
  get() {
    let result = value;
    if (value === "a") {
      value = "b";
    } else if (value === "b") {
      value = "c";
    }
    return result;
  },
});
console.log(a + a + a);
/**
 * 解法1(优化版):Object.defineProperty() 内部变量
 */
Object.defineProperty(this, "a", {
  get() {
    this._v = this._v || "a";
    if (this._v === "a") {
      this._v = "b";
      return "a";
    } else if (this._v === "b") {
      this._v = "c";
      return "b";
    } else {
      return this._v;
    }
  },
});
console.log(a + a + a);

/**
 * 解法2: Object.prototpye.valueOf()
 */
let index = 0;
let a = {
  value: "a",
  valueOf() {
    return ["a", "b", "c"][index++];
  },
};
console.log(a + a + a);

/**
 * 解法3:charCodeAt,charFromCode
 */
let code = "a".charCodeAt(0);
let count = 0;
Object.defineProperty(this, "a", {
  get() {
    let char = String.fromCharCode(code + count);
    count++;
    return char;
  },
});
console.log(a + a + a); // 'abc'

/**
 * 解法3(优化版一):内部变量this._count和_code
 */
Object.defineProperty(this, "a", {
  get() {
    let _code = "a".charCodeAt(0);
    this._count = this._count || 0;
    let char = String.fromCharCode(_code + this._count);
    this._count++;
    return char;
  },
});
console.log(a + a + a); // 'abc'

/**
 * 解法3(优化版二):内部变量this._code
 */
Object.defineProperty(this, "a", {
  get() {
    this._code = this._code || "a".charCodeAt(0);
    let char = String.fromCharCode(this._code);
    this._code++;
    return char;
  },
});
console.log(a + a + a); // 'abc'

/*
 题目扩展: 打印`a...z`
 a+a+a; //'abc'
 a+a+a+a; //'abcd'
*/
/**
 * charCodeAt,charFromCode
 */
let code = "a".charCodeAt(0);
let count = 0;
Object.defineProperty(this, "a", {
  get() {
    let char = String.fromCharCode(code + count);
    if (count >= 26) {
      return "";
    }
    count++;
    return char;
  },
});
// 打印‘abc’
console.log(a + a + a); // 'abc'

// 打印‘abcd’
let code = "a".charCodeAt(0);
let count = 0;
// {...定义a...}
console.log(a + a + a); // 'abcd'

// 打印‘abcdefghijklmnopqrstuvwxyz’
let code = "a".charCodeAt(0);
let count = 0;
// {...定义a...}
let str = "";
for (let i = 0; i < 27; i++) {
  str += a;
}
console.log(str); // "abcdefghijklmnopqrstuvwxyz"

/*
 题目扩展(优化版): 打印`a...z`
 a+a+a; //'abc'
 a+a+a+a; //'abcd'
*/

Object.defineProperty(this, "a", {
  get() {
    this._code = this._code || "a".charCodeAt(0);
    let char = String.fromCharCode(this._code);
    if (this._code >= "a".charCodeAt(0) + 26) {
      return "";
    }
    this._code++;
    return char;
  },
});
// 打印‘abc’
console.log(a + a + a); // 'abc'

参考资料:
https://developer.mozilla.org...
https://developer.mozilla.org...
https://developer.mozilla.org...

期待和大家交流,共同进步,欢迎大家加入我创建的与前端开发密切相关的技术讨论小组:

努力成为优秀前端工程师!
查看原文

赞 15 收藏 11 评论 0

XboxYan 关注了用户 · 2020-05-01

zangeci @zangeci

关注 25

XboxYan 赞了回答 · 2020-05-01

解决有没有什么办法可以监听到value的改变?

重写DOM原型方法你觉得呢?

var desc = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')
Object.defineProperty(HTMLInputElement.prototype, 'value', {
    ...desc,
    set(v) {
        console.log('set',v);
        desc.set.call(this, v)
    }
})

屏幕快照 2020-05-01 上午7.44.38.png

关注 4 回答 2

XboxYan 关注了用户 · 2020-04-30

边城 @jamesfancy

从事软件开发 20 年,在软件分析、设计、架构、开发及软件开发技术研究和培训等方面有着非常丰富的经验,近年主要在研究 Web 前端技术、基于 .NET 的后端开发技术和相关软件架构。

关注 10978

认证与成就

  • 获得 1117 次点赞
  • 获得 27 枚徽章 获得 3 枚金徽章, 获得 5 枚银徽章, 获得 19 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • xy-ui

    面向未来的原生 Web Components UI组件库

  • 第三方电影天堂React Native客户端

    这是一个使用react-native技术实现的第三方电影天堂客户端。 大概是全网个人影视类项目最漂亮、体验最好的了吧。 最为一名偏体验偏设计的前端开发者,对界面和用户体验都有极高的重视。

  • xboxyan的博客

    这是一个以koa框架为基础,mongoDB为数据库的博客管理系统。 前端已react为框架实现的单页面应用。

注册于 2016-04-23
个人主页被 4.6k 人浏览