前言   整理以前的面试题,发现问js数据类型的频率挺高的,回忆当初自己的答案,就是简简单单的把几个类型名称罗列了出来,便没有了任何下文。其实这一个知识点下可以牵涉发散出很多的知识点,如果一个面试者只是罗列的那些名词出来,可能面试官都不愿意继续问下去了,这该算是js基础的基础了。如果这个问题没有很好的回答,其他问题仍旧没有突出的亮点,很可能就过不了。   在网上看了一个体系,可作为大致的学习检阅自己的途径,按照清单上的知识检测自己还有哪些不足和提升,最后形成自己的知识体系。在工作、学习甚至面试时,可以快速定位到知识点。 1. JavaScript规定了几种语言类型 2. JavaScript对象的底层数据结构是什么 3. Symbol类型在实际开发中的应用、可手动实现一个简单的 Symbol 4. JavaScript中的变量在内存中的具体存储形式 5. 基本类型对应的内置对象,以及他们之间的装箱拆箱操作 6. 理解值类型和引用类型 7. null和 undefined的区别 8. 至少可以说出三种判断 JavaScript数据类型的方式,以及他们的优缺点,如何准确的判断数组类型 9. 可能发生隐式类型转换的场景以及转换原则,应如何避免或巧妙应用 10. 出现小数精度丢失的原因, JavaScript可以存储的最大数字、最大安全数字, JavaScript处理大数字的方法、避免精度丢失的方法 一、JavaScript规定了几种语言类型   问:讲一下js的数据类型?   答:js的数据类型分为简单数据类型和复杂数据类型; 简单数据类型有六种,分别是String(字符串)、Number(数字)、Null(空)、undefined(未定义)、boolean(布尔值)、symbol(符号),表示不能再继续分下去的类型,在内存中以固定的大小存储在栈中,按值访问; 复杂数据类型是指对象,这里有常见的array、function、object等,本质上是一组无序的键值对组成。它的值大小不固定,所以保存在堆中,但在栈中会存储有指向其堆内存的地址,按引用来访问。js不允许直接访问内存中的位置,也就是说不能直接操作对象的内存空间。也就是说,当我们想要访问应用类型的值的时候,需要先从栈中获得对象的地址指针,然后通过地址指针找到其在堆中的数据。 需要注意的是, 1、简单数据类型中的boolean、number、string不是由内置函数new出来的,尽管他们有对应的引用类型; 2、symbol是ES6引入的一种新的原始数据,表示独一无二且不可改变的值。通过 Symbol 函数调用生成,由于生成的 symbol 值为原始类型,所以 Symbol 函数不能使用 new 调用; 3、将一个变量赋值给另一个变量时,基础类型复制的是值,赋值完成两个变量在没有任何关系;而对象类型的复制的是地址,修改一个变量另一个变量也会跟着一起变化。(如何解决这个问题?关于深拷贝and浅拷贝) 二、JavaScript对象的底层数据结构是什么 这个问题目前对我来说,不能够理解到底是想问什么,还有问题,看到一篇这个文章,转载《从chrome源码看js object的实现》:https://www.rrfed.com/2017/04/04/chrome-object/ 三、Symbol类型在实际开发中的应用、手动实现一个简单的 Symbol (暂未学习总结) 四、JavaScript中的变量在内存中的具体存储形式   js的数据类型分为简单数据类型和复杂数据类型;在内存中,简单数据类型以固定的大小存储在栈中;复杂数据类型存储在堆中,且大小不固定,同时在栈中会存储其指向堆地址的指针。 因为这里问的是内存中的存储形式,所以我一直注意的是内存中堆栈,后来忽然看到一篇文章写了数据结构中的堆和栈就有一点懵,先简单记录一下相关知识点。 内存的堆栈:   是一种物理结构,用于存放不同数据的内存空间,分为栈区和堆区。 1)栈内存:   栈(stack)是向低地址扩展的数据结构,是一块连续的内存区域;一般来说其大小是系统预先规定好的,存储大小已知的变量(函数的参数值、局部变量的值等)。由操作系统自动申请分配并释放(回收)空间,无需程序员控制,这样的好处是内存可以及时得到回收。但栈的大小有限,如果申请的空间超过栈的剩余空间,就会提示栈溢出(一般无穷次的递归调用或大量的内存分配会引起栈溢出)。 在分配内存的时候类似于数据结构中的栈,先进后出的原则,即从栈低向栈顶,依次存储。栈是向下增长,即从高地址到低地址分配内存,且内存区域连续、每个单元的大小相同。如下图: 2)堆内存:   在现代程序中,在编译时刻不能决定大小的对象将被分配在堆区。一般由程序员分配释放,例如;c++和Java语言都为程序员提供了new(或malloc()),该语句创建的对象(或指向对象的指针),然后使用delete(或free())语句释放。如果程序员不主动释放,程序结束时由OS回收。 在内存分配的时候方式类似于链表,堆是向上增长,即从低地址到高地址扩展,是不连续的内存区域。如下图: 数据结构的堆栈:   是一种抽象的数据存储结构, 栈:一种连续存储的数据结构,特点是存储的数据先进后出,只能在栈顶一端对数据项进行插入和删除。 堆:是一棵完全二叉树结构(知识点未掌握) 五、基本类型对应的内置对象,以及他们之间的装箱拆箱操作 1)基本包装类型   问:有了基本类型为什么还要包装类型?   答:为了便于操作基本类型值,ECMAScript提供了3个特殊的引用类型:Boolean、Number和String, 每当读取一个基本类型值的时候,后台会创建一个对应的基本包装类型的对象,从而能够调用一些方法来操作这些基本类型。每个包装类型都映射到同名的基本类型。 2)装箱和拆箱 装箱就是把基本类型转换为对应的内置对象,这里可分为隐式和显式装箱。 拆箱就是与装箱相反,把对象转变为基本类型的值。 Ⅰ 隐式装箱 在读取模式下,访问基本类型值(即读取基本类型的值),就会创建基本类型所对应的基本包装类型的一个对象,从而让我们能够调用一些方法来操作这些数据。这个基本包装类型是临时的,操作基本类型值的语句一经执行完毕,就会立即销毁新创建的包装对象。 var s1 = "stringtext"; var s2 = s1.substring(2); 如上面的例子,第一行变量s1是一个基本类型的字符串,第二行调用了s1的substring()方法,并将结果保存在了s2中。 基本类型值不是对象,从逻辑上讲它们不应该有方法。其实这里就包含了隐式装箱,后台自动完成了一系列的处理。当第二行代码访问s1时,访问过程处于一种读取模式,即从内存中读取这个字符串的值。 在读取字符串时,后台会完成一下处理。 (1)创建String类型的一个实例 => var s1 = new String("stringtext"); (2)在实例上调用指定的方法 => var s2 = s1.substring(2); (3)摧毁这个实例 => s1 = null; 注:①上面s1 = null;这种做法叫做解除引用,一旦有数据不再有用,通过设置其值为null来是释放其引用;一般适用于大多数全局变量和全局对象的属性。   ②引用类型和基本包装类型的主要区别:就是对象的生存期。使用new操作符创建的引用类型的实例,在执行流离开当前作用域之前都一直保存在内存中。而自动创建的基本包装类型的对象,则只存在于一行代码的执行瞬间,然后立即被销毁,因此我们不能在运行时为基本类型值添加属性和方法。 var s1 = "stringtext"; s1.color = "red"; //在这一句话执行完的瞬间,第二行创建的String就已经被销毁了。 console.log(s1.color);//执行这一行代码时又创建了自己的String对象,而该对象没有color属性。 //undefine   Ⅱ 显式装箱 通过New调用Boolean、Number、String来创建基本包装类型的对象。不过,不建议显式地创建基本包装类型的对象,尽管它们操作基本类型值的能力相当重要,每个基本包装类型都提供了操作相应值的便捷方法。 Object构造函数会像工厂方法一样,根据传入值的类型返回相应基本包装类型的实例。 var obj = new Object("stringtext"); console.log(obj instanceof String); //true   Ⅲ 拆箱 把对象转变为基本类型的值,在拆箱的过程调用了JavaScript引擎内部的抽象操作,ToPrimitive(转换为原始值),对原始值不发生转换处理,只针对引用类型。 JavaScript引擎内部的抽象操作ToPrimitive()是这样定义的, ToPrimitive(input [, PreferredType]) 该操作接受两个参数,第一个参数是要转变的值,第二个是PreferredType为可选参数,只接受Number或String,作用是设置想要转换原值时的转换偏好。最后使input转换成原始值。 如果PreferredType被标志为Number,则会进行下面的操作来转换input。 ①如果输入的是一个原始值,则直接返回它; ②否则,如果输入的值是一个对象,则调用该对象的valueOf()方法,如果valueOf()方法的返回值是一个原始值,则返回这个原始值; ③否则,调用这个对象的toString()方法,如果toString方法的返回值是一个原始值,则返回这个原始值; ④否则,抛出TypeError异常; 如果PreferredType被标志为String,则转换操作的第二步和第三步的顺序会调换。即 ①如果输入的是一个原始值,则直接返回它; ②否则,如果输入的值是一个对象,则调用该对象的 toString()方法,如果toString()方法的返回值是一个原始值,则返回这个原始值; ③否则,调用这个对象的valueOf()方法,如果valueOf()方法的返回值是一个原始值,则返回这个原始值; ④否则,抛出TypeError异常; 如果没有PreferredType的值会按照这样的规则来自动设置: Date类型的对象会被设置为String,其他类型的值被设置为Number inputTpye result Null 不转换,直接返回 Undefined 不转换,直接返回 Number 不转换,直接返回 Boolean 不转换,直接返回 String 不转换,直接返回 Symbol 不转换,直接返回 Object 按照下列步骤进行转换 参考文章: 《js隐式装箱》 https://sinaad.github.io/xfe/2016/04/15/ToPrimitive/ 《[译]JavaScript在中,{} + {}等于多少?》 https://www.cnblogs.com/ziyunfei/archive/2012/09/15/2685885.html 原文《javaScript在中,what is {} + {} in javascrupt ?》 https://2ality.com/2012/01/object-plus-object.html 六、理解值类型和引用类型 js包含两种数据类型,基本数据类型和复杂数据类型,而其对应的值基本类型的值指的是简单的数据段,引用类型指的是那些可能有多个值构成的对象。可以从三个方面来理解:动态的属性、复制变量的值、传递参数 1)、动态的属性 定义基本类型值和引用类型值的方式类似,即创建一个变量并为该变量赋值。两者的区别在于,对于引用类型的值,我们可以为其添加属性和方法,也可以改变和删除其属性和方法;对于基本类型的值,我们不能为其动态地添加属性。 var person = new Object(); //创建一个对象并将其保存在变量person中 person.name = "Song"; //为该对象添加一个名为name的属性,并赋值为Song console.log(person.name); //访问name这个属性 //Song 2)、复制变量的值 在从一个变量向另一个变量复制基本类型值和引用类型值时,两则也是不同的,这主要是由于基本类型和引用类型在内存中存储不同导致的。   Ⅰ基本类型的值 基本类型的值是存在栈中,存储的即是基本类型的值;如果从一个变量向另一个变量复制的时候,就会重新创建一个变量的新值然后将其复制到为新变量分配的位置上,此时两个变量各自拥有属于自己的独立的内存空间,因此两者可以参与任何操作而不会相互影响。 var a = 1; var b = a; b = 2; console.log(a);//1 console.log(b);//2 内存变化大致如下:   Ⅱ复制引用类型的值 引用类型的值存储在堆中,同时在栈中会有相应的堆地址(指针),指向其在堆的位置。此时如果我们要复制一个引用类型时,复制的不是堆内存中的值,而是将栈内存中的地址复制过去,复制操作结束后,两个对象实际上都指向堆中的同一个地方。因此改变其中一个对象(堆中的值改变),那么会影响到另一个对象。 var obj1 = { name:"Song" }; var obj2 = obj1; obj2.name = "D"; //改变obj2的name属性的值,则将obj1的也改变了。 console.log(obj1.name); // D 注:关于深拷贝和浅拷贝 3)、传递参数 ECMAScript中所有函数的参数都是按值传递的,无论在向参数传递的是基本类型还是引用类型。(我的理解:正因为是按值传递的,所以我们才可以利用此来完成深拷贝) 有一道关于证明引用类型是按值传递还是按引用传递的题目如下: 复制代码 function test(person){ person.age = 26; person = {   name:'yyy',   age:30 } return person } const p1 = {   name:'yck',   age:25 }; const p2 = test(p1); console.log(p1); console.log(p2); 复制代码 首先当我们从一个变量向另一个变量复制引用类型的值时,这个值是存储在栈中的指针地址,复制操作结束后,两个变量引用的是同一个对象,改变其中一个变量,就会影响另一个变量。 而在向参数传递引用类型的值时,同样是把内存中的地址复制给一个局部变量,所以在上述代码中,将p1的内存地址指针复制给了局部变量person,两者引用的是同一个对象,这个时候在函数中改变变量,就会影响到外部。 接下来相当于从新开辟了一个内存空间,然后将此内存空间的地址赋给person,可以理解为将刚才指向p1的指针地址给覆盖了,所以改变了person的指向,当该函数结束后便释放此内存。 (此图作为自己的理解,不代表实际,很有可能实际并不是这样操作的。) 所以在person.age = 26;这句话执行后把p1内存里的值改变了,打印出来p1是{name: "yck", age: 26} p2是{name: "yyy", age: 30} 而我理解的如果按引用传递,则相当于person的指向是和p1也一样,所以后续只要是对person进行了操作,都会直接影响p1。 因此在这种情况下,打印出来p1和p2都是{name: "yyy", age: 30} 七、null和 undefined的区别 1)、null类型 《高程》上解释:null值表示一个空对象指针,所以这也是使用typeof操作符检测null值时会返回"object"的原因。 var car = null; console.log(typeof car); //object 一般来说,我们要保存对象的变量在还没有真正保存对象之前可以赋值初始化为null,其他的基础类型在未赋值前默认为undefined,这样一来我们直接检查变量是否为null可以知道相应的变量是否已经保存了一个对象的引用。 即如果定义的变量准备在将来保存为对象,那么我们将该变量初始化为null,而不是undefined。 2)、undefined类型 在使用var申明变量但未对其初始化时,这个变量的值就是undefined。 var s; console.log(s == undefined); //true 3)、null和undefined的区别 一般来说undefined是派生自null的值,因此null == undefined 是为true,因为它们是类似的值;如果用全等于(===),null ===undefined会返回false ,因为它们是不同类型的值。以此我们可以区分null和undefined。 八、至少可以说出三种判断 JavaScript数据类型的方式,以及他们的优缺点,如何准确的判断数组类型   问:判断js数据类型有哪几种方式,分别有什么优缺点?怎么样判断一个值是数组类型还是对象?(或者typeof能不能正确判断类型)   答:一般来说有5种常用的方法,分别是typeof、instanceof、Object.prototype.toString()、constructor、jquery的type(); 1)对于typeof来说,在检测基本数据类型时十分得力,对于基本类型,除了null都可以返回正确类型,对于对象来说,除了function都返回object。 基本类型   typeof "somestring" // 'string'   typeof true // 'boolean'   typeof 10 // 'number'   typeof Symbol() // 'symbol'   typeof null // 'object' 无法判定是否为 null   typeof undefined // 'undefined' 复杂类型   typeof {} // 'object'   typeof [] // 'object' 如果需要判断数组类型,则不能使用这样方式   typeof(() => {}) // 'function' 注:怎么使用复合条件来检测null值的类型? var a = null; (!a && typeof a === "object"); // true 2)对于instanceof来说,可以来判断已知对象的类型,如果使用instanceof来判断基本类型,则始终返回false。 其原理是测试构造函数的prototype是否出现在被检测对象的原型链上;所有的复杂类型的值都是object的实例,在检测一个引用类型值和Object构造函数时,instanceof操作符始终返回true。 [] instanceof Array //true -》 无法优雅的判断一个值到底属于数组还是普通对象 ({}) instanceof Object //true (()=>{}) instanceof Function //true 而且在《高程》上还看到说一个问题,如果不是单一的全局执行环境,比如网页中包含多个框架,那么实际上存在两个以上不同的全局执行环境,从而存在两个以上不同版本的Array构造函数,如果从一个框架向另外一个框架传入数组,那么传入的数据与在第二个框架中原生创建的数组分别具有各自不同的构造函数。eg:例如index页面传入一个arr变量给iframe去处理,则即使arr instanceof Array还是返回false,因为两个引用的Array类型不是同一个。并且constructor可以重写所以不能确保万无一失。 对于数组来说,相当于new Array()出的一个实例,所以arr.proto === Array.prototype;又因为Array是Object的子对象,所以Array.prototype.proto === Object.prototype。因此Object构造函数在arr的原型链上,便无法判断一个值到底属于数组还是普通对象。 注:判断变量是否为数组的方法 3)通用但比较繁琐的方法Object.prototype.toString() 该方法本质是利用Object.prototype.toString()方法得到对象内部属性[[Class]],传入基本类型也能够判断出结果是因为对其值做了包装。 Object.prototype.toString.call({}) === '[object Object]' -------> true; Object.prototype.toString.call([]) === '[object Array]'  -------> true; Object.prototype.toString.call(() => {}) === '[object Function]'  -------> true; Object.prototype.toString.call('somestring') === '[object String]'  -------> true; Object.prototype.toString.call(1) === '[object Number]'  -------> true; Object.prototype.toString.call(true) === '[object Boolean]'  -------> true; Object.prototype.toString.call(Symbol()) === '[object Symbol]'  -------> true; Object.prototype.toString.call(null) === '[object Null]'  -------> true; Object.prototype.toString.call(undefined) === '[object Undefined]'  -------> true; Object.prototype.toString.call(new Date()) === '[object Date]'  -------> true; Object.prototype.toString.call(Math) === '[object Math]'  -------> true; Object.prototype.toString.call(new Set()) === '[object Set]'  -------> true; Object.prototype.toString.call(new WeakSet()) === '[object WeakSet]'  -------> true; Object.prototype.toString.call(new Map()) === '[object Map]'  -------> true; Object.prototype.toString.call(new WeakMap()) === '[object WeakMap]'  -------> true; 4)根据对象的constructor判断 [].constructor === Array --------> true var d = new Date(); d.constructor === Date ---------> true (()=>{}).constructor === Function -------> true 注意: constructor 在类继承时会出错 eg: function A(){}; function B(){}; A.prototype = new B(); //A继承自B var aobj = new A(); aobj.constructor === B  --------> true; aobj.constructor === A   --------> false; 而instanceof方法不会出现该问题,对象直接继承和间接继承的都会报true: 5)jquery的type() 如果对象是undefined或null,则返回相应的“undefined”或“null”, jQuery.type( undefined ) === "undefined" jQuery.type() === "undefined" jQuery.type( null ) === "null" 如果对象有一个内部的[[Class]]和一个浏览器的内置对象的 [[Class]] 相同,我们返回相应的 [[Class]] 名字。 jQuery.type( true ) === "boolean" jQuery.type( 3 ) === "number" jQuery.type( "test" ) === "string" jQuery.type( function(){} ) === "function" jQuery.type( [] ) === "array" jQuery.type( new Date() ) === "date" jQuery.type( new Error() ) === "error" // as of jQuery 1.9 jQuery.type( /test/ ) === "regexp" 6)如何判断一个数组? var a = []; a.instanceof Array;  --------> true a.constructor === Array  --------> true Object.prototype.toString.call(a) === '[object Array]'  --------> true Array.isArray([]);  --------> true 九、可能发生隐式类型转换的场景以及转换原则,应如何避免或巧妙应用 (暂未整理) 十、出现小数精度丢失的原因、 JavaScript可以存储的最大数字以及最大安全数字、JavaScript处理大数字的方法、避免精度丢失的方法   问:0.1+0.2 === 0.3 为什么是false?   答:在ECMAScript数据类型中的Number类型是使用IEEE754格式来表示的整数和浮点数值,所谓浮点数值就是该数值必须包含一个小数点,并且小数点后面必须至少有一位数字。而在使用基于IEEE754数值的浮点运算时出现参数舍入的误差问题,即出现小数精度丢失,无法测试特定的浮点数值。   ①在进行0.1+0.2的时候首先要将其转换成二进制。   0.1 => 0.0001 1001 1001 1001…(无限循环)   0.2 => 0.0011 0011 0011 0011…(无限循环)   ②由于 JavaScript 采用 IEEE 754 标准,数值存储为64位双精度格式,数值精度最多可以达到 53 个二进制位(1 个隐藏位与 52 个有效位)。如果数值的精度超过这个限度,第54位及后面的位就会被丢弃,所以在相加的时候会因为小数位的限制而将二进制数字截断。   0.0001 1001 1001 1001…+0.0011 0011 0011 0011… = 0.0100110011001100110011001100110011001100110011001100   ③再转换成十进制就成了0.30000000000000004,而非我们期望的0.3 在《js权威指南》中有指出: Javascript采用了IEEE-745浮点数表示法(几乎所有的编程语言都采用),这是一种二进制表示法,可以精确地表示分数,比如1/2,1/8,1/1024。遗憾的是,我们常用的分数(特别是在金融的计算方面)都是十进制分数1/10,1/100等。二进制浮点数表示法并不能精确的表示类似0.1这样 的简单的数字,上诉代码的中的x和y的值非常接近最终的正确值,这种计算结果可以胜任大多数的计算任务:这个问题也只有在比较两个值是否相等时才会出现。 这个问题并不是只在javascript中才会出现,在任何使用二进制浮点数的编程语言中都会出现这个问题。 所以说,精度丢失并不是语言的问题,而是浮点数存储本身固有的缺陷。只不过在 C++/C#/Java 这些语言中已经封装好了方法来避免精度的问题,而 JavaScript 是一门弱类型的语言,从设计思想上就没有对浮点数有个严格的数据类型,所以精度误差的问题就显得格外突出。 javascript的未来版本或许会支持十进制数字类型以避免这些舍入问题,在这之前,你更愿意使用大整数进行重要的金融计算,例如,要使用整数‘分’而不是使用小数‘元’进行货比单位的运算。   问:怎么避免精度丢失?    答:一般常用的有四个方法,第一个是设置一个“能够接受的误差范围”,在这个范围内,可认为没有误差;第二个是使用三方的类库math.js;第三是使用toFixed()方法;第四是封装一个计算类(加、减、乘、除)。   ①ES6在Number对象上面,新增了一个极小的常量Number.EPSILON,它表示1与大于1的最小浮点数之间的差,它是实际上是javascript能够表示的最小精度(可以接受的最小误差范围),误差如果小于这个值,就可以认为已经没有意义了,即不存在误差。 Number.EPSILON === Math.pow(2, -52) // true 说明这个值Number.EPSILON是等于 2 的 -52 次方   写一个误差检测函数,来判断0.1 + 0.2 === 0.3   设置误差范围为 2 的-50 次方(即Number.EPSILON * Math.pow(2, 2)),即如果两个浮点数的差小于这个值,我们就认为这两个浮点数相等。 复制代码   function withinErrorMargin (left, right) {   return Math.abs(left - right) < Number.EPSILON * Math.pow(2, 2);   } withinErrorMargin(0.1 + 0.2, 0.3) //true 复制代码   ②math.js是一个广泛应用于JavaScript 和 Node.js的数学库,它的特点是灵活表达式解析器,支持符号计算,内置大量函数与常量,并提供集成解决方案来处理不同的数据类型,如数字,大数字,复数,分数,单位和矩阵。   ③toFixed()方法   定义:toFixed() 方法可把 Number 四舍五入为指定小数位数的数字。   用法:NumberObject.toFixed(num) 其中num是必须的,规定小数的位数,是 0 ~ 20 之间的值,包括 0 和 20,有些实现可以支持更大的数值范围。如果省略了该参数,将用 0 代替。 然而实际上,并不是完美的,可能你开发时候测试的几个实例恰巧都是你想要的结果,可能在实际上线后遇到大量的数据后发现出问题了,不能正确的计算。一般是在遇到最后一位是5的时候,就不是'四舍五入",eg:2.55.toFixed(1) // 2.5,而我们齐期望的是2.56。 我有查这个产生误差的原因,有人说是“银行家的舍入规则”