1. JavaScript有哪些数据类型,它们的区别?
JavaScript共有八种数据类型,分别是 Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt, 其中 Symbol 和 BigInt 是 ES6 中新增的数据类型
Symbol: 代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。
BigInt: 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。
栈: 原始数据类型(Undefined、Null、Boolean、Number、String、Symbol、BigInt) 堆: 引用数据类型(对象、数组和函数)
栈: 占据空间小、大小固定,属于被频繁使用数据; 堆: 占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。
2. 数据类型检测的方式有哪些
typeof
能判断基础类型, 数组、对象、null 都会被判断为 object
typeof 2 // number
typeof true // boolean
typeof 'str' // string
typeof undefined // undefined
typeof function(){} // function
typeof [] // object
typeof {} // object
typeof null // object
instanceof
可以正确判断对象的类型,不能判断基本数据类型, 其内部运行机制是判断在其原型链中能否找到该类型的原型。
2 instanceof Number // false
'str' instanceof String // false
true instanceof Boolean // false
[] instanceof Array // true
function(){} instanceof Function // true
{} instanceof Object // true
复制代码
constructor
虽然简单和复杂数据类型都能判断,但是构造函数可以被手动改变,所以也不是百分之百准确
(2).constructor === Number // true
(true).constructor === Boolean // true
('str').constructor === String // true
([]).constructor === Array // true
(function() {}).constructor === Function // true
({}).constructor === Object // true
复制代码
function Fn(){};
Fn.prototype = new Array();
var f = new Fn();
console.log(f.constructor===Fn); // false
console.log(f.constructor===Array); // true
复制代码
Object.prototype.toString.call()
使用 Object 对象的原型方法 toString 来判断数据类型:
var a = Object.prototype.toString;
a.call(2)
a.call('str')
a.call(true)
a.call(null)
a.call(undefined)
a.call([])
a.call(function(){})
a.call({})
复制代码
同样是检测对象 obj 调用 toString 方法,obj.toString() 的结果和 Object.prototype.toString.call(obj) 的结果不一样,这是为什么?
这是因为 toString 是 Object 的原型方法,而 Array、 function 等类型作为 Object 的实例,都重写了 toString 方法。不同的对象类型调用 toString 方法时,根据原型链的知识,调用的是对应的重写之后的 toString 方法(function 类型返回内容为函数体的字符串,Array 类型返回元素组成的字符串…),而不会去调用 Object 上原型 toString 方法(返回对象的具体类型),所以采用 obj.toString() 不能得到其对象类型,只能将 obj 转换为字符串类型;因此,在想要得到对象的具体类型时,应该调用 Object 原型上的 toString 方法。
3. 判断数组的方式有哪些
通过 Object.prototype.toString.call() 做判断
Object.prototype.toString.call(obj).slice(8,-1) === 'Array';
复制代码
通过原型链做判断
obj.__proto__ === Array.prototype;
复制代码
通过 ES6 的 Array.isArray() 做判断
Array.isArray(obj);
复制代码
通过 instanceof 做判断
obj instanceof Array
复制代码
通过 Array.prototype.isPrototypeOf
Array.prototype.isPrototypeOf(obj)
复制代码
4. intanceof 操作符的实现原理及实现
instanceof 用于判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。
function myInstanceof(left, right) {
// 获取对象的原型
let proto = Object.getPrototypeOf(left)
// 获取构造函数的 prototype 对象
let prototype = right.prototype;
// 判断构造函数的 prototype 对象是否在对象的原型链上
while (true) {
if (!proto) return false;
if (proto === prototype) return true;
// 如果没有找到,就继续从其原型上找,Object.getPrototypeOf方法用来获取指定对象的原型
proto = Object.getPrototypeOf(proto);
}
}
复制代码
5. 为什么0.1+0.2 ! == 0.3,如何让其相等
在开发过程中遇到类似这样的问题:
let n1 = 0.1, n2 = 0.2
console.log(n1 + n2) // 0.30000000000000004
复制代码
这里得到的不是想要的结果,要想等于0.3,就要把它进行转化:
(n1 + n2).toFixed(2) // 注意,toFixed为四舍五入
复制代码
toFixed(num) 方法可把 Number 四舍五入为指定小数位数的数字。
不等于 0.3 的原因是二进制相加的结果
6. 如何获取安全的 undefined 值?
因为 undefined 是一个标识符,所以可以被当作变量来使用和赋值,但是这样会影响 undefined 的正常判断。表达式 void ___ 没有返回值,因此返回结果是 undefined。void 并不改变表达式的结果,只是让表达式不返回值。因此可以用 void 0 来获得 undefined。
7. typeof NaN 的结果是什么?
NaN 指“不是一个数字”,NaN 是一个“警戒值”,用于指出数字类型中的错误情况,即“执行数学运算没有成功,这是失败后返回的结果”。
typeof NaN; // "number"
复制代码
NaN 是一个特殊值,它和自身不相等,是唯一一个非自反的值。 NaN !== NaN 为 true。
8. isNaN 和 Number.isNaN 函数的区别?
函数 isNaN 接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回 true,因此非数字值传入也会返回 true ,会影响 NaN 的判断。
函数 Number.isNaN 会首先判断传入参数是否为数字,如果是数字再继续判断是否为 NaN ,不会进行数据类型的转换,这种方法对于 NaN 的判断更为准确。
9. == 操作符的强制类型转换规则?
对于 == 来说,如果对比双方的类型不一样,就会进行类型转换。假如对比 x 和 y 是否相同,就会进行如下判断流程:
- 首先会判断两者类型是否
相同, 相同的话就比较两者的大小; - 类型
不相同的话,就会进行类型转换; - 会先判断是否在对比
null和undefined,是的话就会返回true - 判断两者类型是否为
string和number,是的话就会将字符串转换为number - 判断其中一方是否为
boolean,是的话就会把boolean转为number再进行判断 - 判断其中一方是否为
object且另一方为string、number或者symbol,是的话就会把object转为原始类型再进行判断
其流程图如下:
10. 其他值到字符串的转换规则?
Null 和 Undefined 类型 ,null 转换为 "null",undefined 转换为 "undefined",
Boolean 类型,true 转换为 "true",false 转换为 "false"。
Number 类型的值直接转换,不过那些极小和极大的数字会使用指数形式。
Symbol 类型的值直接转换,但是只允许显式强制类型转换,使用隐式强制类型转换会产生错误。
对普通对象来说,除非自行定义 toString() 方法,否则会调用 toString() 来返回内部属性 [[Class]] 的值,如"[object Object]"。如果对象有自己的 toString() 方法,字符串化时就会调用该方法并使用其返回值。
11. 其他值到数字值的转换规则?
Undefined 类型的值转换为 NaN。
Null 类型的值转换为 0。
Boolean 类型的值,true 转换为 1,false 转换为 0。
String 类型的值转换如同使用 Number() 函数进行转换,如果包含非数字值则转换为 NaN,空字符串为 0。
Symbol 类型的值不能转换为数字,会报错。
对象(包括数组)会首先被转换为相应的基本类型值,如果返回的是非数字的基本类型值,则再遵循以上规则将其强制转换为数字。
为了将值转换为相应的基本类型值,抽象操作会首先检查该值是否有 valueOf() 方法。如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用 toString() 的返回值来进行强制类型转换。
如果 valueOf() 和 toString() 均不返回基本类型值,会产生 TypeError 错误。
12. 其他值到布尔类型的值的转换规则?
以下这些是假值:
- undefined
- null
- +0、-0 和 NaN
- ""
假值的布尔强制类型转换结果为 false。从逻辑上说,假值列表以外的都应该是真值。
13. || 和 && 操作符的返回值?
||,第一个参数为 true 就返回第一个操作数的值,如果为 false 就返回第二个操作数的值。
&&,则相反, true 返回第二个操作数的值,false 返回第一个操作数的值。
14. Object.is() 与比较操作符 “===”、“==” 的区别?
== 进行相等判断时,如果两边的类型不一致,则会进行强制类型转化后再进行比较。
=== 进行相等判断时,类型不一致时,不会做强制类型准换,直接返回 false。
使用 Object.is 来进行相等判断时,一般情况下和 === 的判断相同,它处理了一些特殊的情况,比如 -0 和 +0 不再相等,两个 NaN 是相等的。
15. 什么是 JavaScript 中的包装类型?
在 JavaScript 中,基本类型是没有属性和方法的,但是为了便于操作基本类型的值,在调用基本类型的属性或方法时 JavaScript 会在后台隐式地将基本类型的值转换为对象,如:
const a = "abc";
a.length; // 3
a.toUpperCase(); // "ABC"
复制代码
在访问 'abc'.length 时,JavaScript 将 'abc' 在后台转换成 String('abc') ,然后再访问其 length 属性。
JavaScript也可以使用 Object 函数显式地将基本类型转换为包装类型:
var a = 'abc'
Object(a) // String {"abc"}
复制代码
也可以使用 valueOf 方法将包装类型倒转成基本类型:
var a = 'abc'
var b = Object(a)
var c = b.valueOf() // 'abc'
复制代码
看看如下代码会打印出什么:
var a = new Boolean( false );
if (!a) {
console.log( "Oops" ); // never runs
}
复制代码
答案是什么都不会打印,因为虽然包裹的基本类型是 false ,但是 false 被包裹成包装类型后就成了对象,所以其非值为 false,所以循环体中的内容不会运行。
16. JavaScript 中如何进行隐式类型转换?
首先要介绍 ToPrimitive 方法,这是 JavaScript 中每个值隐含的自带的方法,用来将值 (无论是基本类型值还是对象)转换为基本类型值。如果值为基本类型,则直接返回值本身;如果值为对象,其看起来大概是这样:
/**
* @obj 需要转换的对象
* @type 期望的结果类型
*/
ToPrimitive(obj,type)
复制代码
type 的值为 number 或者 string。
- 当
type为number时规则如下:
- 调用 obj 的
valueOf方法,如果为原始值,则返回,否则下一步; - 调用 obj 的
toString方法,后续同上; - 抛出
TypeError异常。
- 当
type为string时规则如下:
- 调用 obj 的
toString方法,如果为原始值,则返回,否则下一步; - 调用 obj 的
valueOf方法,后续同上; - 抛出
TypeError异常。
可以看出两者的主要区别在于调用 toString 和 valueOf 的先后顺序。默认情况下:
- 如果对象为
Date对象,则type默认为string; - 其他情况下,
type默认为number。
总结上面的规则,对于 Date 以外的对象,转换为基本类型的大概规则可以概括为一个函数:
var objToNumber = value => Number(value.valueOf().toString())
objToNumber([]) === 0
objToNumber({}) === NaN
复制代码
而 JavaScript 中的隐式类型转换主要发生在+、-、*、/以及==、>、<这些运算符之间。而这些运算符只能操作基本类型值,所以在进行这些运算前的第一步就是将两边的值用ToPrimitive转换成基本类型,再进行操作。
以下是基本类型的值在不同操作符的情况下隐式转换的规则 (对于对象,其会被ToPrimitive转换成基本类型,所以最终还是要应用基本类型转换规则):
- +操作符+操作符的两边有至少一个string类型变量时,两边的变量都会被隐式转换为字符串;其他情况下两边的变量都会被转换为数字。
1 + '23' // '123'
1 + false // 1
1 + Symbol() // Uncaught TypeError: Cannot convert a Symbol value to a number
'1' + false // '1false'
false + true // 1
复制代码
- -、*、\操作符NaN也是一个数字
1 * '23' // 23
1 * false // 0
1 / 'aa' // NaN
复制代码
- 对于**==**操作符
操作符两边的值都尽量转成number:
3 == true // false, 3 转为number为3,true转为number为1
'0' == false //true, '0'转为number为0,false转为number为0
'0' == 0 // '0'转为number为0
复制代码
- 对于**<和>**比较符
如果两边都是字符串,则比较字母表顺序:
'ca' < 'bd' // false
'a' < 'b' // true
复制代码
其他情况下,转换为数字再比较:
'12' < 13 // true
false > -1 // true
复制代码
以上说的是基本类型的隐式转换,而对象会被ToPrimitive转换为基本类型再进行转换:
var a = {}
a > 2 // false
复制代码
其对比过程如下:
a.valueOf() // {}, 上面提到过,ToPrimitive默认type为number,所以先valueOf,结果还是个对象,下一步
a.toString() // "[object Object]",现在是一个字符串了
Number(a.toString()) // NaN,根据上面 < 和 > 操作符的规则,要转换成数字
NaN > 2 //false,得出比较结果
复制代码
又比如:
var a = {name:'Jack'}
var b = {age: 18}
a + b // "[object Object][object Object]"
复制代码
运算过程如下:
a.valueOf() // {},上面提到过,ToPrimitive默认type为number,所以先valueOf,结果还是个对象,下一步
a.toString() // "[object Object]"
b.valueOf() // 同理
b.toString() // "[object Object]"
a + b // "[object Object][object Object]"
复制代码
17. 为什么会有BigInt的提案?
JavaScript中 Number.MAX_SAFE_INTEGER 表示最⼤安全数字,计算结果是 9007199254740991,即在这个数范围内不会出现精度丢失(⼩数除外)。但是⼀旦超过这个范围,js就会出现计算不准确的情况,这在⼤数计算的时候不得不依靠第三⽅库进⾏解决,因此官⽅提出了 BigInt 来解决此问题。
18. object.assign和扩展运算法是深拷贝还是浅拷贝,两者区别
结论: 都为浅拷贝
扩展运算符:
let outObj = {
inObj: {a: 1, b: 2}
}
let newObj = {...outObj}
newObj.inObj.a = 2
console.log(outObj) // {inObj: {a: 2, b: 2}}
复制代码
Object.assign():
let outObj = {
inObj: {a: 1, b: 2}
}
let newObj = Object.assign({}, outObj)
newObj.inObj.a = 2
console.log(outObj) // {inObj: {a: 2, b: 2}}
复制代码
19. let、const、var的区别
块级作用域: 块作用域由 { } 包括,let 和 const 具有块级作用域,var 不存在块级作用域。块级作用域解决了 ES5 中的两个问题:
- 内层变量可能覆盖外层变量
- 用来计数的循环变量泄露为全局变量
变量提升: var 存在变量提升,let 和 const 不存在变量提升,即在变量只能在声明之后使用,否在会报错。
给全局添加属性: 浏览器的全局对象是 window,Node 的全局对象是 global。var 声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是 let 和 const 不会。
重复声明: var 声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的遍历。const 和 let 不允许重复声明变量。
暂时性死区: 在使用 let、 const 命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区。使用 var 声明的变量不存在暂时性死区。
初始值设置: 在变量声明时,var 和 let 可以不用设置初始值。而 const 声明变量必须设置初始值。
指针指向: let 和 const都是 ES6 新增的用于创建变量的语法。 let 创建的变量是可以更改指针指向(可以重新赋值)。但 const 声明的变量是不允许改变指针的指向。
| 区别 | var | let | const |
|---|---|---|---|
| 是否有块级作用域 | × | ✔️ | ✔️ |
| 是否存在变量提升 | ✔️ | × | × |
| 是否添加全局属性 | ✔️ | × | × |
| 能否重复声明变量 | ✔️ | × | × |
| 是否存在暂时性死区 | × | ✔️ | ✔️ |
| 是否必须设置初始值 | × | × | ✔️ |
| 能否改变指针指向 | ✔️ | ✔️ | × |
20. 如果new一个箭头函数的会怎么样
箭头函数是 ES6 中的提出来的,它没有 prototype,也没有自己的 this 指向,更不可以使用 arguments 参数,所以不能New一个箭头函数。
new 操作符的实现步骤如下:
- 创建一个对象
- 将构造函数的作用域赋给新对象(也就是将对象的__proto__属性指向构造函数的prototype属性)
- 指向构造函数中的代码,构造函数中的this指向该对象(也就是为这个对象添加属性和方法)
- 返回新的对象
所以,上面的第二、三步,箭头函数都是没有办法执行的。
21. 箭头函数与普通函数的区别
(1)箭头函数比普通函数更加简洁
- 如果没有参数,就直接写一个空括号即可
- 如果只有一个参数,可以省去参数的括号
- 如果有多个参数,用逗号分割
- 如果函数体的返回值只有一句,可以省略大括号
- 如果函数体不需要返回值,且只有一句话,可以给这个语句前面加一个void关键字。最常见的就是调用一个函数:
let fn = () => void doesNotReturn();
复制代码
(2)箭头函数没有自己的this
箭头函数不会创建自己的this, 所以它没有自己的this,它只会在自己作用域的上一层继承this。所以箭头函数中this的指向在它在定义时已经确定了,之后不会改变。
(3)箭头函数继承来的this指向永远不会改变
var id = 'GLOBAL';
var obj = {
id: 'OBJ',
a: function(){
console.log(this.id);
},
b: () => {
console.log(this.id);
}
};
obj.a(); // 'OBJ'
obj.b(); // 'GLOBAL'
new obj.a() // undefined
new obj.b() // Uncaught TypeError: obj.b is not a constructor
复制代码
对象obj的方法b是使用箭头函数定义的,这个函数中的this就永远指向它定义时所处的全局执行环境中的this,即便这个函数是作为对象obj的方法调用,this依旧指向Window对象。需要注意,定义对象的大括号{}是无法形成一个单独的执行环境的,它依旧是处于全局执行环境中。
(4)call()、apply()、bind()等方法不能改变箭头函数中this的指向
var id = 'Global';
let fun1 = () => {
console.log(this.id)
};
fun1(); // 'Global'
fun1.call({id: 'Obj'}); // 'Global'
fun1.apply({id: 'Obj'}); // 'Global'
fun1.bind({id: 'Obj'})(); // 'Global'
复制代码
(5)箭头函数不能作为构造函数使用
构造函数在new的步骤在上面已经说过了,实际上第二步就是将函数中的this指向该对象。 但是由于箭头函数时没有自己的this的,且this指向外层的执行环境,且不能改变指向,所以不能当做构造函数使用。
(6)箭头函数没有自己的arguments
箭头函数没有自己的arguments对象。在箭头函数中访问arguments实际上获得的是它外层函数的arguments值。
(7)箭头函数没有prototype
(8)箭头函数不能用作Generator函数,不能使用yeild关键字