你了解JavaScript中的变量吗?
什么是变量
变量是用来存储值的容器,可以存储任意类型的值,并且存储的值是可以改变的(松散类型)。 注意: 变量是存储值的,它并不代表值本身,也就是说变量实际上是用于保存值的占位符。
类型
首先我们要明确一个概念:变量是没有类型的,值才有类型,所谓的变量的类型指的是其绑定的值的类型。
类型的分类
基础类型:undefined、null、boolean、number、string、symbol 复杂类型:Object、Array、Function
当然有其他的说法:Array、Function 都是属于 Object 的子类,所以复杂类型只有 Object 一种。 个人觉得子类这种说法和有多个复杂类型不冲突,毕竟类型是类型,类型的实现是实现,两者并没有必然的因果关系。
undefined
变量未持有值的时候,变量的值就是undefined。
看看下面这段代码:
var a;
console.log(a); // undefined
console.log(b); // Uncaught ReferenceError: b is not defined
console.log(typeof a); // undefined
console.log(typeof b); // undefined
输出a
和其类型,值是 undefined
,这个大家都没有异议。
但b
就很坑了,输出b
报错,这里应该提示b
不存在(not found 之类的),而实际上的错误信息是b is not defined。
typeof
就让人更头疼了,一个不存在的变量的类型是undefined
???这个逻辑说明 typeof 有一个特殊的处理机制:如果什么都不是就返回 undefined
这就让很多人都误以为b
不存在等于b is undefined。然后就把undefined与未声明对等。但是这个是错误的。
一个神奇的操作 在非严谨模式下,可以为全局表示符undefined赋值(what the f***)
function setUndefined(val) {
undefined = val;
}
setUndefined('a');
console.log(undefined); // undefined
Number
JavaScript 跟其他语言一个很大不同点就是:没有区分整数和小数,所有的数字都属于**数字(Number)**类型。 Number 是基于IEEE 754标准来实现的,使用的是“双精度浮点数”的格式(64 位的二进制)。 基于这套标准的角度,JavaScript 中其实是没有“整数”,所谓的“整数”其实就是没有小数位的浮点数:
42 === 42.0; // true
为什么这么理解呢? 因为在区分整数与浮点数的语言中,两种数据类型的存储方式是不一样的,整数的存储是直接存储二进制数的(符号位+数值),浮点数是分段存储的(符号位+指数+分数)。两个者从根本上就存在差别,而 JavaScript 中全部都是以浮点数的方式存储的。
浮点数的内存分布
数字的显示
科学计数法 一般来说,太大的数字或者太小的数字,会使用科学计数法来记录和显示。
let a = 4e11;
console.log(a); // 400000000000
toExponential 可以将数字转换成科学计数法显示。
let b = 4400000000000;
console.log(b.toExponential()); // "4.4e+12"
toPrecision
可以指定要显示的有效数个数,个数大于实际位数时会自动补0
,会根据显示的位数进行四舍五入。
let c = 32.45;
console.log(c.toPrecision(1)); // "3e+1"
console.log(c.toPrecision(2)); // "32"
console.log(c.toPrecision(3)); // "32.5"
console.log(c.toPrecision(5)); // "32.450"
注意: 上面的俩各个方法和toFixed
方法返回的都是字符串不是数字。
let d = 32.45;
console.log(d.toExponential()); // "3.245e+1"
console.log(d.toPrecision(3)); // "32.5"
console.log(d.toFixed(1)); // "32.5"
语法问题
下面的写法是否有效?
88.toFixed(3);
这个写法是无效的,因为运算符.
会被优先识别成常量数字的一部分(88.是一个有效的数字)。上面的代码会报错:
Uncaught SyntaxError: Invalid or unexpected token
那要怎么写呢?
(88).toFixed(3); // "88.000"
88..toFixed(3); // "88.000"
88 .toFixed(3); // "88.000" 88和.中间是空格
精度问题
精度问题是因为数字存储的时候需要装换成二进制,双精度浮点数的长度是64位,如果小数部分需要的位数大于分数的位数(52位),那么就会出现精度问题。
NaN
NaN表示”不是一个数字“(not a Number),是IEEE 754中的规定的是一类值,表示未定义或不可表示的值,IEEE 754中用指数部分全为1并且小数部分非零来表示NaN。 记住NaN表示的是一类值不是一个值,我们看下NaN的特性:
NaN === NaN; // false
NaN != NaN; // true
这里可能会有一个疑问,NaN指的是一类值,那么NaN != NaN
应该不会永远为真(会不会出现两个NaN一样?),这里主要是因为IEEE 754规定了NaN与任意值都不能相等,不过IEEE 754并没有说明设计NaN的原因,个人理解NaN的定义是为了保证数学运算的正确性,毕竟IEEE 754对NaN的定义是未定义或无法表示的值,在数学运算中,假如我们做了两个复杂的数学运算并且想比较他们的值是否相等,那么当且仅当两个运算结果得到的数字是一致的时候,他们才是相等的,如果计算错误或者输入值有问题,导致计算过程中出现NaN,那么计算的结果也会是NaN,两个返回结果比较应该是不等的,这样可以避免很多计算错误导致的问题。(关于NaN的说法,你有没有其他看法,可以在评论区一起交流)
因为NaN不等于任意值,所以它也是JavaScript中唯一一个非自反的值(非自反:x === x不成立)。
坑 第一个: not a Number这个意思很容易引起误会,例如:
let d = 3 / "aaa"; // NaN
console.log(typeof d); // "number"
怎么理解上面代码的意思?? 不是数字的数字是数字类型。 所以我们不能就字面意思来理解,应该将它理解成”无效的数字“更贴切。
第二个:
JavaScript提供一个方法来识别NaN:isNaN
,这玩意也是个坑,它的作用不是识别传入是的参数是不是NaN,而是识别传入的参数“既不是NaN,也不是数字”。
let e = 3 / 'aaa';
let f = 'aaa';
console.log(isNaN(e)); // true
console.log(isNaN(f)); // true
这次倒是需要从字面意思上去理解:'aaa'和NaN都符合not a Number的定义。
不过es6新增的Number.isNaN
是可以正确识别的:
console.log(Number.isNaN(f)); // false
当然我们也可以自己实现这个方法:
if (!Number.isNaN) {
Number.isNaN = function(num) {
return num != num;
// return typeof num === 'number' && window.isNaN(num);
}
}
String
很多人认为字符串就是字符串数组,虽然两者有很多相似的地方但是,其实大不相同。
let string = 'demo';
let arr = ['d', 'e', 'm', 'o'];
string.length; // 4
arr.length; // 4
string[0]; // d
arr[0]; // d
string[3] = 'f'; // demo
arr[3] = 'f'; // ['d', 'e', 'm', 'f']
string[0]
这种写法不一定是合法的,在IE6中这个写法是非法的,要使用string.charAt(1)
。 为什么现在大部分浏览器的都允许这种写法呢? ES5对字符串的描述: 字符串类型是有限的零个或多个 16 位无符号整数值(“元素”)的有序序列 也就是说在ES的中,字符串实际上是被当成一个有序的序列来处理,也就是一个类数组,使用String
关键字来定义一个字符串就可以看到:
var s = new String('demo')
打印s
可以得到以下的结果:
{
0: "d"
1: "e"
2: "m"
3: "o"
length: 4
__proto__: String
[[PrimitiveValue]]: "demo"
}
当然,有人会说,这是定一个字符串对象,不是字符串。是的,s
是一个字符串对象,但是它其实是等价于var ss = "demo"
,因为JavaScript在操作ss
的时候,也会自动将它转换成new String("demo")
的包装对象。
- 数组的元素是可修改的,但是字符串不行。因为字符串对应未知的属性并不是可读或配置的。简单说:字符串是不可变的。所有字符串的成员函数都是返回新的字符串,不会变更原有的字符串。
为什么要做一个不可变的设计 个人认为有这几点:
String
是一个基础数据类型,在ECMAScript中String是一个原始数据类型,而原始数据类型是不可变的。- es对字符串的定义是“零个或多个16位的字符的有序序列”,空字符串就是代表零,那么如果在一个非零的字符串中将某一位置空,那就很容易引起误会,看到的字符串与实际的长度不匹配。(类似iOS上八分之一个空格的问题)
- 字符串在初始化的时候就已经申请了固定长度的内存空间(“demo”在初始化的时候会申请长度为4个字符的空间)如果可变,那么我在原来的字符串上增加,这就意味着要扩展原来已经申请的内存空间。es为了保证字符串的效率,字符串的存放是在一段连续的内存中的,如果现在追加字符串的长度,就要申请新的内存空间,追加的内存空间不能保证跟原来是连续的,这样会对字符串的寻址产生很大的影响。
所以字符串的修改操作是这样的:
var s = 'demo';
s = s + ' demo';
- 申请一个长度为4个字符的内存空间,存储demo
- 把s和demo绑定
- 申请一个长度为9个字符的内存空间,把s + ' demo'存储进去
- 把新的字符串与s绑定
变量在内存中
JavaScript的变量存储主要是在栈区和堆区(其他的还有代码区,C和Java还有全局区)
看看变量是怎么存储的: 定义一些变量:
var num = 10,
bol = true,
str = "abc",
obj = {a: 2},
arr = [1,2,3];
function demo(){
var a=1,
b='2',
c={c:3};
};
上面的变量在内存中的情况:
可以看到越先定义的遍历越先入栈,函数是最先进去的,因为被提升了。
为什么内存要分栈区和堆区: 主要是为了保证效率和速度 栈区:
- 栈内存的大小是固定的(有最大限制),遵循后进先出的原则。存储的都是在编译时就可确定大小的内容。
- 存取速度快,仅次于寄存器。(整个栈都是连续的有关联的)
- 可以实现快速回收:修改栈顶的指针即可完成自顶向下的销毁。 缺点:大小是固定的,不好扩展
堆区:
- 堆相对于栈来说,堆的空间就大很多,用来存储运行时大小不能确定的内容(大小可变化)
- 堆在运行时会动态的申请内存空间,由系统进行分配,动态分配和销毁都是比较占时间的,所以相对来说效率较低
- 堆的自由度更高,不需要管空间(要多大)和时间(停留多久)的问题
总结
本文总结了下一些基础数据类型的相关的点和一些个人的看法,变量是我们最常用的,也是最容易忽略的,相信大家多多少少都遇到过由变量引起的问题,了解下一些基础的点,可以让我们更好的避开奇奇怪怪的坑。 关于本文的话题,大家有没有其他看法,可以在评论区跟其他人互动下。
转载自:https://juejin.cn/post/7287773353309388855