类型检测、快速区分
(1)JS有几种数据类型,哪几种是新增的*
JS有八种基础数据类型:undefined、null、boolean、number、string、object、symbol、bigInt
新增的是symbol和bigInt
symbol:是独一无二的,且是不可变的,用来解决全部变量冲突和内部变量覆盖
bigInt:可以表示任意精度的正数,安全的存储和操作大数据,即使超出了number的安全整数范围
(2)基础数据类型如何进行分类,使用起来有什么区别,使用过程中如何区分它们**
可以区分为原始数据类型和引用数据类型
原始数据类型:undefined、null、boolean、number、string
引用数据类型:对象、数组、函数
两种类型的区别:
- 使用起来效果不同: 原始数据类型直接赋值后,不存在引用关系;
引用数据类型被赋值后,会产生属性引用关系,修改之后的数据,原始数据也会被修改
- 存储位置不同: 原始数据额类型是栈存储 => 先进后出栈维护结构 => 在操作系统中,栈区是由编译器自动分配释放 => 更多是以临时变量方式的去界定(变化很频繁);
引用数据类型是堆存储: => 堆内存是由开发者进行分配释放的 => 开发者不释放,直到程序结束前,这段内存都会被占用
原始数据:放置在栈中,空间小、大小固定、操作频繁
引用类型:数据量大、大小不固定、赋值给的是地址(所以赋值后,修改会影响原始数据)
(3)如何进行类型区分判断?*
typeof:
typeof 2 // number
typeof true // boolean
//问题:无法区分对象和数组
typeof {} //object
typeof [] //object
有限哪些需要注意的特例:
typeof null //object
typeof NaN //number
NaN用来判断当前变量不是一个数字,但NaN本身是一个数字
instanceof:
2 instanceof Number //false
[] instanceof Array //true
可以判断数组、对象、函数,但有一点,当不知道这个类型是什么时候,需要一个个试,才可能试到结果为true
手写instanceof的原理实现***:
原理是通过查找原型链,判断类型
function myInstance(left,right){
//获取对象的原型
let _proto = Object.getPrototypeOf(left)
//构造函数的prototype
let _prototype = right.prototype
while(true){
//左侧原型不存在
if(!_proto){
return false
}
//左右两侧全等
if(_proto === _prototype){
return true
}
//没找到,再往上找一层
_proto = Object.getPrototypeOf(_proto)
}
}
// 测试
function Test(){}
let test = new Test()
console.log(myInstance(test,Test())) //true
constructor:通过构造函数
(2).contructor === Number //true
([]).contructor === Array //true
问题、隐患:contructor本质上代表的是构造函数指向的类型,不是构造出来的实例指向的类型。构造函数是可能被修改的
function Fn(){}
Fn.prototype = new Array() //构造函数被修改
var f = new Fn()
使用contructor要确保构造函数的指向是无误的
Object.prototype.toString.call()
let a = Object.prototype.toString
a.call(2) //number
a.call([]) //[object array]
a.call({}) //[object object]
为什么使用call,使用obj.toString()结果就不一样**?
因为要保证toString()是Object上的原型方法,根据原型链知识,优先调用本对象的属性,没有时再去原型链上找,防止toString被改写
当对象中有个属性和Object的属性重名时,使用的顺序是什么样的:优先使用自己的
如果要优先使用Object的属性,如何做:使用call(类似Object.prototype.toString.call())
类型转换
(1)isNaN 和 Number.isNaN 的区别**
isNaN包含一个隐式转换:isNaN => 接受参数 => 尝试参数转成数值型 => 不能被转数值的参数,返回true => 会导致非数字值传入返回true
Number.isNaN => 接受参数 => 判断参数是否为数字 => 如果是数字,再判断是否为NaN => 不会进行数据类型转换
(2)有没有其他的类型转换场景?***
- 所有的类型转换成字符串:
Null,Undefined => 'null','undefined'
Boolean => 'ture'/'false'
Number => '数字'
大数据会转换成带有指数形式的
Symbol => '字符串内容'
普通对象 => '[Object Object]'(所以先转成字符串再转成对象)
- 转成数字 undefined => NaN
Null => 0
Boolean => true-1 , false-0
String => 包含了非数字的值-NaN,纯数字的值-对应的数字,如果是空字符串-0
Symbol => 因为不可变,不能被转为数字,报错
对象 => 先转换成相对应的基本值类型 => 再进行上面基本类型相应的转换
- 转成Boolean:
undefined,null,false,-0,+0,NaN,'' => false
(3)原始数据类型如何具有属性操作***
如let a = 6; 可以进行访问a.length
原始数据类型,在调用属性和方法时,js会在后台隐式的将基本类型转换成对象
前置知识:js包装对象
let a = 'abc'
a.length //3
//js在收集阶段
Object(a) //String{'abc'}
//去包装
let a = 'abc'
let b = Object(a)
let c = b.valueOf() // 'abc'
这就是为什么使用call,使用obj.toString()结果就不一样:因为有包装类型,包装类型之后会对原有的toString()做改写,a.toString()实际上是String类型的对象,String类型的对象的上一级才是Object,根据原型链知识,会优先调用String的toString,而不是Object的toString,只有Object的toString才会打印出[object string]、[object object]、[object array],String的toString会打印出具体的值,如上面定义的'abc'
代码的执行结果:
let a = new Boolean( false ) //a=>Boolean{},也就是a是一个对象
if(!a){ //对对象取反,就是false
console.log('abc') //不执行
}
数组操作的相关问题
数组的的基本操作方法,如何使用*
- 转换方法:toString(),toLocalString(),join()
- 尾部操作:pop(),push()
- 首部操作:shift(),unshift()
- 排序:recerse(),sort()
- 连接:concat()
- 截取:slice()
- 插入:splice()
- 索引:indexOf(),lastIndexOf()
- 迭代方法:every(),some(),filter(),map(),forEach()
- 归并:reduce()
数组中改变数组的方法:
- push 是往数组的末尾添加数据,返回值是数组的长度,同时原数组会改变
- pop 是删除数组最后一个元素,这次返回的可是被删除的元素数组,同时原数组改变
- unshift 是向数组开头添加一个或者多个的元素,返回值是数组的长度,同时原数组会改变
- shift 是删除数组的第一个元素 并且返回被删除的元素 同时原数组改变 例如:var arr = ['a','b','c'];
console.log(arr.splice(1,0)) //从下标1开始删除0个元素,返回的是个空数组
// console.log(arr.splice(1,1)) // 从下标1开始删除一个元素,返回的是删除的元素 b
// console.log(arr.splice(1,0,'hello','world')) // 从下标1开始删除0个元素,添加2个新的元素, 返回的是['a', 'hello', 'world', 'b', 'c']
- reverse 将数组倒序(原数组改变)
- sort 将数组进行升序排列 (只能是数字或者字母) var arr = [2,5,4,1,3,6]
arr.sort((a,b)=>{ return a - b }) //升序
arr.sort((a,b)=>{ return b - a }) //降序
数组中不改变数组的方法:
- Array.filter() 会新创建一个数组 将符合条件的值丢进去
- Array.concat() 连接多个数组(拼接在一起) 返回一个新的数组
- Array.slice() 对数组中的数据进行截取 放到一个新的数组里返回
- Array.join() 将数组通过某个分隔符分割转换成字符串 返回的是一个字符串
- Array.map() 对数组的每一项进行处理 返回到一个新的数组里
- Array.every() 判断数组中的每一项是否符合规范,都符合返回一个true ,否则返回false
- Array.some() 判断数组,如果其中有一项符合条件的话就返回true,都不符合返回false
- Array.indexOf() 找索引,如果找到则返回相应的索引值,否则返回 -1
变量提升、作用域
谈谈对变量提升及作用域的理解
- 现象:无论在任何位置声明的函数、变量,都被提升到模块、函数的顶部
- js实现原理:分为解析和执行,解析时:检查语法、预编译,把代码中即将执行的变量和函数声明调整到全局顶部,并且赋值为undefined,然后再加上上下文、arguments、传入函数参数,再统一执行预编译; 就产生了全局上下文:变量定义、函数声明
函数上下文:变量定义、函数声明、this、arguments
再去执行阶段,按照代码顺序从上而下逐行运行
- 变量提升存在的意义:提高性能(否则每次调用都会重新解析变量);更加灵活(允许先使用,后定义)
- 提出特殊情况:ES6中新增的let const取消了变量提升机制
闭包
(1)闭包的概念、作用
闭包:在一个函数中访问另一个函数作用域中变量的方法
优点:可以重复使用变量,不会造成变量污染
缺点:可能会引起内存泄漏,解决办法:在退出函数之前,将不再使用的局部变量全部删除
作用:函数的外部可以访问到函数内部的变量,跨作用域,创建私有变量(问题:已经运行结束的逻辑,依然残留在闭包里,变量得不到回收)
(2)闭包经典题目
for(var i=0;i<5;i++){
setTimeout(()=>{
console.log(i) //会打印5个5
},i*1000)
}
//利用闭包解决
for(var i=0;i<5;i++){
(function(j){ //产生独立的运行环境
setTimeout(()=>{
//每次传入参数j,就会立即执行一次
console.log(j) //会打印0、1、2、3、4
},j*1000)
})(i)
}
// 利用let块级作用域解决
for(let i=0;i<5;i++){
setTimeout(()=>{
console.log(i) //会打印0、1、2、3、4
},i*1000)
}
ES6
(1)const对象的属性可以修改吗
const只能保证指针是固定不变的,指向的数据结构属性,无法控制是否变化的
(2)new一个箭头函数会发生什么
new执行的全过程:
- 创建一个对象
- 构造函数作用域赋给新对象(也就是把对象的原型属性指向构造函数的prototype属性)
- 指向构造函数后,构造函数中的this指向该对象
- 返回一个新的对象(也就是实例化之后的实例对象)
箭头函数,没有prototype,也没有独立的this指向,更没有arguments
(3)JS和ES中内置对象有哪些
- 值属性类:Infinity、NaN、undefined、null
- 函数属性:eval()、parseInt()
- 对象:Object、Function、Boolean、Symbol、Error
- 数字:Number、Math、Date
- 字符串:String、RegExp
- 集合类:Map、Set、weakMap、weakSet
- 抽象控制:promise
- 映射:proxy
原型、原型链
(1)对原型、原型链的理解
构造函数:是JS中用来构造新建一个对象的
构造函数内部有一个属性 prototype,它的值是一个对象,包含了共享的属性和方法
使用了构造函数创建对象后,被创建的对象内部会存在一个指针(_proto_),它指向构造函数prototype属性的对应值
JS规定了一个链式获取属性的规则:获取对象的属性时,先去对象内部本身是否包含该属性,不包含,就会顺着指针去原型对象里查找(也就是构造函数的prototype),还不包含,再往上层级里去查找,直到找到null
(2)继承方式
异步编程
(1)遇到哪些异步执行方式:
- 回调函数 => callback(会造成回调地狱)
- promise => 使用链式调用(会造成语义不明确,阅读不方便)
- generator => 步进的方式,需要考虑如何控制执行
- async await => 在不改变同步书写习惯的前提下,进行异步处理(promise+generator的语法糖)
(2)对promise的理解
- 是一个对象,一个容器,承载着未来会触发的一个流程或操作
- 有三个状态:pending、resolved、rejected
- 两个过程,只能一次性单向从pending => resolved或者pending => rejected
- 缺点:无法取消;处于pending状态时,无法确定当前的吸粉状态
内存和浏览器执行问题
(1)垃圾回收(GC):收集不使用的变量,释放其占用的内存空间
JS有两种变量,局部变量、全局变量
(2)现代浏览器如何处理垃圾回收:标记清除、引用计数
(3)减少垃圾的方案:
- 数组优化:清空数组时,赋值一个[](会创建一个新的对象) => length = 0
- object优化:对象尽量复用,减少深拷贝
- 函数优化:循环中的函数表达式,尽量统一放在外面
JS浅拷贝和深拷贝
赋值不属于拷贝
首先,大家需要区分,赋值不属于拷贝:
let arr = [1,2,3]
let arr1 = arr
// 这里仅仅是把数组的内存地址赋值给arr1,这里不叫拷贝
概念
浅拷贝与深拷贝主要是作用于多层级数组或对象时存在的情况,多层级数组及对象举例如下:
let arr = [1,2,[3,4],{n:1}] // 多层级数组,数组里还有数组或对象
let obj = {a:1,b:2,c:{d:3,e:[1,2]}} // 多层级对象,对象里还有数组或对象
(1)浅拷贝:指只对对象或数组的第一层进行复制,其他层级复制的是所存储的内存地址。举例如下:
let arr = [2, 3, [4, 6]]
let arr1 = [...arr] // 这里我们运用扩展运算符浅拷贝了数组arr
console.log(arr === arr1)
// false,可以看到浅拷贝的数组arr1和arr指向的是不同的内存地址
arr[0] = 0 // 这里改动原数组第一个元素
console.log(arr) // [0,3,[4,6]],原数组发生了变化
console.log(arr1) // [2,3,[4,6]],新数组无变化
console.log(arr[2] === arr1[2])
// true,但是它们的第三个元素[4,6],指向的都是同一个数组
// 这里我们修改原数组的第三个元素[4,6]的第一个元素,把4改为1
arr[2][0] = 1
console.log(arr1) // [2, 3, [1, 6]],此时打印新数组,发现它也发生了改变
通过上例可以看出浅拷贝虽然复制出了一个新的数组,但是当数组的元素为引用数据类型时,浅拷贝只拷贝了地址,通过原数组改动这个地址指向的数组,新数组同样也会发生变化。
(2)深拷贝:会构造一个新的复合数组或对象,遇到引用所指向的引用数据类型会继续执行拷贝。用于解决浅拷贝只能拷贝一层的情况。举例如下:
let arr = [2, 3, [4, 6]]
let arr1 = JSON.parse( JSON.stringify(arr) )
// 通过数组转字符串再字符串转数组的方法进行了深拷贝
console.log(arr === arr1)
// false,可以看到深拷贝的数组arr1和arr指向的是不同的内存地址
console.log(arr[2] === arr1[2])
// false,即使是数组里第二层级的数组也是不相同
通过上例可以看出深拷贝是每一个层级都在堆内存中开辟了新的空间,是拷贝了一个全新的数组或对象,不会受原数组或原对象的影响。
实现浅拷贝的常用方法
方法1:通过扩展运算符实现
扩展运算符的方式既可以浅拷贝数组(上面已举例),也可以浅拷贝对象,这里我们再举一个浅拷贝对象的例子:
let obj = {a:1,b:2,c:{d:3,e:[1,2]}}
let obj1 = {...obj}
// 通过扩展运算符浅拷贝,获得对象obj1
console.log(obj === obj1)
// false,obj和obj1分别指向不同的对象
console.log(obj.c === obj1.c)
// true,但是obj的c属性的值和obj1的c属性的值是同一个内存地址
方法2:通过Object.assign方法实现
Object.assign()方法只适用于对象,可以实现对象的合并,语法:
Object.assign(target, source_1, ..., source_n).
Object.assign()方法会将source里面的可枚举属性复制到target,复制的是属性值,如果属性值是一个引用类型,那么复制的是引用地址,因此也属于浅拷贝。举例如下:
let target= {
name: "小明",
}
let obj1 = {
age: 28,
sex: "男",
}
let obj2 = {
friends: ['朋友1','朋友2','朋友3'],
sayHi: function (){
console.log( 'hi' )
},
}
let obj = Object.assign(target,obj1,obj2)
console.log(obj === target) // true,因此可以用变量接收结果,也可以直接使用target
obj1.age = 30 // 把obj1的age属性值改成30
console.log("target",target)
console.log("obj1",obj1)
上面打印结果如下:
我们可以看出返回的结果obj和target都指向浅拷贝的新对象,修改obj1的属性age不会影响target的age属性值。
此时给target的friends属性添加一个新的朋友4,操作如下:
target.friends.push("朋友4")
console.log("target",target)
console.log("obj2",obj2)
我们再来看看上面的打印结果:
此时target的friends属性和obj2的friends属性的值指向同一个数组。
实现深拷贝的常用方法
方法1:通过递归复制所有层级实现
这里我们通过封装一个deepClone函数来实现深层次拷贝,该方法适用于对象或数组,代码如下:
let obj = {
name: '小明',
age: 20,
arr: [1, 2],
}
function deepClone(value) {
// 判断传入参数不是对象或数组时直接返回传入的值,不再执行函数
if (typeof value !== 'object' || value == null) {
return value
}
//定义函数的返回值
let result
// 判断传进来的数据类型数组还是对象,对应创建新的空数组或对象
if (value instanceof Array) {
result = []
} else {
result = {}
}
// 循环遍历拷贝
for (let key in value) {
//函数递归实现深层拷贝
result[key] = deepClone(value[key])
}
// 将拷贝的结果返回出去
return result
}
let newObj = deepClone(obj)
obj.arr[0] = 0 // 修改原对象的arr属性对应的数组的元素值
console.log("obj",obj)
console.log("newObj ",newObj )
以下是上面代码的打印结果:
我们可以看到深层递归的方式不会复制引用地址,所以用原对象obj修改其arr属性对应的数组的元素,并不会影响新的对象newObj。
方法2:通过JSON对象的stringify和parse方法实现
上面我们讲解深拷贝概念时用过该方法深拷贝数组,这里我们举例来深拷贝对象:
let obj = {
name: '小明',
age: 20,
arr: [1, 2],
}
let obj1= JSON.parse( JSON.stringify(obj) )
console.log(obj.arr === obj1.arr)
// false,此时obj的arr属性和obj1的arr属性值不是同一个数组
通过代码我们可以发现,JSON.stringify()方法会把obj先转化为字符串,字符串就已经不代表任何空间地址了,就是单纯的字符串,而JSON.parse()方法把字符串解析成新对象,对象的每个层级都会在堆内存中开辟新空间。
总结
JS的浅拷贝与深拷贝主要是作用于多层级数组或对象中。浅拷贝是只复制创建数组或对象的第一层,其他层级和原数组或对象拥有相同地址值,因此修改浅拷贝的数组或对象的深层的数值就会影响原数组或对象的值。而深拷贝则是拷贝一个全新的数组或对象,每一个层级都在堆内存中开辟了新的空间,和原数组或对象相互不影响
无论是浅拷贝还是深拷贝,一般都用于操作Object 或 Array之类的复合类型。
比如:想对某个数组 或 对象的值进行修改,但是又想保留原来数组 或 对象的值不被修改!
此时:就可以用深拷贝来创建一个新的数组 或 对象,从而达到操作(修改)新的数组 或 对象时,保留原来数组 或 对象。
在JS中还有一些原生封装好的浅拷贝方法:
如数组方法:concat(),filter(),slice(),map()等。
它们在修改数组时,不会修改原来的数组,而是返回一个新的数组。