护驾
前端面试必不可少的一问,那就是JavaScript。在此,我已经嗝屁很多回了。立个Flag—拿捏住JavaScript,死死地!。Flag还是要立,万一实现了呢?
JavaScript的核心知识点
变量类型与浅拷贝、深拷贝
🙋:JS的数据类型有哪些?
基本数据类型:字符串(String)、数字(Number)、布尔(Boolean)、对空(Null)、未定义(Undefined)、Symbol(ES6新增,表示独一无二的值)、BigInt(ES10新增,表示大于2^53-1的整数)。
引用数据类型:对象(Object)、数组(Array)、函数(Function),还有两个特殊的对象:正则(RegExp)和日期(Date)。
Tips📎:
1.基本数据类型保存在栈(stack)内存中;引用数据类型保存在堆(heap)内存中。
2.栈:自动分配内存空间,系统自动释放,里面存放的基本数据类型的值和引用类型的地址;堆:动态分配的内存,大小不定,也不会自动释放,里面存放的是引用类型的值。
🙋:什么是浅拷贝、深拷贝?如何实现浅拷贝、深拷贝?
浅拷贝:创建新的数据,这个数据有着原始数据属性值的一份精确拷贝,如果属性是基本类型,拷贝的就是基本类型的值;如果属性是引用类型,拷贝的就是内存地址,指向同一个object。
/*------ 实现浅拷贝 ------*/
// 1.利用Object.assign():可以把任意多的源对象自身的枚举属性拷贝给目标对象,然后返回目标对象(针对对象只有一层,没有嵌套的情况)
var obj = { name: '偷一只猪ma' }
var imoon = Object.assign({}, obj)
console.log(imoon); //{name:'偷一只猪ma'},改变obj的值,imoon的值也会跟着改变
// 2.ES6展开运算符...
var obj = { name: '偷一只猫ma' , info : { age: 1 }}
var copy = { ...obj }
obj.info.age = 18
console.log(copy.info.age) //18
// 3.数组只有一层的话,可以利用concat、slice
var arr = [1, 2, 3]
var arr1 = arr.concat()
console.log(arr1); //[1,2,3]
var arr2 = arr.slice()
console.log(arr2); //[1,2,3]
深拷贝:开辟了一个新的内存空间,两个对象属性完全相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性。
/*------ 实现深拷贝 ------*/
// 1.lodash库中的 _.cloneDeep(拷贝对象)
// 2.JSON.parse(JSON.stringify(拷贝对象))
//但是这种方式会忽略undefined、symbol和函数
// 3.递归
function deepClone (obj) {
let objClone = Array.isArray(obj) ? [] : {}
if (obj && typeof obj === 'object') {
for (const key in obj) {
if (Object.hasOwnProperty.call(obj, key)) {
if (obj[key] && typeof obj[key] === 'object') {
objClone[key] = deepClone(obj[key])
} else {
objClone[key] = obj[key]
}
}
}
}
return objClone
}
let arr = { name: '1', info: { age: 12 } }
let arr1 = deepClone(arr)
arr.info.age = 19
console.log(arr1.info.age); //12
🙋:如何判断数据类型?
💧typeof
一般用来判断基础数据类型,不能判断Array、Object、Null、RegExp、Date,都会返object。
typeof Symbol() //symbol
typeof '' //string
typeof 1 //number
typeof true //boolean
typeof null //object
typeof undefined //undefined
typeof [] //object
typeof new Function() //function
typeof new RegExp() //object
typeof new Date() //object
typeof {} //object
💧💧instanceof
用来判断A是否为B的实例,表达式:A instanceof B。如果A是B的实例,则返回true,否则返回false。instanceof用来测试一个对象在其原型链中是否存在一个构造函数的prototype属性,但不能检测null和undefined。
[] instanceof Array //true
[] instanceof Object //true
/s/g instanceof RegExp //true
new Date('2022/03/14') instanceof Date //true
[1,2,3] instanceof Array //true
💧💧💧constructor
- null和undefined是无效对象,不会存在constructor。
- constructor是不稳定的,把类的原型进行重写,在重写的过程中会把原来的覆盖掉。
💧💧💧💧Object.prototype.toString.call()
判断类型最准的一个方法。
🙋:什么是深拷贝、浅拷贝?如何实现一个深拷贝、浅拷贝?
深拷贝
变量作用域与解构赋值
🙋:JS的变量作用域?
变量作用域:变量的可用性范围,ES5可分为全局作用域、局部作用域,ES6则在原有基础上新增一个块级作用域。
全局作用域:不在任何函数内定义的变量就具有全局作用域。全局作用域的变量实际上被绑定在window的一个属性上。
var imoon = "偷一只猪ma"
console.log(imoon); //偷一只猪ma
console.log(window.imoon); //偷一只猪ma
/*以变量方式定义的函数也是一个全局变量*/
function stealPig () {
console.log('偷一只猪ma');
}
stealPig() //偷一只猪ma
window.stealPig() //偷一只猪ma
局部作用域:函数内部声明的变量,只能在函数内部被访问。
function stealPig(){
var a=1;
let b=2;
console.log(a,b) //1 2
}
stealPig()
console.log(a) //报错,a is not undefined
console.log(b) //报错,b is not undefined
块级作用域:使用let/const声明的变量,且只大括号里面起作用
if (true) {
var a = 1 //a为全局变量
let b = 2 //b为块级变量,只在大括号里面起作用
console.log(a, b); //1 2
}
console.log(a); //1
console.log(b); //报错,b is not undefined
解构赋值:
从ES6开始,JavaScript引入了解构赋值。
/*对多个变量进行解构*/
let [x, y, z] = [1, 2, 3]
console.log(x); //1
console.log(y); //2
console.log(z); //3
/*数组本身嵌套,注意嵌套层次和位置要保持一致*/
let [a, [b, c]] = [10, [20, 30]]
console.log(a); //10
console.log(b); //20
console.log(c); //30
/*对一个对象进行解构*/
/*对象本身嵌套,也要注意嵌套层次一致*/
var imoon = {
name: '偷一只猪ma',
age: 18,
address:{
city:'chengdu'
}
}
var { age,address:{city} } = imoon
console.log(age); //18
console.log(city); //chengdu
/*把对象的password属性赋值给变量id*/
var imoon = {
name: '偷一只猪ma',
age: 18,
city: "chengdu",
password: '123'
}
let { name, password: id } = imoon
console.log(name); //偷一只猪ma
console.log(id); //123
console.log(password); //报错,password is not defined
/*single是person中不存在的属性,避免返回undefined,则使用默认值*/
var imoon = {
name: '偷一只猪ma',
age: 18
}
let { name, single=true } = imoon
console.log(name);
console.log(single);
变量提升:
JavaScript的函数定义有个特点,会先扫描整个函数体的语句,把所有声明的变量提升到函数顶部。
function stealPig(){
var x='hello'+y
console.log(x)
var y='偷一只猪ma'
}
/*JS看到的如下:*/
function stealPig(){
var y
var x='hello'+y
console.log(x) //helloundefined
y='偷一只猪ma'
}
foo()
闭包
🙋:什么是闭包?
闭包有3个特性:
- 1.函数嵌套函数;
- 2.函数内部可以访问函数外部的参数和变量;
- 3.参数和变量不会被垃圾回收机制回收;
举几个🌰:
🌰 One: 创建闭包最常用的方式,就是在一个函数内部创建另一个函数。
function stealPig() {
var a = 1, b = 2
function sum () {
return a + b
}
return sum
}
var b = stealPig()
console.log(b()); //3
🌰 Two: 经典面试题之定时器与闭包,准确来说是异步与闭包。
众所周知,
JavaScript是单线程的。简单来说,单线程就是同一时间只能做一件事。那么它怎样执行异步代码呢?JS会在执行时生成一个主任务队列(先进先出),队列里的任务会按顺序一个一个执行。当遇到异步任务时,会将其相关回调放到任务队列中,当主任务队列里的任务都执行完成后,JS通过事件循环机制(EventLoop),发现任务队列中有任务等待,就取出来添加到主线程中开始执行。
for(var i = 0; i < 10; i++){
setTimeout(function () {
console.log(i) //10个10
}, 0)
}
/*如果想打印0-9,该如何?就在for循环里面创建闭包*/
for (var i = 0; i < 10; i++) {
(function (j) {
setTimeout(function () {
console.log(j)//0-9
}, 0)
})(i)
}
🌰 Three:测试题
function fun(n,o){
console.log(o)
return {
fun:function(m){
return fun(m,n)
}
}
}
var a=fun(0) //?
a.fun(0) //?
a.fun(1) //?
a.fun(2) //?
var b=fun(0).fun(1).fun(2).fun(3) //?
var c=fun(0).fun(1) //?
c.fun(2) //?
c.fun(3) //?
优、缺点:
优点:
-
- 可以读取函数内部变量
-
- 可以避免全局污染
缺点:
-
- 闭包会导致变量不会被垃圾回收机制清除,会大量消耗内存
-
- 不恰当的使用闭包可能会造成
内存泄漏
- 不恰当的使用闭包可能会造成
内存泄漏:说白了就是已经不在使用的内存,没能得到及时的释放。
箭头函数
ES6新增的一种函数,看似匿名函数的一种简写。如:()=>{}
🙋:箭头函数与普通函数的区别?
📎 One: 写法不一样。
/*普通函数*/
function stealPig(){
console.log('偷一只猪ma')
}
/*箭头函数*/
let stealCat=()=>{
console.log('偷一只猫ma')
}
📎 Two:箭头函数不能作为构造函数使用。
/*普通函数*/
function stealPig(pig){
this.pig=pig
}
const p=new stealPig(18)
console.log(p) //stealPig{pig:18}
/*箭头函数*/
let stealCat = (cat) => {
this.cat = cat
}
const c = new stealCat(18)
console.log(c); //报错,stealCat is not a constructor
📎 Three:两者的this指向不同。
普通函数的this指向:谁调用该函数就指向谁。
箭头函数的this指向:箭头函数本身没有this,箭头函数内部的this指向了其外层作用域,谁定义了函数,this就指向谁。
📎 Four: apply()、call()、bind()目的是改变this指向,箭头函数的this由函数定义时作用域决定,已经确定就无法改变。
📎 Five: 箭头函数没有自己的arguments,一种替代方案就是使用扩展运算符。
/*普通函数*/
function sum () {
console.log(arguments);
//Arguments(2) [4, 6, callee: ƒ, Symbol(Symbol.iterator): ƒ]
return arguments[0] + arguments[1];
};
sum(4, 6);
/*箭头函数*/
let sum1 = (a, b) => {
console.log(arguments); //报错,arguments is not defined
}
sum1(4, 6)
/*解决办法:使用扩展运算符*/
let sum1 = (...c) => {
console.log(c); //[4,6]
}
sum1(4, 6)
📎 Six: 箭头函数没有prototype原型。
原型与原型链
🙋:什么是原型?
//构造函数
var Imoon = function (name) {
this.name = name
}
var stealDog = new Imoon('橘橘')
console.log(stealDog.name) //橘橘
console.log(stealDog.age) //undefined
- 每个对象都有一个
_proto_属性,并且指向它的prototype原型对象。
- 任何一个函数,只要被
new了以后,就是构造函数(如Imoon),而new出来的被称为实例(如stealDog)。任何函数都可以当作构造函数。 - 函数中都有一个
prototype属性,,声明一个函数的时候就会有个prototype属性,这个属性会初始化一个空对象,也就是原型对象。 原型对象里面会有个构造器:constructor,它会默认指向声明的那个函数。
🙋:什么是原型链?
当访问一个对象的某个属性时,会先在这个对象本身属性上查找,如果没有找到,则会起它的
_proto_隐式原型上查找,即它的构造函数的prototype,如果还没找到,就会在构造函数的prototype的_proto_中查找,这样一层一层就会形成链式结构,称之为原型链。
在stealDog查找某个属性时,会执行下面的步骤: