这篇文章, 来聊聊 JS 中的数据类型与变量. 这是在学习 JS 时最基础的一类问题, 但却很重要. 希望我的分享有帮助到你.
文章开头, 我先提几个面试中遇到的问题:
比如:
如何理解参数的按值传递?
什么是暂时性死区?
什么是变量提升?
全局变量和 Windows 的属性有什么区别? 为什么?
... ...
这篇文章的风格, 在分析知识点的同时, 插入一些我经历过的面试题.
基本数据类型
在 JS 中, 基本数据类型有 6 种, 即数值, 字符串, 布尔值, null,undefined,Symbol.
对于基本数据类型, 我们需要明白的是: 基本类型在内存中的存储方式是栈. 每一个值都是单独存放, 互不影响.
基本类型都是按值访问的. 在比较时, 按值进行比较:
1 === 1 // true
引用数据类型
引用类型的值保存在堆中, 而引用是保存在栈中.
引用类型按引用访问. 在比较时, 也比较的引用:
{} === {} // => false
参数的传递方式
在 JS 中, 参数可以是任何类型的值, 甚至可以是函数.
这里要分析的是参数是以哪种类型传递的? 引用类型还是基本类型?
先看一个基础的例子:
- var out_num = 1;
- function addOne(in_num) {
- in_num += 1;
- return in_num;
- }
- addOne(out_num); // => 2
- out_num // => 1
这个例子中, 我们给 addOne() 函数传递一个实参 out_num, 这个时 out_num 会传递给 in_num, 即内部存在着 in_num = out_num 的过程. 最后我们看到的结果是 out_num 并没有被函数改变, 说明 in_num 和 out_num 是两个在内存中独立存放的值, 即按值传递.
再来看一个变形:
- var out_obj = { value: 1 };
- function addOne(in_obj) {
- in_obj.value += 1;
- return in_obj;
- }
- addOne(out_obj); // => { value: 2 }
- out_obj // => { value: 2 }
问题来了? 函数参数不是按值传递吗? 为什么这里函数内部的处理反映到外部了? 这是一个超级超级超级的理解误区.
首先, 我们还是得摆正观点, 即函数参数是按值传递的. 那这里怎么理解呢? 对于引用类型而言, 前面说引用类型分为引用和实际的内存空间. 在这里 out_obj 依旧传递给 in_obj, 即 in_obj = out_obj ,out_obj 和 in_obj 是两个引用, 它们在内存中的存储方式是独立的, 但是它们却指向同一块内存.
而 in_obj.value = 1 则是直接操作的实际对象. 实际对象的改变, 会同步到所有引用这个实际对象的引用.
你再来看这个例子, 或许就会更清晰一些.
- var out_obj = { value: 1 };
- function addOne(in_obj) {
- in_obj = { value: 2 };
- return in_obj;
- }
- addOne(out_obj); // => { value: 2 }
- out_obj // => { value: 1 }
你只要抓住一点: 对象的赋值就会造成引用指向的实际对象发生改变.
如何判断数据类型
判断数据类型, 通常有三种具体的方法:
1,typeof 操作符
typeof 操作符返回一个表示数据类型的字符串. 它存在以下明显的缺陷:
- typeof null // => 'object'
- typeof [] // => 'object'
这是因为在 JS 语言设计之初遗留的 bug. 可以阅读这篇文章 http://2ality.com/2013/10/typeof-null.html 了解更多关于 typeof 处理 null 的问题.
所以 typeof 最好用于判断一些基本类型, 比如数值, 字符串, 布尔值, undefined,Symbol.
2,instanceof 操作符
typeof 的背后是通过判断 type tags 来判断数据类型, 而 instanceof 则是通过判断构造函数的 prototype 是否出现在对象原型链上的任何位置.
举个例子:
- {
- } instanceof Object // => true
- [] instanceof Array // => true
- [] instanceof Object // => true
也判断自定义类型:
- function Car(make, model, year) {
- this.make = make;
- this.model = model;
- this.year = year;
- }
- var auto = new Car('Honda', 'Accord', 1998);
- console.log(auto instanceof Car);
- // => true
- console.log(auto instanceof Object);
- // => true
所以, 对于字面量形式的基本数据类型, 不能通过 instanceof 判断:
- 1 instanceof Number // => false
- Number(1) instanceof Number // => false
- new Number(1) instanceof Number // => true
- 3,Object.prototype.toString()
这是目前最为推荐的一种方法, 可以更加精细且准确的判断任何数据类型, 甚至是 JSON, 正则, 日期, 错误等等. 在 Lodash 中, 其判断数据类型的核心也是 Object.prototype.toString() 方法.
Object.prototype.toString.call(JSON) // => "[object JSON]"
关于这背后的原理, 你可以阅读这篇文章
4, 其他
上面三种是通用的判断数据类型的方法. 面试中还会出现如何判断一个数组, 如何判断 NaN, 如何判断类数组对象, 如何判断一个空对象等问题. 这一类问题比较开放, 解决思路通常是抓住判断数据的核心特点.
举个例子: 判断类数组对象.
你先要知道 JS 中类数组对象是什么样子的, 并寻求一个实际的参照物, 比如 arguments 就是类数组对象. 那么类数组对象具有的特点是: 真值 & 对象 & 具有 length 属性 & length 为整数 & length 的范围大于等于 0, 小于等于最大安全正整数 (Number.MAX_SAFE_INTEGER).
在你分析特点的时候, 答案就呼之欲出了.[注意全面性]
数据类型如何转换
JS 数据类型的动态性将贯穿整个 JS 的学习, 这是 JS 非常重要的特性, 很多现象就是因为动态性的存在而成为 JS 独有.
正是由于动态性, JS 的数据类型可能在你毫无察觉的情况下, 就发生了改变, 直到运行时报错.
这里主要分析下面 8 种转换规则.
1,if 语句
if 语句中的类型转换是最常见的.
- if (isTrue) {
- // ...
- } else {}
在 if 语句中, 会自动调用 Boolean() 转型函数对变量 isTrue 进行转换.
当 isTrue 的值是 null, undefined, 0, NaN, '' 时, 都会转为 false. 其余值除 false 本身外都会转为 true.
2,Number() 转型函数
我们重点关注 null undefined 以及字符串在 Number() 下的转换:
- Number(null) // => 0
- Number(undefined) // => NaN
- Number('') // => 0
- Number('123') // => 123
- Number('123abc') // => NaN
注意和 parseInt() 对比.
- 3,parseInt()
- parseInt(null) // => NaN
- parseInt(undefined) // => NaN
- parseInt('') // => NaN
- parseInt('123') // => 123
- parseInt('123abc') // => 123
- 4,==
这里需要注意的是:
- null == undefined // => true
- null == 0 // => false
- undefined == false // => false
null 与 undefined 的相等性是由 ECMA-262 规定的, 并且 null 与 undefined 在比较相等性时不能转换为其他任何值.
5, 关系操作符
对于两个字符串的比较, 是比较的字符编码值:
'B' <'a' // => true
一个数值, 另一个其他类型, 都将转为数字进行比较.
两个布尔值转为数值进行比较.
对象, 先调用 valueOf(), 若不存在该方法, 则调用 toString().
6, 加法
加法中特别注意的是, 数字和字符串相加, 将数字转为字符串.
- '1' + 2 => // '12'
- 1 + 2 => // 3
对于对象和布尔值, 调用它们的 toString() 方法得到对应的字符串值, 然后进行字符串相加. 对于 undefined 和 null 调用 String() 取得字符串'undeifned' 和'null'.
{ value: 1 } + true // => "[object Object]true"
7, 减法
对于字符串, 布尔值, null 或者 undefined, 自动调用 Number(), 转换结果若为 NaN, 那么最终结果为 NaN.
对于对象, 先调用 valueOf(), 如果得到 NaN, 结果为 NaN. 如果没有 valueOf(), 则调用 toString().
8, 乘法, 除法
对于非数值, 都会调用 Number() 转型函数.
变量提升与暂时性死区
JS 中有三种声明变量的方式: var, let, const.
var 声明变量最大的一个特点是存在变量提升.
- console.log(a); // undefined
- var a = 1;
- console.log(a); // 1
第一个打印结果表示, 在声明变量 a 之前, a 就已经可以访问了, 只不过并未赋值. 这就是变量提升现象.(具体原因, 我放在后面分析作用域的时候来写)
let 和 const 就不存在这个问题, 但是又引入了暂时性死区这样的概念.
- /**
- * 这上面都属于变量 a 的暂时性死区
- * console.log(a) // => Reference Error
- */
- let a = 1;
- console.log(a); // => 1
即声明 a 之前, 不能够访问 a, 而直接报错.
而暂时性死区的出现又引出另外一个问题, 即 typeof 不再安全. 你可以参考这篇文章
补充: 一个经典面试题
- for (var i = 0; i <4; i++) {
- setTimeout(function(){
- console.log(i);
- }, i * 1000);
- }
我先不再这里展开分析, 我打算放到异步与事件循环机制中去分析. 不过这里将 var 替换成 let 可以作为一种解决方案. 如果你有兴趣, 也可以先去分析.
对于 const, 这里再补充一点, 用于加深对基本类型和引用类型的理解.
- const a = 1;
- const b = {
- value: 1
- };
- a = 2; // => Error
- b.value = 2; // => 2
- b = {
- value: 2
- }; // => Error
本质上, const 并不是保证变量的值不得改动, 而是变量指向的内存地址不得改动.
声明全局变量
直接通过 var 声明全局变量, 这个全局变量会作为 Windows 对象的一个属性.
- var a = 1;
- Windows.a // => 1
在这里提出两个问题, 一是 let 声明的全局变量会成为 Windows 的属性吗? 二是 var 声明的全局变量和直接在 Windows 创建属性有没有区别?
先来回答第一问题. let 声明的全局变量不会成为 Windows 的属性. 用什么来支撑这样的结论呢? 在 ES6 中, 对于 let 和 const 声明的变量从一开始就形成封闭作用域. 想想之前的暂时性死区.
第二个问题, var 声明的全局变量和直接在 Windows 创建属性存在着本质的区别. 先看下面的代码:
- var a = 1;
- Windows.a // => 1
- Windows.b = 2;
- delete Windows.a
- delete Windows.b
- Windows.a // => 1
- Windows.b // => undefined
我们可以看到, 直接创建在 Windows 上的属性可以被 delete 删除, 而 var 创建的全局属性则不会. 这是现象, 通过现象看本质, 二者本质上的区别在于:
使用 var 声明的全局变量的 [[configurable]] 数据属性的值为 false, 不能通过 delete 删除. 而直接在对象上创建的属性默认 [[configurable]] 的值为 true, 即可以被 delete 删除.(关于 [[configurable]] 属性, 在后面的文章中分析对象的时候还会提到)
小结
在这篇「数据类型与变量」文章中, 分析了 7 个大类. 再来回顾一下:
基本类型, 引用类型, 参数传递方式, 如何判断数据类型, 数据类型如何转换, 变量提升与暂时性死区, 声明全局变量.
这些不仅是校招面试中的高频考点, 也是学习 JS 必不可少的知识点.
Tip1:《JavaScript 高级程序设计》这本书被称作 "前端的圣经" 是有原因的. 对于正在准备校园招聘的你, 非常有必要! 书读百遍, 其义自见. 你会发现你在面试中遇到的绝大部分 JS 相关的知识点都能在这本书中找到 "答案"!
Tip2: 在准备复习的过程中, 注意知识的模块性与相关性. 你得有自己划分知识模块的能力, 比如今天的「数据类型与变量」模块. 相关性是指, 任何的知识都是由联系的, 比如这里牵涉到作用域, 内存等模块.
来源: https://segmentfault.com/a/1190000017016407