-目录-
-- 扯dan
-- JavaScript 中的数据类型与分类
-- 数组的解构
-- 数组的解构成功与完全解构
-- 数组解构当中默认值问题
-- 对象的解构
-- 对象解构当中默认值问题
-- 字符串解构
-- 扯dan+1
温馨提示:阅读此文大约需要7、8、9、10...分钟
记得第一次创业时,曾经有幸和GameLoft的几个老员工一起开发游戏,在上地三街的一个狭小阴暗的厂房里,对面就是当时火遍大江南北的"JJ斗地主",梦想着有一天能像对面公司一样牛X。虽然后期有些不愉快,但并不影响我对主程能力的崇拜,那位主程跟我说过一句至今都觉得很经典的话"程序写的好不好,就看内存玩的好不好",当时的项目是用C++写的,过去的事情好像啰嗦的有点多了…"收!"
(华丽又尴尬的转折来了)那么我们接下来先聊一聊Js中的数据类型在内存中的分配规则。
JavaScript中的数据类型大致有哪些,怎么分类?
应该大致分为两类:基础类型、引用类型
Undefined、
Null、
Boolean、
Number、
String、
Symbol(ES6中新增的一种基本类型,表示独一无二的值)
这些都是基础类型,而
Function
Object
这两个可以说是引用类型。
内存中是分为栈内存和堆内存的,栈的概念就像是一个过去家里烧火用的蜂窝煤炉子,每个栈内存的大小都是固定的,所以针对于基础类型足够使用了,好比如每次从炉子上面放入一个蜂窝煤,每一个蜂窝煤的大小都是固定的,所蕴含的能量也是固定的,而对象则不是,对象中的属性是不断添加的,所以对象需要保存在堆内存中,栈中只是保存了对象在堆内存的地址索引,其实说白了也就是C++中的指针。下面这个图可以很好地说明:
看完图,我们来看个例子加深下理解:
// 只改变的是栈内存的
namespace x {
let a = "xxx";
let b = a;
a = 'yyy'
console.log(b); // xxx
}
// b是一个栈内存,指向的是a在堆内存的地址,也可以理解为b是a的一个引用,同a指向了同一片内存区域,a和b在栈中存的内容是一样的
namespace x1 {
let a = {
name: 'xxx',
}
let b = a;
a.name = "yyy"
console.log(b.name); // yyy
}
再来看一个例子:
var age = '12'
var people = {
name: 'llp'
}
function Hello(num,obj) {
num = '18'
obj.name = 'kjj'
console.log(num, obj)
}
Hello(age, people)
console.log(age, people)// 12 kjj
在说明这个之前,我们可以先回顾一下上一篇提到的块级作用域,函数Hello就是一个块级作用域,那么块级作用域里的变量只在对应的作用域里生效,出了作用域,没有被引用的变量就会被回收,这里的函数参数num、obj是形式参数,实际调用时传入的实际参数为两个栈内存的地址,这两个形式参数的栈内存会被两个实际参数的栈内存给赋值,所以Hello方法内改变num仅仅只是改变了函数内的值,而没有改变外部的age,相反,形式参数obj所引用的实际堆内存地址发生变动,就影响了外部的变量,所以输出的内容为people是会发生变化的,算了,知道你们肯定懒得去敲,那我帮你敲一遍验证一下:
以上都是按照我的理解尽可能的通俗阐述,不知道你看懂了么?当然,以上内容并不是我们最近要总结的ES6的主线内容,接下来的解构赋值却是,而且可以说是相当好玩。
ES6的解构方式
解构(Destructuring)顾名思义,将一种结构的数据解析成另一种结构,两种结构可以也相同可以(一定约定下)不同,而我所总结的目的也就是想整理和阐述下怎么个“解法”,都有哪些个“约定”。开始进入主题~
先来看定义:“ES6允许按照一定的模式,从数组和对象中提取值,对变量进行赋值,这被称为解构”。那么我们就重点从数组和对象这两个主要部分来进行区分整理。
数组
解构成功与完全解构(这两个不是对立的关系,是子集的关系)
我们先来看看解构成功情况下的例子:
例1:
let[a, b, c] = [1, 2, 3];
例2:
let[a, [[b], c]] = ["aa", [["bb"], "cc"]];
console.log(a,b, c);// aa bb cc
例3:
let [ , , c] =["aa", "bb", "cc"];
console.log(c);// cc
例4:
let [a, ...b] = [1,2, 3, 4];
console.log(a,b);//1 [2,3,4];
相必你一定还会有很多疑问,别急,以上你看到的只是完全解构的,对应的还有"不完全解构的",即为"="左侧的内容只有部分能够匹配右边的部分,这样的解构依然是有效且成功的
例5:
let [x, y, ...z] =['a'];
console.log(x);// a
console.log(y);// undefined
console.log(z);// []
那么未成功被解构赋值的变量则为undefined,更多的不完全解构的例子如:
例6:
let[a] = ['aa','bb'];
console.log(a);// aa
例7:
let [a, [b], d] =[1, [2, 3], 4];
console.log(a,b,d);// 1 2 4
有没有觉得看得云里雾里,有就对了,例子的变种千千万,但存在必有道理,数组的解构道理在于什么呢?综上例可以看出,数组的解构,是一种对格式要求极其严谨的赋值操作,专业讲,这叫“模式匹配”,即左右两边均为相同的模式(注意对顺序也是一个模式中的影响因素),即可进行解构,只是解构结果完全不完全的区别,但如果对于数组解构中,等号右侧是一个非可遍历的解构,那么就会解构失败,如
例8:
let [a] = 1;
let [a] = false;
let [a] = NaN;
let [a] = undefined;
let [a] = null;
let [a] = {};
数组解构当中的默认值问题
默认值为undefined时需要注意的一些问题:
数组的解构赋值是允许指定默认值的,你甚至可以指定默认值是undefined,但要注意的是ES6中因为内部约定"==="是一个严格相等的运算符,用于判断一个位置是否有值,在解构赋值过程中,右侧的数组成员只有在严格等于undefined的情况下,指定的默认值才会生效,比如:
let[x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b'] =['a', undefined]; // x='a', y='b'
let[x, y = 'b'] = ['a', null]; // x='a',y=null
第三个例子中的null不严格等于undefined。
默认值为函数时需要注意的一些问题:
以上是一个默认值为undefined的问题,再来看一个默认值为函数的问题。如:
例9:
function f() {
console.log('a');
}
let [x = f()] = [1];
例10:
let x;
if ([1][0] ===undefined) {
x = f();
} else {
x = [1][0];
}
因为当默认值是函数时,函数是惰性的,也就是在需要用时才会执行,所以例9、10这两种写法表达的是一个意思,是不是感觉代码能简介很多。当然还是要提醒一下,注意"函数是惰性的!"
对象
不难看出,数组结构的解构是与元素顺序有一定的关系,而对象则不然,简而言之,对象解构的特点就是“键值匹配”的模式;
例1:
let exp:{ a:string }= { a: 'aaa' };
console.log(exp) //"aaa"
浏览完例1,我们在来对“键值匹配”进行解读,其本质是先找到同名属性,其次,再赋值给属性对应的变量,针对于上例来解释,被改变的是对象exp.a所对应的变量,而a仅仅只是一个索引赋值的匹配模式,即所谓的键。
再来看一个阮老师简洁又严谨的例子:
例2:
const node = {
loc: {
start: {
line: 1,
column: 5
}
}
};
let { loc, loc: , loc: { start: { line }} } = node;
line // 1
loc // Object
start // Object
阮老师的注解:“上面的代码有三次解构,分别是对loc, start, line三个属性的解构赋值,注意,最后一次对line属性的解构赋值中,只有line是变量,其他loc, start都是模式,不是变量。”
例3:
let { log, sin, cos} = Math; // 将Math对象的对数、正弦、余弦三个方法,复制到对应的变量上,使用起来方便了很多。
例4:
let arr = [1, 2, 3];
let = arr;
first // 1
last // 3
由于数组本质是特殊的对象,因此可以对数组进行对象属性的解构。上例就是对数组进行了对象的解构,数组arr的键对应的值是1,[arr.length-1]就是2键,对应的值是3。
在这里需要插一句,对象的解构赋值是一个浅拷贝,也就是我在本文最开始所介绍的内存中的拷贝关系,什么?不信?好吧,就知道你有点蒙,请看下图中我敲的一个例子截图,先定义了一个变量a, 这是一个具有一定结构的对象,紧接着按照上述的对象解构赋值方式,我进行了一波赋值,然后新创建的未命名变量按照“键值匹配”的规则被赋值了,随后我改变了被赋值的未命名变量(先后改变并输出了其中的基础类型变量和引用类型变量),结果很显然,这是一个浅拷贝,a的基础类型变量ab没有发生变化,但引用类型变量ac对应的acb被改变了,这意味着a.ac的堆内存发生了变化,好了,解释到这里,我觉得我都已经有点啰嗦了。
按照分析数组解构的思路,接下来该说一下对象解构过程中的默认值问题,不过,也确实没什么好说的,和数组解构要求一致,对象属性值严格等于undefined 时,默认值方可生效,如:
例5:
let = ;
// x = 3
let = ;
// y = null
字符串
字符串的解构赋值是将字符串先转换为一个数组的对象,那么进而就可以进行数组的解构赋值操作,如
例1:
const [a, b, c] ='abc';
a //"a"
b //"b"
c //"c"
例2:
let ='hello';
len // 5
这样的解构赋值也是成立的,因为数组也是特殊对象,其中也是有length这个属性的。
看到这里,基本上可以做一个本篇的结束语了,这么多范例和用法,其实仔细想想,我们的生产环境中不乏有他们的身影,当函数需要返回多个值,需要构建临时对象、当解析服务器返回的json时需要进行拷贝构造、当函数传参需要进行指定形参默认值、当遍历需要只取键名或只取键值……等等,是不是觉得很神奇?其实他们基础就是如上,希望对你能有帮助。
有些看过或部分没耐心看完的朋友多半会发此一问(事实上确实很多朋友问我),这么简单的解构赋值会用就行了,有必要扣得这么仔细或是累么?对此疑问我个人是这么认为的,首先大多数的图书,或是现有市面上的专业介绍的都是比较复杂,章节很多,但实际上我们使用的并不是那么全面,但做技术和做学问是不能分开的,我们可以误解学者们的考究,但不能误解科学学习的态度,所以提炼、剥离与总结是我们刚需开发者应该且必做的过程。其次针对于分享笔记有没有必要这么写的这么费劲,我是觉得,首先既然是分享,那么受众群体必然是专业技能的持有者或是学习者,那么我必须严格要求自己会仔细学习一遍两遍…甚至多遍,这种反复学习探究与核实的过程其实是对自己基础学科知识的夯实与打磨,与其说是分享给别人,更多的是提高了自己,何乐而不为呢?当然如果你不认同,那也可以出门左转哈,谢谢,欢迎大家参与分享讨论,多多给予批评和支持。再次谢谢!
下期预告:
ES6 学习笔记 - Js的异步处理与应用
领取专属 10元无门槛券
私享最新 技术干货