前言
整理以前的面试题,发现问js数据类型的频率挺高的,回忆当初自己的答案,就是简简单单的把几个类型名称罗列了出来,便没有了任何下文。其实这一个知识点下可以牵涉发散出很多的知识点,如果一个面试者只是罗列的那些名词出来,可能面试官都不愿意继续问下去了,这该算是js基础的基础了。如果这个问题没有很好的回答,其他问题仍旧没有突出的亮点,很可能就过不了。
在网上看了一个体系,可作为大致的学习检阅自己的途径,按照清单上的知识检测自己还有哪些不足和提升,最后形成自己的知识体系。在工作、学习甚至面试时,可以快速定位到知识点。
1. JavaScript规定了几种语言类型2. JavaScript对象的底层数据结构是什么3. Symbol类型在实际开发中的应用、可手动实现一个简单的 Symbol4. 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)堆内存:
数据结构的堆栈:
是一种抽象的数据存储结构,
栈:一种连续存储的数据结构,特点是存储的数据先进后出,只能在栈顶一端对数据项进行插入和删除。
堆:是一棵完全二叉树结构(知识点未掌握)
五、基本类型对应的内置对象,以及他们之间的装箱拆箱操作
1)基本包装类型
问:有了基本类型为什么还要包装类型?
2)装箱和拆箱
var s1 = "stringtext";var s2 = s1.substring(2);
(1)创建String类型的一个实例 => var s1 = new String("stringtext");(2)在实例上调用指定的方法 => var s2 = s1.substring(2);(3)摧毁这个实例 => s1 = null;
var s1 = "stringtext";s1.color = "red"; //在这一句话执行完的瞬间,第二行创建的String就已经被销毁了。console.log(s1.color);//执行这一行代码时又创建了自己的String对象,而该对象没有color属性。//undefine
var obj = new Object("stringtext");console.log(obj instanceof String);//true
inputTpye
|
result
|
Null
|
不转换,直接返回
|
Undefined
|
不转换,直接返回
|
Number
|
不转换,直接返回
|
Boolean
|
不转换,直接返回
|
String
|
不转换,直接返回
|
Symbol
|
不转换,直接返回
|
Object
|
按照下列步骤进行转换
|
六、理解值类型和引用类型
js包含两种数据类型,基本数据类型和复杂数据类型,而其对应的值基本类型的值指的是简单的数据段,引用类型指的是那些可能有多个值构成的对象。可以从三个方面来理解:动态的属性、复制变量的值、传递参数
1)、动态的属性
定义基本类型值和引用类型值的方式类似,即创建一个变量并为该变量赋值。两者的区别在于,对于引用类型的值,我们可以为其添加属性和方法,也可以改变和删除其属性和方法;对于基本类型的值,我们不能为其动态地添加属性。
var person = new Object(); //创建一个对象并将其保存在变量person中person.name = "Song"; //为该对象添加一个名为name的属性,并赋值为Songconsole.log(person.name); //访问name这个属性//Song
2)、复制变量的值
在从一个变量向另一个变量复制基本类型值和引用类型值时,两则也是不同的,这主要是由于基本类型和引用类型在内存中存储不同导致的。
var a = 1;var b = a;b = 2;console.log(a);//1console.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类型
var car = null;console.log(typeof car);//object
var s;console.log(s == undefined);//true
八、至少可以说出三种判断 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 --------> truevar d = new Date();d.constructor === Date ---------> true(()=>{}).constructor === Function -------> true注意: constructor 在类继承时会出错eg:function A(){};function B(){};A.prototype = new B(); //A继承自Bvar 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.9jQuery.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的未来版本或许会支持十进制数字类型以避免这些舍入问题,在这之前,你更愿意使用大整数进行重要的金融计算,例如,要使用整数‘分’而不是使用小数‘元’进行货比单位的运算。
问:怎么避免精度丢失?
Number.EPSILON === Math.pow(2, -52)// true 说明这个值Number.EPSILON是等于 2 的 -52 次方
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的数学库,它的特点是灵活表达式解析器,支持符号计算,内置大量函数与常量,并提供集成解决方案来处理不同的数据类型,如数字,大数字,复数,分数,单位和矩阵。
2.65.toFixed(1) //2.6 结果正确2.45.toFixed(1) //2.5 希望得到的结果是2.42.35.toFixed(1) //2.4 结果正确
function RoundNum(n, m){ //n表示需要四舍五入的数,m表示需要保留的小数位数 var newNum = Math.round(n * Math.pow(10, m)) / Math.pow(10, m) ; //首先将要保留的小数位数的小数部分转成整数部分,利用幂函数将n乘以10的m次方 //然后利用Math.round()方法进行四舍五入处理 //最后再除以10的m次方还原小数部分 //注:此时还未能将所有数字正确转换。例如将1.0001保留3位小数我们想要的结果是1.000,而此时newNum里面的值是1 //所以还需要处理此种特殊情况,即保留的小数位上全0 var newSNum = newNum.toString(); //这一步将刚才进行处理过的数转换成字符串 var rs = newSNum.indexOf('.'); //利用indexOf查找字符串中是否有.,它返回某个指定的字符串值在字符串中首次出现的位置,不存在则返回-1 if (rs < 0) { rs = newSNum.length; newSNum += '.'; } while (newSNum.length <= rs + m) { //在末尾加0 newSNum += '0'; } return newSNum; } console.log(RoundNum(1.0005, 3)); //得到1.001
④封装一个计算类(加、减、乘、除)
(暂未实际写过)
问:JavaScript可以存储的最大数字以及最大安全数字
答:最大数字是Number.MAX_VALUE、最大安全数字是Number.MAX_SAFE_INTEGER。Number.MAX_VALUE大于Number.MAX_SAFE_INTEGER,我的理解是js可以精确表示最大安全数字以内的数,超过了最大安全数字但没超过最大数字可以表示,但不精确,如果超过了最大数字,则这个数值会自动转换成特殊的Infinity值。
由于内存的限制,ECMAScript并不能保存世界上所有的数值,ECMAScript能够表示的最小数值是Number.MIN_VALUE,能够表示的最大数值是Number.MAX_VALUE。超过数值是正值,则被转成Infinity(正无穷),如果是负值则被转成-Infinity(负无穷)。如果在某次返回了正或负的Infinity值,那么该值将无法继续参与下一次的计算,所以我们需要确定一个数值是不是有穷的,即是不是位于最小和最大的数值之间,可以使用isFinite()函数,如果该函数参数在最小和最大数值之间时会返回true。注意,如果参数类型不是数值,Number.isFinite
一律返回false
。
JavaScript 能够准确表示的整数范围在-2^53
到2^53
之间(不含两个端点),超过这个范围,无法精确表示这个值。ES6 引入了Number.MAX_SAFE_INTEGER
和Number.MIN_SAFE_INTEGER
这两个常量,用来表示这个范围的上下限。Number.isSafeInteger()
则是用来判断一个整数是否落在这个范围之内。