1 数据类型
原始类型
- String
- Number
- Boolean
- null
- undefined
- Symbol
引用类型
- Object
- Array
- Date
- RegExp
- Symbol
问:说说JavaScript中的基本类型有哪些?以及各个数据类型是如何存储的?
堆栈池内存
- 栈:存放变量
- 堆:存放复杂对象
- 池:存放常量
栈和堆是两种基本的数据结构。栈在内存中自动分配内存空间,堆在内存中动态分配内存空间,不一定会自动释放,在项目中将对象类型手动置为null, 是为了减少无用内存消耗。
2 类型转换
数字类型转换
Number:
- 字符串 => 数字:非有效字符串转化为NaN;
- 布尔 => 数字:1或0
- null => 数字:0
- undefined => 数字:NaN
- ‘’ =>数字:0
- 对象 => 数字: 先把对象转化为字符串,再转化为数字
- [] => 数字:0 遵循对象转换的规则
Number(10); // 10
Number('10'); // 10
Number(null); // 0
Number(''); // 0
Number(true); // 1
Number(false); // 0
Number([]); // 0
Number([1,2]); // NaN
Number('10a'); // NaN
Number(undefined); // NaN
parseInt()/parseFloat()
对于字符串来说,从左到右依次查找有效的数字字符,直到遇到非有效数字字符,停止查找(不管后面是否还有数字)
let str = '12.5px'
parseInt(str) // 12
parseFloat(str) // 12.5
parseFloat(true) // NaN
字符串类型转换
String
对于原始类型来说,转字符串类型默认调用toString() 方法,在外层包一层引号
- 数字=> 字符串:包裹一层引号。
- NaN=>字符串: ‘NaN’
- true=>字符串: ‘true’
- null =>字符串:‘null’ (浏览器会报错(禁止你使用)—— 通常可以进行转换)
- undefined=> 字符串:‘undefined’ (浏览器会报错(禁止你使用)—— 通常可以进行转换)
- Object=>字符串:’[object,Object]’
普通对象转化为字符串为’[object,Object]’,因为Object.prototype.toString方法不是转化为字符串的,而是用来检测数据类型的。
String(123); // "123"
String(true); // "true"
String(null); // "null"(报错)
String(undefined);// "undefined"(报错)
String([1,2,3]) // "1,2,3"
String({}); // "[object Object]"
布尔类型转换
Boolean
- ‘’、 undefined、NaN、null、false、0 => false
- 其余转化为true
Boolean('') // false
Boolean(undefined) // false
Boolean(null) // false
Boolean(NaN) // false
Boolean(false) // false
Boolean(0) // false
Boolean({}) // true
Boolean([]) // true
3 隐式转换
什么是隐式转换?
在js中,当运算符在运算时,如果两边数据不统一,CPU就无法计算,这时编译器会自动将运算符两边的数据做一个数据类型转换,这种由编译器自动转换的方式就称为隐式转换
隐式转换的规则
转成string类型
- +(字符串连接符)
转成number类型
- ++/–(自增自减运算符)
- ±*/%(算术运算符)
- > < >= <= == !== === (关系运算符)
转成boolean类型
- ! (逻辑非运算符)
面试题大坑
坑一:字符串连接符与算数运算符隐式转换规则混淆
// 字符串连接符(只要+号两边有一边是字符串)
1+"true" // 1true
// 算数运算浮(会把其他数据类型调用Number()方法转换成数字)
1+true // 2
1+undefined //NaN
1+null // 1
坑二:关系运算符:会把其他数据类型转换成number之后再比较关系
"2" > 10 // false
"2" >"10" // true,两边都是字符串时按照unicode编码 (字符串。charCodeAt())转换成数字
"abc" > "b" // false, 先比较'a'和'b', 'a'不大于'b',直接返回结果
"abc" > "aad" // true, 先比较'a'和'a',两者相等,接着比较'b'和'a',得出结果
NaN == NaN // false, NaN与任何数据比较都是NaN
undefined==null // true,如果数据类型是undefined和null ,得出固定结果true
坑三:复杂数据类型在隐式转换时会先转换成String, 再转换成Number运算
var a=??
if(a == 1 && a == 2 && a==3) {
console.log(1)
} // 如何完善a, 使其正确打印1
// 原理:
// 重写对象的valueOf() 方法
var a = {
i:0,
valueOf: function() {
return ++a.i
}
}
if(a==1 && a==2 && a==3) { // 每一次运算时都会调用一次a的valueOf方法
console.log("1");
}
坑四:逻辑非隐式转换与关系运算符隐式转换混淆
空数组的toString()会得到空字符串,而空对象的toString() 方法会得到字符串’[object, Object]’, Number(’[object, Object]’) == NaN, NaN与任何数据比较都不等
- []==0, true
- ![]==0, true
- []==![], true
- []==[], false
- {} = !{}, false
- {} ={}, false
[]==0 // true, [].valueOf().toString() = '', Number('') ==0 成立
![]==0 // true, 逻辑非优先级高于关系运算符, ![]==false(空数组转布尔得到true),false==0 成立
[]==![] // true, ![]== 0, []==0
[] == [] // false, 引用类型数据存在堆中,栈中存的是地址,所以结果为false
{} ==!{} // false, !{} == false, Number({}.valueOf().toString()) == NaN
{} == {} // false, Number({}.toString()) == NaN
4 数据类型检测
- typeof
- instanceof
- constructor
- Object.prototype.toString.call()
typeof
typeof是一元运算符,同样返回一个字符串类型。一般用来判断一个变量是否为空或者是什么类型。
typeof undefined // 'undefined'
typeof '10' // 'String'
typeof 10 // 'Number'
typeof false // 'Boolean'
typeof Symbol() // 'Sysbol'
typeof Function // 'function'
typeof null // 'Object'
typeof [] // 'Object'
typeof {} // 'Object'
instanceof
instanceof 运算符用来测试一个对象在其原型链中是否存在一个构造函数的prototype属性。可以使用instanceof来进行判断某个对象是不是另一个对象的实例
instanceof 的原理是通过判断该对象的原型链中是否可以找到该构造类型的prototype类型
function Foo() {}
var f1 = new Foo();
console.log(f1 instanceof Foo); // true
constructor
对于引用类型,除此之外,还可以使用xx.constructor.name
构造函数来判断。
// constructor 构造来判断
console.log(fn.constructor.name) // Function
console.log(date.constructor.name)// Date
console.log(arr.constructor.name) // Array
console.log(reg.constructor.name) // RegExp
Object.prototype.toString.call()
一种最好的基本类型检测方式,它可以区分null、sring、boolean、number、undefined、array、function、object、date、 math数据类型
// 判断基本类型
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(“abc”);// "[object String]"
Object.prototype.toString.call(123);// "[object Number]"
Object.prototype.toString.call(true);// "[object Boolean]"
// 判断引用类型
function fn() {
console.log(“xiaolu”);
}
var date = new Date();
var arr = [1,2,3];
var reg = /[hbc]at/gi;
Object.prototype.toString.call(fn); // "[object Function]"
Object.prototype.toString.call(date); // "[object Date]"
Object.prototype.toString.call(arr); // "[object Array]"
Object.prototype.toString.call(reg); // "[object RegExp]"
5 如何判断数组
**使用instanceof方法: ** 用于判断一个变量是否某个对象的实例
var arr = []
console.log(arr instanceof Array) // true
**使用constructor方法: **
console.log([].constructor == Array) // true
使用Object.prototype.toString.call()方法:
var arr = []
console.log(Object.prototype.toString.call(arr) === '[object Array]') // true
ES5的Array.isArray :
console.log(Array.isArray([])) // true
6 == 和 ===的区别
=== :严格意义上的相等,会比较两个操作符的类型和值。
-
如果 X 和 Y 的类型不同,返回 false ;
-
如果 X 和 Y 的类型相同,比较值的大小。
== :非严格意义上的相等,先判断两个操作符的类型是否相等,如果类型不同,则先进行类型转换,然后再判断值是否相等。
-
如果 X 和 Y 的类型相同,返回 X == Y 的比较结果;
-
如果 X 和 Y 的类型不同,根据下方表格进一步判断;
null == undefifined -> true
String == Number -> 字符串转数字,再比较
Boolean == Number -> 布尔转数字,再比较
Object == String,Number,Symbol -> Object 转化为原始类型
7 dom0级事件dom2级事件
dom0级事件
原理:
- 给当前元素的某一私有属性(例如onclick)赋值的过程,之前属性默认值是null,如果我们赋值了一个函数,就相当于绑定了一个方法)
- 当我们赋值成功(赋值一个函数),此时浏览器会把DOM元素和赋值的函数建立关联,以及建立DOM元素的行为监听,当某一行为被用户触发,浏览器会把赋值的函数执行;
分为两种:
一是在标签内写事件:
<button onclick="fn()">按钮</button>
二是获取元素绑定事件:
<button id="btn">按钮</button>
var btn = $('#btn').get(0)
btn.onclick = function() {}
注:
第二种方法会覆盖第一种
dom2级事件
原理:
- DOM2事件绑定是在eventTarget这个内置类的原型上定义的,调用的时候,首先要通过原型链找到这个方法,然后执行完成事件绑定
- 浏览器会给当前元素的某个事件行为开辟一个事件池(事件队列)
- 当元素的某一行为被触发,浏览器回到对应事件池中,把当前放在事件池的所有方法按序依次执行
方法: addEventListener()和removeEventListener()。
- addEventListener(): 可以为元素添加多个事件处理程序,触发时会按照添加顺序依次调用。
- removeEventListener():不能移除匿名添加的函数。
参数:
- 第一个参数是事件名(如click)
- 第二个参数是事件处理程序函数
- 第三个参数如果是true则表示在捕获阶段调用,为false表示在冒泡阶段调用。
事件流:
- 事件捕获阶段
- 处于目标阶段
- 事件冒泡阶段
document.getElementById("btn").attachEvent("onclick", function() { }); // 只有ie支持
//等价于
document.getElementById("btn").addEventListener("click", function(){alert(1)}, false);
注:
dom0级与dom2级是共存的 不会覆盖
8 常用数组,字符串方法
常用数组方法
- push():从尾部添加一个或多个元素
- pop():删除数组的最后一个元素
- shift():删除数组的第一个元素
- unshift():向数组的开头添加一个或多个元素
- splice(start,0,item):像数组中间添加元素
- concat(数组||字符串):把两个或多个数组连接起来
- join():将数组转化为字符串
- toString():数组转化为字符串
- reverse():数组反转
- sort():数组排序
- indexOf() :返回指定元素在数组中出现的位置,没有则返回-1
- slice():截取几个连续的元素组成新数组
常用字符串方法
- chartAt():返回指定位置的字符
- indexOf():返回字符串中一个子串第一处出现的索引
- concat():将多个字符组合起来
- match():在字符串内检索指定的值,或找到一个或多个正则表达式的匹配
- replace():在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串
- search():检索字符串中指定的子字符串,或检索与正则表达式相匹配的子字符串
- split():字符串转化为数组
- slice(start,end):提取字符串的某个部分,并以新的字符串返回被提取的部分
- substr(start,length):在字符串中抽取从 start 下标开始的指定数目的字符
- substring(start,stop):提取字符串中介于两个指定下标之间的字符
9 数组去重
问:如何给一个数组去重
通过实现一个函数,将下列数组中重复的元素进行删除
[1,2,1,1,1,2,3,3,3,2]
结果:
[1,2,3]
最Low的两种方案
-
创建一个新数组,然后遍历老数组,每次检查当前元素是否在新数组中,如果在,则不加入。
let ary = [1,2,1,1,1,2,3,3,3,2] let newAry = [] for(let i = 0;i < ary.length; i++){ let item = ary[i]; if(!newAry.includes(item)){ newAry.push(item) } } console.log(newAry)
-
每拿出一项数据,就与当前数组其他数据做对比,相同的数据进行删除,依次遍历所有数据
let ary = [1,2,1,1,1,2,3,3,3,2] for(let i = 0;i < ary.length; i++){ var item = ary[i]; for(let j = i + 1; j < ary.length; j++){ var compare = ary[j]; if(compare === item){ ary.splice(j, 1); // 数组塌陷问题:J 后边每一项索引都提前了一位,下次要比较应该 还是 j 这个索引 j--; } } }
最优秀的两种方案
方案一:基于对象去重
对象去重,最主要的是我们优化了上述谈到的数组塌陷问题。如果当前是重复的数据,就将数组的最后一个数据移动到当前数据进行替换,这样重复的数据删除了,而且数组塌陷问题也得到解决。
let obj = {}
for(let i = 0;i < ary.length; i++){
let item = ary[i]
if(obj[item]){
// 将最后一项填补当前项
ary[i] = ary[ary.length - 1]
// 数组长度减一
ary.length--;
// 索引减-
i--;
continue;
}
obj[item] = item
}
console.log(ary)
方案二: 基于Set去重
Set是ES6的新语法,可以去掉重复的元素
ary = new Set(ary); console.log([...ary])
10 数组展平
const arr = [1, 2, 3, [4,5,[5, 6], 7, 8], 9, [10,11,[12]]]
用现成api
flat(depth)
- depth: 指定要提取嵌套数组的结构深度,默认值为1
- depth取 Infinity, 展开任意深度嵌套的数组
const arr1 = arr.flat(Infinity)
利用join() 展平
const arr2 = arr.join().split(',').map(Number)
利用toString()直接展平
const arr3 = arr.toString().split(',').map(Number)
原生循环递归实现
function myFlat(arr) {
const result = []
arr.forEach((item)=> {
if(Array.isArray(item)) {
result.push(...myFlat(item))
} else {
result.push(item)
}
})
return result
}
cosnt arr4 = myFlat(arr)