跳到主要内容

11 篇博文 含有标签「JS」

查看所有标签

dart与js的互操作(一)

· 阅读需 1 分钟

https://dart.dev/interop/js-interop/past-js-interop


@js.JSExport()
class _MyAppState extends State<MyApp> {
final _streamController = StreamController<void>.broadcast();
DemoScreen _currentDemoScreen = DemoScreen.counter;
int _counterScreenCount = 0;

@override
void initState() {
super.initState();
final export = js.createJSInteropWrapper(this);
js.globalContext['_appState'] = export;
js.globalContext.callMethod('_stateSet'.toJS); // 调用js方法,将Dart实例化
}

JS 节流与防抖(附 React Hooks)

· 阅读需 10 分钟

在搜索框里敲「react」:r、re、rea、reac、react——如果每次敲键都发一次请求,一次输入就白白浪费 4 次网络请求,而且前 4 次的结果你根本来不及看,它们已经在回来的路上了。再往下想:滚动页面时 onScroll 一秒钟触发几十次,resize 拖拽时更是每帧一次。

这类问题有一个共同的名字:事件风暴——短时间内触发太多次,而真正需要执行的只有少数几次。

一、回答两个问题

事件风暴的本质是:触发频率远高于执行需求。那么「到底该执行哪几次、什么时候执行」,其实只取决于你对这件事的期望。把期望问清楚,答案自己就出来了。

问题 1:我在乎的是「最后的结果」吗?

输入搜索、窗口 resize、表单项校验……这些场景里,过程不重要,结果才重要。你敲了一长串,真正需要的只是「停下来之后」那一次的搜索结果;窗口拖了一路,真正需要的只是「停住之后」那一次的布局重算。

防抖的定义用一句话说:持续触发时一律不执行,直到停止触发后过一段时间,才执行最后一次。

一个贴切的比喻是电梯关门:有人进出,电梯就重新计时关门,等人流彻底安静了,才关门上楼。你不在乎期间谁进进出出,只在乎「最后走的那一刻」。

问题 2:需要在过程中「持续跟进」,但不能太频繁吗?

滚动加载更多、滚动进度条、拖拽坐标跟随、游戏里的连点……这些场景里,过程是有价值的——滚到一半就该触发加载,进度条该跟着走。但你不能让每一次 scroll 都触发,那太浪费了。

节流的定义用一句话说:保证一定时间间隔内最多执行一次,但会持续执行,直到结束。

贴切的比喻是班车时刻表:每 10 分钟一班,不管站台多少人、什么时候来人,到点就发。你保证的是「规律性」——不会因为人挤就狂发车,也不会因为没人就永远不发。

心智模型:

防抖(debounce)节流(throttle)
模型电梯等人,人静才关门班车到点发车
关心的事最后一次的结果过程中的规律性
触发密集时全部取消,只等最后固定节奏放行

二、防抖

防抖的核心:新来的调用,作废旧的计时。 翻译成代码:

function debounce(fn, wait = 300) {
let timer = null;
return function (...args) {
clearTimeout(timer); // ① 取消上一个还没生效的定时器
timer = setTimeout(() => fn(...args), wait); // ② 重新计时
};
}

逐行看为什么必须这么写:

  • 为什么要返回一个新函数、用闭包存 timer 因为每次触发都要「看到」同一个 timer 才能取消它。timer 藏在闭包里,新函数每次调用共享这一个变量。
  • 为什么先 clearTimeoutsetTimeout 这一句就是防抖的全部秘密。时间线:t=0 调用 → 定 300ms → t=100ms 又调用 → 取消 t=0 那次、重排到 t=400ms → 只要你一直在敲,它就永远在重排、永远不执行 → 你一停,300ms 后执行最后一次。

这个「重置」动作,就是前面说的电梯重新计时

补一个坑:this 会丢

如果直接用 () => fn(...args)this 就丢了。当防抖函数被用在事件监听里(el.addEventListener('click', debounced)),监听器内部的 this 是那个元素;但 setTimeout 回调里的 this 不是它。所以要先记住:

function debounce(fn, wait = 300) {
let timer = null;
return function (...args) {
const context = this; // 记住调用时的 this
clearTimeout(timer);
timer = setTimeout(() => fn.apply(context, args), wait);
};
}

到这里,一个「停下来才执行」的防抖就好了——这是**后缘(trailing)**版本,也是输入搜索最常用的形态。

进一步:有时候第一次就该立即执行

有个反例:防连点。用户快速双击「提交」按钮,你希望第一次点击就立即执行,后面的点击才被防抖掉。如果只用上面的 trailing 版本,第一次点击也会被延迟 300ms,体验很差。

于是防抖又有了 前缘(leading):一个周期开始先立即执行一次,之后进入等待。

function debounce(fn, wait = 300, { leading = false } = {}) {
let timer = null;
return function (...args) {
const context = this;
const isIdle = timer === null; // 当前没有在等待 = 一个周期的开始
clearTimeout(timer);
timer = setTimeout(() => {
timer = null; // 等待结束,回到空闲
if (!leading) fn.apply(context, args); // trailing 模式在结束时补执行
}, wait);
if (leading && isIdle) fn.apply(context, args); // leading 模式:空闲时立即执行
};
}

注意这里的取舍:leading 模式第一次立即执行、之后防抖;trailing 模式全部延迟、只在安静后执行最后一次。两者通常二选一。

三、从「闸门」长成节流

节流的核心:记上次执行的时间,间隔不够就拦下。

function throttle(fn, wait = 200) {
let lastTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastTime >= wait) { // 距上次执行够久了 → 放行
lastTime = now;
fn.apply(this, args);
}
};
}

这就是时间戳闸门:第一次调用时 lastTime=0now 是很大的毫秒时间戳,now - 0 远超 wait,直接放行并记录;之后 wait 毫秒内的调用全被 if 拦下;时间一到,第一次撞上来的调用放行。

它有个优点:首次立即执行,绝不空等。但也有个隐藏的坑:被拦在闸门内的最后一次调用,被永久丢弃了。

场景很真实:滚动到底部触发「加载更多」,如果最后一次 scroll 恰好滚到了底,却撞在闸门内被丢掉,加载就永远不触发。所以你还需要一个兜底——闸门放行的同时,给「卡在门里」的调用排一个定时器,等间隔到了补执行最后一次。

function throttle(fn, wait = 200, { leading = true, trailing = true } = {}) {
let lastTime = 0;
let timer = null;
let lastArgs, lastCtx;

const invoke = () => {
lastTime = Date.now();
timer = null;
fn.apply(lastCtx, lastArgs);
};

return function (...args) {
lastCtx = this;
lastArgs = args;
const now = Date.now();
const remaining = wait - (now - lastTime);

if (remaining <= 0) { // 间隔已到 → 立即放行(leading)
clearTimeout(timer);
invoke();
} else if (!timer) { // 还没到 → 排定时器,兜住最后一次(trailing)
timer = setTimeout(invoke, remaining);
}
};
}

于是节流从「时间戳闸门」生长成了「闸门 + 定时器兜底」:闸门保证节奏(leading),定时器保证不丢最后一次(trailing)。这已经是生产级的形态了。

四、leading / trailing:把两个概念统一到一个视角

看到这里你会发现,防抖和节流的完整版都有 leading / trailing 两个开关——它们其实是同一对「前后缘」概念:

leading(前缘)trailing(后缘)
防抖周期开始立即执行一次,之后防抖周期结束(安静后)执行最后一次
节流间隔到点立即执行结束时补执行被拦住的最后一次
一句话「先做再说」「做完收尾」

配上场景,选择就顺理成章了:

场景选型原因
输入框实时搜索防抖(trailing)只关心停下来的最终结果
按钮防连点防抖(leading)第一次要立刻响应
滚动加载更多 / 滚动进度节流(leading + trailing)过程中要触发,且不能丢最后
resize 重算布局防抖等 resize 停下来再做
mousemove 坐标跟随节流跟住但不每帧执行

一个补充:如果你要的是「每帧执行一次」的视觉类工作(动画、拖拽预览),那其实还有第三个更专门的工具——requestAnimationFrame。它本质上是「以浏览器帧率为间隔的节流」,浏览器会帮你合并到帧回调里,视觉场景下优先用它。

五、React 自定义 Hook

在 React 里,上面的闭包逻辑要套上 hook 的「生命周期」才能用对——尤其注意两点:回调要始终拿到最新闭包(用 ref 兜底),卸载时要清理定时器(避免 setState 在卸载后执行)。

值防抖:useDebounce(输入框最佳搭档)

最常用的形态是「防抖一个值」——输入内容进 state,经过防抖的值才去发请求:

import { useEffect, useState } from 'react';

function useDebounce<T>(value: T, delay = 300): T {
const [debounced, setDebounced] = useState<T>(value);

useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer); // 依赖变化 → 取消上一次计时 = 电梯重新关门
}, [value, delay]);

return debounced;
}

用法——搜索框只跟着防抖后的值发请求:

function SearchBox() {
const [keyword, setKeyword] = useState('');
const debouncedKeyword = useDebounce(keyword, 300);

useEffect(() => {
if (debouncedKeyword) fetchSearch(debouncedKeyword);
}, [debouncedKeyword]);

return <input value={keyword} onChange={(e) => setKeyword(e.target.value)} />;
}

回调防抖:useDebouncedCallback

把「动作」本身防抖,返回一个稳定引用(useCallback 缓存),不会让子组件因每次渲染拿到新函数而重新渲染:

import { useCallback, useEffect, useRef } from 'react';

function useDebouncedCallback<T extends (...args: any[]) => void>(
callback: T,
delay = 300,
) {
const callbackRef = useRef(callback);
const timerRef = useRef<ReturnType<typeof setTimeout>>();

useEffect(() => {
callbackRef.current = callback; // 始终拿到最新闭包
});

useEffect(() => () => clearTimeout(timerRef.current), []); // 卸载清理

return useCallback(
(...args: Parameters<T>) => {
clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => callbackRef.current(...args), delay);
},
[delay],
);
}

回调节流:useThrottledCallback(首次立即 + 末尾兜底)

把「闸门 + 定时器兜底」完整封装:

function useThrottledCallback<T extends (...args: any[]) => void>(
callback: T,
delay = 200,
) {
const callbackRef = useRef(callback);
const timerRef = useRef<ReturnType<typeof setTimeout>>();
const lastTimeRef = useRef(0);
const lastArgsRef = useRef<Parameters<T>>();

useEffect(() => {
callbackRef.current = callback;
});
useEffect(() => () => clearTimeout(timerRef.current), []);

return useCallback(
(...args: Parameters<T>) => {
lastArgsRef.current = args;
const now = Date.now();
const remaining = delay - (now - lastTimeRef.current);

if (remaining <= 0) {
// 间隔已到 → 立即执行,并取消待决的定时器
clearTimeout(timerRef.current);
timerRef.current = undefined;
lastTimeRef.current = now;
callbackRef.current(...args);
} else if (!timerRef.current) {
// 还没到 → 排定时器,兜住最后一次
timerRef.current = setTimeout(() => {
timerRef.current = undefined;
lastTimeRef.current = Date.now();
callbackRef.current(...lastArgsRef.current!);
}, remaining);
}
},
[delay],
);
}

用法——滚动加载更多,节流 + 不丢最后一次:

const loadMore = useThrottledCallback(() => fetchNextPage(), 200);
useEffect(() => {
window.addEventListener('scroll', loadMore);
return () => window.removeEventListener('scroll', loadMore);
}, [loadMore]);

六、总结

回到开头的问题。节流和防抖不是两个需要背的公式,而是你对同一件事的两个追问:

  • 「我在乎最后的结果吗?」 → 防抖。种子是「重置」——新调用作废旧计时,电梯等人静才关门。
  • 「我需要在过程中跟住吗?」 → 节流。种子是「闸门」——间隔不到就拦下,班车到点才发。

两个概念到完整形态后,会汇聚到同一对开关上:leading(先做再说)和 trailing(做完收尾)。理解了这对开关,你就能按场景自由选择,而不是背模板。

聊聊函数柯里化:来源、解决的问题与优缺点

· 阅读需 7 分钟

柯里化(Currying)是函数式编程里最常被提起、也最常被误解的概念之一。面试喜欢问,但很多人只背得住一句「把多参函数变成单参函数的链式调用」,说不清它从哪来、为什么存在、以及它到底划算不划算。

这篇文章把三件事讲透:它是谁发明的它解决什么问题它的代价是什么

手写 JS 系列:从 apply 到 bind,最后手写一个 Promise

· 阅读需 12 分钟

手写系列的意义不是「记住答案」,而是把 API 背后那句被藏起来的话重新说出来:「改变 this」本质是什么?「异步」到底是怎么串起来的? 顺着 apply → call → bind → Promise 这条线写下来,你会发现它们是一脉相承的——前面三个解决的是一件事(函数调用时的 this 和参数),最后一个把「回调」正式升级成了「一等公民」。

vue实现input强制输入Number类型

· 阅读需 2 分钟

需求

如果想要实现input输入Number类型,又不想用难看的type='number'怎么处理

<el-input style="width:200px" :maxlength="4" @keyup.native="isNumber(form,'recruitsCount')" v-model.number="form.recruitsCount" placeholder="请输入数字,不能为空"></el-input>

isNumber(obj,key) {
let isnumber = new RegExp('^[0-9]\\d*$')
if (!isnumber.test(obj[key])) {
obj[key] = null
} else {
return true
}
},

测试代码

<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<title>Document</title>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.js"></script>
</head>

<body>
<div id="app">
<input style="width:200px" :maxlength="4" @keyup="isNumber(form,'count')" v-model.number="form.count" placeholder="请输入数字,不能为空"></input>
</div>
</body>
<script type="text/javascript">
var app = new Vue({
el: '#app',
data: {
form: {
count: null
}

},
methods: {
isNumber(obj, key) {
let isnumber = new RegExp('^[0-9]\\d*$')
if (!isnumber.test(obj[key])) {
obj[key] = null
} else {
return true
}
}
}
})
</script>

</html>



说明

  • 配合.number修饰符实现纯数字输入
  • .nmber的特点是将字符串转为num型,如果转换结果是NaN,则原样输出。
  • 因此如果第一个字符输入非数字,那么就会变成字符串,根据强制类型转换规则,首字母是数字才能转换成功,如1f这种就会转换为1,因此就出现预想中的输入非数字转换为数字的效果。
  • 因此关键是第一个字符不能为非数字,配合keyUp事件,检测首字母是否是数字即可,检查全部也无所谓,因为keyup事件发生时,input事件未发生,input又经过类型转换,因此能完美运行

编程日志(08)—vue实现plupload分片上传

· 阅读需 5 分钟
<!-- plupload封装 -->
<template>
<div class="supload">
<div class="clearfix list-picture" v-if="fileList.length>0&&listType=='picture'">
<div v-for="ff,index in fileList" class="pic-bx small">
<img :src="ff.showUrl">
<div class="cl-bx">
<i class="el-icon-close" @click="deleteFile(ff,index)"></i>
</div>
</div>
</div>
<div class="plupload clearfix" v-show="fileList.length<maxFile">
<template v-if="listType=='text'">
<div class="text_trigger">
<div :id="trigger_id" >
<i class="el-icon-document" ></i>
<span>{{triggerText}}(小于{{maxSize}}Mb)</span>
</div>
</div>
</template>
<template v-if="listType=='picture'">
<div class="fm-up" :id="trigger_id">
<div class="cc">
<i class="el-icon-circle-plus-outline"></i>
<p>点击选择</p>
<span>支持jpg/png格式</span>
<span>560px×400px</span>

</div>
</div>
</template>
</div>
<div class="pgs" v-if="progress>0&&listType=='text'">
<el-progress :text-inside="true" :stroke-width="20" :percentage="progress" status="success"></el-progress>
</div>

<div class="list-text" v-if="listType=='text'">
<template v-for="ff,index in fileList">
<div class="clearfix item-text">
<!-- <div class="progress" :style="{'width':ff.progress+'%'}"></div> -->
<div class="title">{{ff.name}}</div>
<div class="close" @click="deleteFile(ff,index)">删除</div>
</div>

</template>
</div>

</div>
</template>

<script>
/**
* 强制multi_selection为false使其只能逐个上传
*
*/
/**
* tip:
* 为了操作方便fileList以数组形式传入props,直接操作fileList可以实现数据双向绑定
* 并不建议这样去做,父子组件之间尽量保持单向数据流
* 因此可以暂不向外提供文件删除上传的回调入口
*/
/*
xlsx等文档可以在拓展过滤为zip的情况下可选的bug是因为xlsx本质是个压缩包,将xlsx拓展名改成zip再看看
*/
import plupload from 'plupload'
import {guid,getExtName} from '../../lib/util.js'

import config from '../../config/config.js'
export default {
data() {
return {
trigger_id:null, //触发按钮的ID属性
plup:null, // plupload实例对象
progress: 0, // 单文件上传进度
option: null
};
},
props: {
accept: {
type: String,
default: 'image/png,image/jpeg,audio/mp3'
},
// 支持的拓展名以逗号隔开jpg,gif,png,bmp,mp4
extensions:{
type: String,
default: 'mp4'
},
maxFile: {
type: Number,
default: 1
},
maxSize: {
type: Number,
default: 512 //mb
},
// 文件列表
fileList: {
default(){
return []
}
},
// text,picture 以picture形式呈现时,如果超过可上传数量,上传界面将消失
listType:{
default: 'text'
},
params:{
type:Object,
required:true
},
triggerText:{
default:'选择文件'
}
},
computed:{

},
components: {

},

methods: {
transGetUrl(url) {
return config.imgHost + url
},


deleteFile(ff,index){
this.fileList.splice(index,1)
},

},
watch:{
extensions(ne,olg){
this.option.filters.mime_types=[{
title: 'files', extensions: ne
}]

this.plup.destroy()
this.plup = new plupload.Uploader(this.option);
this.plup.init()

}

},
mounted(){
const $this = this;
this.option = {
runtimes: 'html5,flash,silverlight,html4',
url: config.downloadHost+'upload/pluploadUpload',
//url: '/preschool/upload/pluploadUpload',
browse_button: this.trigger_id,
chunk_size: '2048kb',
multi_selection:false,
filters: {
mime_types: [{ title: 'files', extensions: this.extensions }],
max_file_size: this.maxSize+'mb',
prevent_duplicates: true
},
multipart:true,
multipart_params:this.params,
init: {
FilesAdded: function(up, files) {
up.start();
},
UploadProgress: function(up, file) {
$this.progress = file.percent;
},
BeforeUpload: function(up,file) {},
FileUploaded: function(uploader,file,res){
// response,responseHeaders,status
if (res.status==200){
let result = JSON.parse(res.response);
if (result.code==0){
setTimeout(function(){
$this.progress = 0;
$this.fileList.push({
name:file.name,
// url: result.data.replace(/(.*)(pluploadDir)/g,config.imgHost),
url: result.data,
showUrl: $this.transGetUrl(result.data),
type: getExtName(result.data)
});
},500);
}
} else {
}
},
Error:function(up,err){},
Refresh: function(up){},
Destroy(up){},
OptionChanged:function(up,option_name,new_value,old_value){}
// Error: this.error
}
}
this.plup = new plupload.Uploader(this.option);
this.plup.init();
},
beforeMount(){
this.trigger_id = guid();
}
};
/**
* 额外参数传递:
* 1.设置multipart为true
* 为true时将以multipart/form-data的形式来上传文件,为false时则以二进制的格式来上传文件。
* 2. 设置multipart_params:{userId:1024}
* 方式二
* 设置init的BeforeUpload(up,file){up.setOption("multipart_params",{userId:1024})}
* 也可以up.setOption({'multipart_params':{userId:1030}});这样可以方便设置多个参数
* 像 up.setOption({'multipart':true,'multipart_params':{userId:1032}})
* 在某些情况下可能更方便
*/
</script>

调用方式

<plup-vue :file-list="fileListForAttach" list-type="text" :params="extendParamsForUploadComponent" :extensions="format" :maxSize="1024">
</plup-vue>

CSS

/*上传组件*/
.supload{
line-height: 30px;
.pgs{
line-height:20px;width:600px;
}
.list-text{
line-height:20px;width:600px;
.item-text{
line-height: 30px;
height: 30px;
background: #ededed;
position: relative;
margin: 5px auto;
.progress{
position: absolute;
height: 100%;
width: 1%;
background: green;
top: 0;left: 0;
opacity: 0.2;
transition: width 0.5s;
-moz-transition: width 0.5s;
-webkit-transition: width 0.5s;
-o-transition: width 0.5s;

}
.title{
float: left;height: inherit;line-height: inherit;padding: 0 10px;
width: 500px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.close{
float: right;height: inherit;
line-height: inherit;padding: 0 10px;
cursor: pointer;
color:$colorRed
}
}
}
.list-picture {

.pic-bx{
float: left;
margin: 10px 10px 10px 0;
border:1px dashed #ededed;
img{width: 100%; }
position: relative;
}
.cl-bx{
width: 100%;height: 25px;background: rgba(1,1,1,0.5);
position: absolute;right: 0;top: 0;
text-align: right;
line-height: 25px;
i{
font-size: 15px;
color:#fff;
margin-right: 3px;
cursor: pointer;
}
}
.small{
width: 240px;height: 180px;overflow: hidden;
border-radius: 4px
}
}
.plupload{
.text_trigger{
display: inline-block;
position: relative;
color:$colorBlue;
cursor: pointer;
}

}

}
.fm-up{
width: 240px;height: 180px;
text-align: center;
cursor: pointer;
// border: 1px solid #03A9F4;
.cc{
height: 100px;width: 200px;
line-height: 20px;
margin: auto;
margin-top: 40px;
}
color:#dcdcdc;
border: 1px solid #dcdcdc;
i{font-size: 25px;}
border-radius: 4px;
p{line-height: 20px;}
span{font-size: 12px;line-height: 20px;}
}

pkg-node打包可执行文件工具

· 阅读需 6 分钟

前言

最近在研发基于electron的桌面应用,其中涉及到外设交互部分需要使用dll实现,在经历了艰难的环境配置之后,最后由于electron内存管理问题导致应用崩溃使得electron+ffi实现动态链接库调用这条路无法走通。当时还剩下两种方案可以尝试:

  1. 自己创建dll封装原生dll调用,感觉成功几率很低。
  2. 写node addons编译成node插件,需要较强c++功底,难度较大,且最后成功概率不高。

为什么坚持了两个星期还不放弃是因为node+ffi这条路已经走通了,证明第三方的dll是没有问题的。所以试着能不能解决electron中出现的问题。

当electron不能实现的事实逐渐明朗,我开始转移方向。首先想到的是nwjs+ffi的方案,随后迅速想到为什么不使用node自己实现呢?将此部分功能单独分割成为第三方应用,通过某种通信方式实现(写缓存、数据库、http等)。最不济的情况是我为每台终端部署node服务(此应用是B2B,需要实施,不需要自己安装,这一点帮了大忙)

最好的方式是不需要安装node等一堆东西,直接是可执行文件。因此我开始寻找node打包成可执行文件的方法。

编程日志(02)—同一浏览器多用户登录session冲突问题

· 阅读需 2 分钟

账号登录冲突

在后端程序中,往往将session和成功登录的用户id进行绑定来保持登录状态,这样前后端交互时就不用显示传递用户ID来判断是哪个用户,并通过设置session过期时间来保障账户的安全。

session的值会被后端设置在cookie,前端无需做什么特别操作,正常请求接口即可。由于session就相当于用户id,当同一个浏览器存在多个标签页,A页是A用户,B页是B用户,那么session绑定的其实是后登陆的那一个用户的id,因为这种绑定是在登陆接口处理的。这样的话假如请求的数据(比如查看个人信息)是基于session拿用户id的,就会导致A用户拿到B用户的数据。

编程日志(01)--cocosCreator实现类似jQuery的ajax

· 阅读需 3 分钟

为什么在cocosCreator中不使用axios等工具

cocosCreator版本:2.0.7

在游戏场景中少不了要使用网络交互,在以往的编程中,用jQuery实现或者axios实现并不是很难,从论坛的反馈来看,cocos对ES6的支持尤其是promise,经过长时间的更新似乎依然没有很好地完善,由于开发时间限制,这里就没有去体验这些说法直接使用回调函数避免兼容性问题原始实现ajax请求。

深入了解解构

· 阅读需 2 分钟
let o = {a:{b:1,c:2}};
let {a:{b}}=o;
b // 1

a在这里是作为模式,并不会被赋值

对象的解构赋值

对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。

let { bar, foo } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"

let { baz } = { foo: "aaa", bar: "bbb" };
baz // undefined

上面代码的第一个例子,等号左边的两个变量的次序,与等号右边两个同名属性的次序不一致,但是对取值完全没有影响。第二个例子的变量没有对应的同名属性,导致取不到值,最后等于undefined

如果变量名与属性名不一致,必须写成下面这样。

let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"

let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj;
f // 'hello'
l // 'world'

这实际上说明,对象的解构赋值是下面形式的简写(参见《对象的扩展》一章)。

let { foo: foo, bar: bar } = { foo: "aaa", bar: "bbb" };

也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。

let { foo: baz } = { foo: "aaa", bar: "bbb" };
baz // "aaa"
foo // error: foo is not defined

上面代码中,foo是匹配的模式,baz才是变量。真正被赋值的是变量baz,而不是模式foo。

看一个复杂的例子

const node = {
loc: {
start: {
line: 1,
column: 5
}
}
};

let { loc, loc: { start }, loc: { start: { line }} } = node;
line // 1
loc // Object {start: Object}
start // Object {line: 1, column: 5}

上面代码有三次解构赋值,分别是对loc、start、line三个属性的解构赋值。注意,最后一次对line属性的解构赋值之中,只有line是变量,loc和start都是模式,不是变量。