声明:题目内容为网络资料,回答讲解为个人见解,如有错误麻烦指正
1.基础知识和语法:
1.1 数据类型
1.JavaScript的数据类型有哪些?
回答:JavaScript的数据类型有七种,Number数值型、String字符串型、undefined、Null、Boolean布尔值类型、Symbol符号型。还有一种复杂类型Object对象类型。js中所有的值都可以用着七种类型来表达。
2.JavaScript有哪些引用类型?
回答:Array、Object、Function、Date、RegExp、Symbol、Map和Set。这些都是常见的引用类型,当然还有Promise、Error这些生产中会遇到的。
3.如何判断JavaScript的数据类型?
要判断数据类型有很多方式,常见的是instanceof操作符,可以判断以恶个对象是否是某个构造函数的实例比如 arr instanceof Array,不过要注意万物皆对象,数组也是对象的一种,所以判断数组instanceof Object会返回true;
const arr = [];
arr instanceof Array; // true
const obj = {};
obj instanceof Object; // true
arr instanceof Object; // true 数组也是对象
typeof操作符,他返回一个字符串,typeof null会返回object,null会被认为是空对象;
typeof 42; // "number"
typeof 'Hello'; // "string"
typeof true; // "boolean"
typeof undefined; // "undefined"
typeof null; // "object" (注意这是一个历史遗留问题,实际上应为 "object")
typeof {}; // "object"
typeof []; // "object"
typeof function() {}; // "function"
还有就是复杂一些的Object.prototype.toString.call()方法,会返回对象内部属性[[Class]],比如
Object.prototype.toString.call(42); // "[object Number]"
Object.prototype.toString.call('Hello'); // "[object String]"
Object.prototype.toString.call(true); // "[object Boolean]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call({}); // "[object Object]"
Object.prototype.toString.call([]); // "[object Array]"
Object.prototype.toString.call(function() {}); // "[object Function]"
此外还有些特殊的Array.isArray()判断括号内的是不是数组。
还可以用===判断是否为null;还可以用一些数组方法,比如concat、splice、sort,给变量使用看看会不会报错,判断是不是数组。
4.强制类型转换、隐式类型转换分别什么是,列举出场景说明。
回答:强制类型转换指的是通过代码显式的将一个数据类型转换为另一个数据类型。而隐式类型转换就是js中的某些方法、操作符在运行时会隐藏的将数据类型转换,而不需要我们自己显式的编写代码去转换。
常见的强制类型转换代码如下:
let str = '123'
let num = Number(str) // 会转为123 如果str无法转成数组会显示NaN
let num = 123
let str = toString(num) // 会变成'123'
let value = 'adsda' // 任意非空字符串
let bool = Boolean(value) // 为true
常见隐式转换
// 数字转字符串
let num = 10
let str = "The number is" + num // +拼接字符串,会将num变成'10'字符串然后拼接在后面。
// 字符串转数字
let result = '10' * 2 // 在算术运算中 会将可以转为数字的字符串转为数字
let number = '100'
console.log(+number) // 会打印数字型的100
// == 比较运算中
let num = '10'
let num2 = 10
console.log(num1 == num2) //结果为true,字符串被隐式转换成数值类型
// 逻辑运算中
let str= 'sadasda' // 非空字符串
if(str){
console.log('true')
} // 会打印true 所有非空字符串会被隐式转换成布尔类型进行判断
5.简单介绍下symbol。
回答:Symbol是ES6新增的特性之一,它表示一个独一无二的值,每个Symbol都唯一,常用来作为对象的键来避免命名冲突和隐藏属性。不过要注意,作为键值时,它是不可枚举的。这意味着不会被for..in循环出来,也不会被Object.keys()、Object.values()、Object.entries()等方法返回。此时要想遍历出不可枚举属性,
需要用到Reflect.ownKeys()方法或者使用Object.getOwnPropertySymbols()方法
唯一性
const onlyOne = Symbol('key') // 括号内可以不填
const obj = {}
obj[onlyOne] = 'value'
console.log(obj[onlyOne]) // 'value'
不可枚举属性
const obj = {
[Symbol('a')]: 'value1',
[Symbol('b')]: 'value2'
};
for (let key in obj) {
console.log(key); // 无输出,Symbol 属性不可枚举
}
console.log(Object.keys(obj)); // []
console.log(Object.values(obj)); // []
console.log(Object.entries(obj)); // []
// 使用 Object.getOwnPropertySymbols(obj) 遍历 Symbol 属性
const symbols = Object.getOwnPropertySymbols(obj);
symbols.forEach(symbol => {
console.log(symbol); // 打印每个 Symbol 属性
console.log(obj[symbol]); // 打印对应的值
});
// 使用 Reflect.ownKeys(obj) 遍历所有属性(包括 Symbol 属性和其他属性)
const keys = Reflect.ownKeys(obj);
keys.forEach(key => {
console.log(key); // 打印每个键(包括 Symbol 属性和其他属性)
console.log(obj[key]); // 打印对应的值
});
6.js中字符串转数字的方法。
回答:可以隐式转换为数组,比如进行算数运算,或者打印时在字符串前添加+号。显示转换的话可以用parseInt()会转换成整数,该方法可以接受两个参数,第二个参数表示进制数。还可以用parseFloat()转为小数。这两个方法都会从字符串头部开始解析,直到结束或者发现非数字字符为止。还可以用Number()显示转换,这种方法要求字符串必须是完全有效的数字,不然返回NaN
7.null和undefined的区别?
回答:null表示一个空对象指针,而undefined是未定义。
let a = null
console.log(typeof(a)) // 会返回object null为空对象指针
let b
console.log(b) // 返回undefined
function add(num1,num2){
const result = num1 + num2
// 函数内没有return语句
}
function add2(num1,num2){
const result = num1 + num2
return
// return语句为空
}
add(1,2) // undefined
add2(1,2)
let obj = {
age:18
}
console.log(obj.name) //访问一个对象中没有的属性,会返回undefined
8.什么情况返回undefined?
回答:如上题,当变量定义了但是没有赋值时就访问、当函数中没有return语句或者return语句为空时就调用函数、当对象中并没有该属性而访问时,这几种情况都会返回undefined。
9.如何去除字符串首尾空格?
回答:可以使用trim方法,也可以for循环自己删除,还可以用正则表达式配合replace。
let str = ' Hello world '
let trimmedStr1 = str.trim() // 'Hello world'
let trimmedStr2 = str.replace(/^\s+|\s+$/g, ''); // "Hello, world!"
1.2 变量声明与作用域(var 、let 、 const)
1.变量提升是什么?与函数提升的区别?
回答:变量提升是js引擎会在代码执行前将变量声明提升到当前作用域顶部,但不包括赋值操作。这种情况是使用var定义的变量独有的特性,这意味着在变量赋值前访问它,得到的是undefined。而函数提升会将函数声明提升到当前作用域顶部,函数提升不包括函数表达式,只包括声明函数。所以我们可以在函数声明前调用函数。注意函数提升是优先于变量提升。
// 变量提升
console.log(a) // 不报错、而是undefined
var a = 100
console.log(a) // 100
实际代码会被js引擎解释为
var a
console.log(a) // undefined
a = 100
console.log(a) // 100
// 这是使用var声明变量独有的特性
// 函数提升
add(1,2) // 3
function add(num1,num2){
console.log(num1+num2)
}
add(4,5) // 9
//函数表达式方法不会被提升
sayHi() // TypeError: sayHi is not a function 报错
var sayHi = function(){
console.log('Hi')
}
sayHi() // 'Hi'
2.var、let、const之间的区别?什么是暂时性死区(TDZ)?
回答:var会有一个变量提升的特性,即使用var声明的变量会被提升到代码顶部,作用域为函数作用域,而不是块级作用域,也就是不会被{ }限制。并且var可以重复声明一个变量,后声明的会覆盖前面声明的。而let和const声明的变量是块级作用域,不会被提升,而且也不能重复声明一个变量。const在声明时必须赋值,并且const声明的变量不能修改,表示常量,使用时要注意。
关于暂时性死区,这是由于块级作用域的原因。访问let和const声明的变量必须在声明后才能访问,不然都会报错。
console.log(myVar); // undefined
var myVar = 10;
console.log(myLet); // ReferenceError: Cannot access 'myLet' before initialization
let myLet = 20;
console.log(myConst); // ReferenceError: Cannot access 'myConst' before initialization
const myConst = 30;
1.3 运算符(算术运算符、比较运算符、逻辑运算符)
1. 0.1+0.2为什么不等于0.3?
回答:是由于精度问题,数字在机器存储中会被转换为0101的二进制字符串,而不是所有的小数都能被准确表示出来,所以实际计算时0.1+0.2输出结果大于0.3,如果要得到0.3需要做舍入操作,也就是Fixed(),或者转成整数计算完再换成小数,或是导入第三方库得到更高精度的计算
console.log(0.1 + 0.2 == 0.3) // false
console.log(0.1 + 0.2) // 输出 0.30000000000000004
2. === 和 == 的区别
回答:===表示全等,数值相同类型相同。==只需要值相同即可,因为==会做隐式的类型转换。
3.null==undefined 输出什么?null===undefined 呢?
回答:null==undefined输出true,而null===undefined输出false,因为null是空对象指针,是对象类型,而undefiend是undefined类型。
1.4 流程控制(if语句、for循环、while)
1.5 函数定义和调用
1.创建函数的几种方式?
回答:常见的方法是函数声明、函数表达式、箭头函数、构造函数、new操作符、使用对象方法(将函数作为对象属性)
// 1.函数声明
function sayHi() {
console.log('Hi')
}
sayHi()
// 2.函数表达式
// 2.1匿名函数表达式
let fn1 = function() {
console.log('This is fn1')
}
fn1()
// 2.2具名函数表达式
let fn2 = function say() {
console.log('This is fn2')
}
fn2()
// 3.箭头函数
let fn3 = ()=>{
console.log('This is fn3')
}
// 4.new
// 4.1 构造函数配合new一个对象,然后使用对象中的方法
function Person(){
this.fn4 = function(){
console.log('This is fn4')
}
}
// 使用构造函数创建对象
let me = new Person()
me.fn4()
// 4.2 new Function创建新的函数
let fn5 = new Function("console.log('This is fn5')")
fn5() // 这种方法在浏览器中可能无法运行,因为浏览器设置了Content Security Policy(内容安全策略)限制,不允许通过new Function
// 5.使用对象方法
let person = {
fn6:function(){
console.log('This is fn6')
}
}
person.fn6()
2.定义一个名为once的函数,传入一个函数作为参数,once函数只能执行一次。
回答:
function once(fn){
let flag = false // 记录函数是否被执行过
return function(...args){
if(!flag){
flag = true //表示已使用
return fn.apply(this,args) // 使用apply改变fn的this指向,指向函数,返回结果
} else {
console.log('该函数已经执行过了,不能执行了')
}
}
}
let myFunc = function() {
console.log('Hello')
}
// 此处需要用变量保存once(myFunc)
let sayOnce = once(myFunc)
sayOnce() // 'Hello'
sayOnce() // '该函数已经执行过了,不能执行了'
// 如果直接调用函数,这种方法会执行返回的匿名函数,每次都会创建新的闭包,达不到我们要的效果。
once(myFunc)(); // 'Hello'
once(myFunc)(); // 'Hello'
1.6 对象和数组的基本操作
1.如何判断两个对象相等?如何判断空对象?
回答:要判断两个对象是不是相等,我们要先了解是浅相等还是深相等。浅相等就是引用地址相同,这样我们只需要===就可判断。要判断深相等就是具体的属性和数值要相等,有以下的方法。1.使用lodash库中的isEqual()方法,手动使用for..in来判断。又或者我们可以将对象转换为JSON字符串,然后判断字符串是否相等。
// 引入了loadsh库 使用Equal方法
const _ = require('lodash')
let obj1 = { name: 'Bob', age: 18 }
let obj2 = { name: 'Bob', age: 18 }
console.log(_.isEqual(obj1, obj2))
//手动实现深度比较 递归+for in
function myEqual(obj1,obj2) {
if(obj1 === obj2) return true // 如果地址相等就直接返回ture
if(typeof obj1 !== 'object' || typeof obj2 !== 'object') return false
const keys1 = Object.getOwnPropertyNames(obj1) // 获得对象1的键名
const keys2 = Object.getOwnPropertyNames(obj2) // 获得对象2的键名
if(keys1.length !== keys2.length) return false
for (let key of keys1){
if(!keys2.includes(key)){
return false
}
// 递归比较属性值
if(!myEqual(obj1[key],obj2[key])){
return false
}
}
return true
}
console.log(myEqual(obj1,obj2))
let obj3 = { name: 'Alice', age: 30 };
Object.defineProperty(obj1, 'id', { value: 123, enumerable: false }); // 设置不可枚举属性
let obj4 = { name: 'Alice', age: 30 };
console.log(myEqual(obj3,obj4))
2.JS中创建对象的几种方式?
回答:字面量创建,构造函数创建,Object.create()方法,工厂函数,Class方法
// 方法一字面量创建
let obj = {
a:'123',
b:'456'
}
console.log(obj)
// 方法二 构造函数
function Person(name,age) {
this.name = name;
this.age = age;
this.say = function(){
console.log('你好')
}
}
let person = new Person('Bob', 18)
console.log(person)
// 方法三 Object.create() 该方法是基于指定的原型对象创建新对象
let personPrototype = {
sayHi : function(){
console.log('Hi')
}
}
let person = Object.create(personPrototype)
person.sayHi() // Hi
person.name = 'Alice'
person.age = 30
console.log(person)
// 方法四 工厂函数
function createPerson(name,age){
return {
name:name, //可以简写为name,
age:age
}
}
let person = createPerson('Tom', 22)
console.log(person)
// 方法五 Class方法
class Person {
constructor(name,age) {
this.name = name
this.age = age
}
}
let person = new Person('Kow', 16)
console.log(person)
3.列举宿主对象、内置对象、原生对象并说明其定义?
回答:宿主对象就是由宿主环境提供的对象。在浏览器中window和document就是宿主对象,nodejs中的global、process、console、require等等。定义:宿主对象取决于宿主环境如浏览器、nodejs。
内置对象:js语言提供的对象,有String、Number、Boolean、Array、Object、Map、Set一类的定义:属于ECMAScript标准的一部分,其特性与定义在ES中。
原生对象:JS语言定义的对象,是由JS引擎提供的对象,是内置对象的一部分。有Object、Array、Fucntion等等,还有Math、JSON一类引擎提供的对象。
4.如何区分数组和对象?
回答:使用数组的判断方法Array.isArray()看返回的true还是false。用instanceof 判断是不是Array不过要注意万物皆对象,要先判断数组。使用数组独有的方法、属性 length、concat等等。还可以用一个繁琐的方法Object.prototype.toString.call()判断类型。还可以用constructor判断。
5.多维数组如何降维?
回答:最朴素的就是递归,一层一层拆开然后concat拼接,或者用展开运算符,ES提供的新方法Array.prototype.flat()
// 递归+concat
function myflat(arr){
let result = []
arr.forEach((item) => {
if(Array.isArray(item)){
result = result.concat(myflat(item))
} else {
result.push(item)
}
})
return result
}
const arr = [1,[2,3,[4,[5],[6,[7]]]]]
const newarr = myflat(arr)
console.log(newarr) //[1, 2, 3, 4, 5, 6, 7]
// 展开运算符
function myflat2(arr){
return [].concat(...arr.map(item => Array.isArray(item) ? myflat2(item) : item))
}
const arr = [1,[2,3,[4,[5],[6,[7]]]]]
const newarr = myflat2(arr)
console.log(newarr) //[1, 2, 3, 4, 5, 6, 7]
// ES2019+ 支持的新方法Array.prototype.flat()
// 卸载原型上的方法,直接数组名.flat(层数)就能得到降维后的数组
const arr = [1,[2,3,[4,[5],[6,[7]]]]]
const newarr = arr.flat(Infinity) //闯入的参数表示多少层,传无穷大就一直往里查询。
console.log(newarr) //[1, 2, 3, 4, 5, 6, 7]
6.怎么获取当前的日期(返回年-月-日 时:分:秒)?
回答:使用new Date()获得当前时间对象,然后用各种时间对象的get方法来获取年月日、时分秒。最后用字符串拼接起来。可以定义成一个函数,方便后续调用。
7.什么是类数组(伪数组),如何将其转化为真实的数组?
回答:类数组就是类似数组的结构,有索引有长度但是不具备数组的方法,比如push、pop等等的对象。常见的类数组就是函数的arguments对象、DOM元素的集合(例如:使用document.getElementsByTagName() 获取的对象)、还有NodeList等等
要想转成真实数组,有以下方法,1.Array.from()传入伪数组为参数即可返回真数组。2.扩展运算符、3.Array.prototype.slice.call()方法,传入伪数组会返回一个真数组。4.Array.prototype.concat.apply(),传入伪数组会返回一个真数组。
几种方法的本质,都是在空数组中复制伪数组的元素。
// 方法一 Array.from() 传入一个类数组
const nodeList = document.querySelectorAll('div')
const arr = Array.form(nodeList)
console.log(arr)
// 方法二 ...扩展运算符
const arr = [...arguments] // arguments是一个类数组对象 真的使用时还是推荐...args代替
console.log(arr)
// 方法三 Array.prototype.slice.call() 传入一个类数组
const nodeList = document.querySelectorAll('div')
const arr = Array.prototype.slice.call(nodeList)
console.log(arr)
// 方法四 Array.prototype.concat.apply() 要传入一个空数组、一个类数组
const nodeList = document.querySelectorAll('div')
const arr = Array.prototype.concat.apply([],nodeList)
console.log(arr)
8.如何遍历对象的属性?
回答:我们可以用for in、Object.keys()配合forEach来遍历可枚举属性。还可以用Object.getOwnPropertyNames()方法、Reflect.ownKeys()配合forEach遍历不可枚举属性
let obj = {
a:1,
b:2,
[Symbol('c')]:3
}
// 方法一 for in 不推荐用于遍历数组,因为会遍历到原型链上的属性
for(const key in obj){
console.log(`${key}:${obj[key]}`)
}
// a:1
// b:2
// 方法二 Object.keys()
Object.keys(obj).forEach(key => {
console.log(`${key}:${obj[key]}`)
})
// a:1
// b:2
// 方法三 Object.getOwnPropertyNames()
Object.getOwnPropertyNames(obj).forEach(key => {
console.log(`${key}:${obj[key]}`)
})
// a:1
// b:2
// 无法输出Symbol属性但是能输出不可枚举属性
// 方法四 Reflect.ownKeys()
Reflect.ownKeys(obj).forEach(key => {
console.log(`${key.toString()}:${obj[key]}`)
})
// a: 1
// b: 2
// Symbol(c): 3
//需要注意原型链上的属性可能会被遍历到,可以使用 hasOwnProperty() 方法来过滤掉继承的属性。
9.如何实现数组的随机排序?
回答:实现数组随机排列,普遍使用的是洗牌算法。从数组最后一位开始往前随机交换。
function shuffleArray(arr){
if(!arr || !arr.length) return
for(let i =arr.length-1;i>0;i-- ){
// 这里必须要带分号,不然浏览器会显示j未定义
const j = Math.floor(Math.random() * (i+1));
// 这种方法不用开辟新空间
[arr[i],arr[j]]=[arr[j],arr[i]]
}
return arr
}
let arr = [1,2,3,4,5,6,7,8]
let newarr = shuffleArray(arr)
console.log(newarr) // 随机排列
10.数组遍历的方法有哪些,分别有什么特点,性能如何?
回答:for循环、forEach、for of、map、filter、reduce
let arr = [1,2,3,4,5,6]
for(let i = 0; i < arr.length;i++){
console.log(`index:${i},value:${arr[i]}`)
}
// for循环是性能高且简单的方法,适用于大多数情况。尤其是大数组时。
arr.forEach((item,index) => {
// 可以对每个元素执行操作
console.log(`index:${index},value:${item}`)
})
// forEach性能中等,适合一次性遍历数组执行操作。
for(const item of arr){
console.log(item)
}
// for of性能较好,适用于比哪里数组中的值而不需要索引。
const newArr = arr.map((item,index) =>{
// 返回新数组,对每个元素执行映射操作
return item+1
})
console.log(newArr) // [2,3,4,5,6,7]
// 性能中等,适合对数组每个元素做转换的情况。
const filterArr = arr.filter((item,index) => {
if(item>=3)
return item
})
console.log(filterArr) // [3,4,5,6]
// 性能中等,适用于筛选情况
const result = arr.reduce((prev,current) => {
// prev是下一个值、current是初始值
return prev+current //传到下一个prev
},0) //最后一个参数位初始值,默认0
console.log(result) //21
// 性能中等,常用来求和,当然还有其他操作。通过累积操作将数组简化为单个值
11.对象深拷贝的简单实现。
回答:实现深拷贝,都是使用递归。也可以用lodash库中的_.cloneDeep()方法
function deepClone(obj){
if(obj === null || typeof obj !== 'object'){
return obj
}
const clone = Array.isArray(obj) ? [] :{};
for(let key in obj){
if(obj.hasOwnProperty(key)){
clone[key] = deepClone(obj[key])
}
}
return clone
}
// 该方法无法处理循环引用
// 通过深拷贝后的对象,不会影响原始对象。
// 这种方法只是简单实现,对普通对象有用。
12.实现JS中所有对象的深度克隆。(包装对象,Date对象,正则对象)
回答:这些对象中的属性值类型各不相同,需要更进一步判断类型。
function deepClone(obj,hash=new WeakMap()){
if(typeof obj !== 'object' || obj===null){
return obj
}
//如果已经克隆,则直接返回
if(hash.has(obj)) return hash.get(obj)
let clone
if (obj instanceof String || obj instanceof Number || obj instanceof Boolean) {
clone = new obj.constructor(obj.valueOf());
}else if(obj instanceof Date){
clone=new Date(obj)
}else if(obj instanceof RegExp){
clone =new RegExp(obj.source,obj.flags)
}else{
clone =Object.create(Object.getPrototypeOf(obj))
}
//将当前对象设置为已克隆
hash.set(obj,clone)
for(let key in obj){
if(Object.prototype.hasOwnProperty.call(obj,key)){
clone[key]=deepClone(obj[key],hash)
}
}
return clone
}
// 正则对象测试样例
const regex = /ab+c/i;
const clonedRegex = deepClone(regex);
console.log(clonedRegex); // 输出:/ab+c/i
// 日期对象测试样例
const date = new Date('2022-04-25T12:00:00');
const clonedDate = deepClone(date);
console.log(clonedDate); // 输出:2022-04-25T12:00:00.000Z
// 包装对象测试样例
const wrappedString = new String('Hello');
const clonedWrappedString = deepClone(wrappedString);
console.log(clonedWrappedString); // 输出:[String: 'Hello']
13.数组常用的方法
回答:push、pop、shift、unshift、splice这些操作数组本身的方法,map、filter、reduce、forEach、find、every、some、findIndex等遍历方法、toString、toLocalString、valueOf等转换方法、concat、reserve、sort、slice等数组变换方法、join、indexOf、includes、lastIndexOf等获取数组信息方法。
14.数组去重的方法有哪些?
回答:可以通过新的数据结构Set,其具有唯一性,再转为数组。使用数组filter方法,配合第一次出现下标indexOf。使用reduce方法,配合include。使用Map对象,其具有唯一性。用双循环。
const arr = [1,2,3,4,1,2,4]
// Set配合展开运算符。
const newarr1 = [...new Set(arr)]
// filter筛选第一次出现的元素
const newarr2 = arr.filter((item,index) => arr.indexOf(item) === index)
// reduce方法
const newarr3 = arr.reduce((prev,current) => {
if(!prev.includes(current)) {
prev.push(current)
}
return prev
} , [])
// Map对象和map方法
const newarr4 = Array.from(new Map(arr.map(item=>[item,item])).values())
// 双重循环for+indexof
const newarr5=[]
for(let i = 0;i<arr.length;i++){
if(newarr5.indexOf(arr[i]) === -1){
newarr5.push(arr[i])
}
}
console.log(newarr1) // [1,2,3,4]
console.log(newarr2) // [1,2,3,4]
console.log(newarr3) // [1,2,3,4]
console.log(newarr4) // [1,2,3,4]
console.log(newarr5) // [1,2,3,4]
15.如何获取对象的属性?
回答:一般情况下,我们可以通过.属性名访问对象属性,也可以用[属性名]访问,当要获得对象所有属性名时可以用Object.keys() 、for..in 、访问不可枚举属性时Object.getOwnPropertyNames()、Object,getOwnPropertySymbols() 、Reflect.ownKeys() 、Object.getOwnPropertyDescriptors() 、还有遍历方法遍历原型链。
16.JS对象类型,基本对象类型和引用对象类型的区别?
回答:js的对象类型可以分为基本对象类型、引用对象类型两种,基本数字类型是存放再栈中的简单数据,undefined,string,symbol,boolean,null,number,引用类型是存在堆中的对象, object,Array,RegExp,Date,Function。区别就是基本数据类型存在栈,具有不可变性,修改他们会返回新的值。引用类型存在堆,变量存储的是对象的地址。对象可以包含多个属性,属性可以是其他类型,可以通过地址访问,对象的值能修改但是引用地址不变。
总结来说:基本对象类型存在栈,按值访问,不可变性强。引用类型存在堆,变量存的是地址,需要通过地址访问,可以动态修改对象的值和属性
17.{}和[]的valueOf和toString的结果是什么?
回答:{}的valueOf是{} ,toString结果是"[object Object]"
[]的valueOf结果是[] , toString结果是''"
2.高级概念
2.1 闭包
1.什么是JS闭包?
回答:JS闭包是一种内部函数访问外部函数作用域的变量,并且即使外部函数结束了,依旧持有对这些变量的引用,依旧保存在内存中。关于闭包的使用情况,模块化、私有变量、延迟执行、事件处理程序等等,都需要用到闭包,它是个强大的功能,但是过度使用会导致内存泄漏。
2.实现一个私有变量,用get可以访问,不能直接访问。
function onlyGet(){
let count = 0;
return {
set: function(num){
count = num
},
get: function(){
return count
}
}
}
const counter = onlyGet()
counter.set(5)
console.log(counter.get())
2.2 原型与继承
1.Function._ _proto_ _(getPrototypeOf)是什么?指向谁?
回答:指向Function.prototype的引用,对象的原型指向创建该对象的构造函数的原型对象。
2.谈一谈JS的原型链,原型链的顶端是什么?Object的原型什么是?Object原型的原型是什么?
回答:关于对象和原型,JS中所有的对象都是可以通过构造函数创建。每个JS对象都有个内部属性[[Prototype]] 可以通过_ _proto_ _访问到,他只想这个对象的原型。关于原型对象,函数对象有一个prototype属性,指向一个对象,称为该函数的原型对象。对象实例通过原型链继承其构造函数的原型对象的属性和方法。关于原型链,访问对象的属性和方法时,Js引擎会沿着对象的原型链向上查找,直到查到对印的属性或方法,或者到达原型链末端(Object.prototype)。
关于原型链的顶端是Object.prototype。 Object的原型是Object构造函数的原型对象。
Object原型的原型是null
// 定义一个构造函数
function Person(name){
this.name=name
}
// 创建一个实例
let person = new Person('Tom')
//person 实例对象的原型指向构造函数的原型对象 Person.prototype
console.log(person.__proto__ === Person.prototype) // true
// Person.prototype的原型指向Object.prototype
console.log(Person.prototype.__proto__ === Object.prototype) //true
// Object.prototype 的原型是null
console.log(Object.prototype.__proto__ === null) //true
3.谈谈Promise+Generator+Async的使用。
回答:关于Promise ,它是一种用于表示异步操作最终完成或失败的对象,可以解决回调地狱。
let myPromise = new Promise((resolve,reject) => {
setTimeout(() => {
resolve('异步操作成功')
// reject('异步操作失败')
}, 1000)
})
myPromise.then((result) => {
console.log(result) // 异步操作成功时执行
}).catch((error) => {
console.log(error) // 异步操作失败时执行
})
关于 Generator函数,它是通过函数内部的yield关键字实现暂停和恢复执行的功能。
function* myGenerator() {
yield 1;
yield 2;
yield 3;
}
let gen = myGenerator()
console.log(gen.next().value) // 1
console.log(gen.next().value) // 2
console.log(gen.next().value) // 3
关于Async/Await 它是Promise的语法糖,让异步代码写起来更像同步,利于理解。
async function fetchData() {
try {
let result = await fetch('https://api.example.com/data') //异步请求
let data = await result.json() //
console.log(data)
} catch (error){
console.error(error) // 打印错误信息
}
}
fetchData()
关于结合使用,Generator和Promise可以实现复杂的异步逻辑,例如按顺序执行多个异步操作或处理多个并发的异步请求
//Promise和Generator结合
function* fetchData() {
try {
let result1 = yield fetch('https://api.example.com/data1');
let data1 = yield result1.json();
let result2 = yield fetch('https://api.example.com/data2');
let data2 = yield result2.json();
console.log(data1, data2);
} catch (error) {
console.error('Error fetching data:', error);
}
}
function runGenerator(generator) {
let gen = generator();
function handleNext(value) {
let next = gen.next(value);
if (!next.done) {
next.value.then(
(result) => handleNext(result),
(error) => gen.throw(error)
);
}
}
handleNext();
}
runGenerator(fetchData);
//Async/Await 更加简洁
async function fetchData() {
try {
let result1 = await fetch('https://api.example.com/data1');
let data1 = await result1.json();
let result2 = await fetch('https://api.example.com/data2');
let data2 = await result2.json();
console.log(data1, data2);
} catch (error) {
console.error('Error fetching data:', error);
}
}
fetchData();
2.3 执行上下文与作用域链
1.什么是作用域链?如何延长?
回答:作用域链其实是js中用于查询变量和函数的查找机制。当代码在一个作用域中执行时,会创建一个作用域链,用于确定在当前作用域中如何访问变量和函数。
要延长作用域链有以下方法:1.函数的创建和调用、cha2.用let、count声明变量长生块级作用域、3.with语句 (不常用)。
关于其查询过程是一种自底向上的过程,从当前作用域开始,逐级向上查找,直到找到目标标识符或到达全局作用域。
2.4 异步编程(promise、回调函数、async/await)
1.setTimeout和setInterval的区别以及用法是什么?
回答:setTimeout会在延迟一段时间后执行一次指定的函数或一段代码,setInterval则是在指定的时间间隔内重复执行指定函数或一段代码。
let timer1 = setTimeout(function() {
console.log('两秒后执行,只执行一次')
}, 2000)
clearTimeout(timer1)
let timer2 = setInterval(function() {
console.log('每隔两秒执行一次')
},2000)
clearInterval(timer2)
// 要注意使用setInterval时,如果执行时间较长或存在异步操作会导致任务堆积。
// 使用定时器要记得取消清除,避免内存泄露或资源浪费。
2.如何用setTimeout来实现setInterval?
let timeId // 需要定义一个全局id来控制这个定时器
function mySetInterval(func,delay){
function interval(){
func()
timeId = setTimeout(interval,delay) // 递归调用setTimout实现间隔执行
}
interval()
}
mySetInterval(()=>{
console.log('模拟')
},1000)
//关闭
clearTimeout(timeId)
3.如何解决异步回调地狱问题?
回答:Promise的诞生很好的解决了这个问题,通过链式调用很好的避免了回调地狱。后续的Async,Await产生,让代码更加优雅,清晰。
4.异步加载JS的方法。
回答:要想异步加载JS其实有很多方法,比如引入时带上defer或者async属性。或者在js中动态创建script标签,配合async、await动态加载也可以。
5.如何实现sleep效果?
回答:使用定时器和Promise对象可以实现,使用Date对象获取时间戳,再搭配while循环也能实现
function mysleep(ms){
let start = Date.now()
while(Date.now() - start<ms);
}
console.log('开始')
mysleep(1000) // 暂停一秒
console.log('结束')
function newSleep(ms){
return new Promise(resolve => setTimeout(resolve,ms))
}
console.log('开始')
newSleep(2000).then(() => {
console.log('结束')
})
6.介绍一下Promise,及其底层如何实现?
回答:Promise是JavaScript中处理异步操作的一种机制,能够比避免回调地狱的问题。Promise的状态有三种Pending进行中,Fulfilled已成功、Rejected已失败。状态一旦发生改变就不会再变化。Promise有链式调用,通过.then进行链式i调用,依次处理异步操作的结果或者错误。还可以使用catch方法或.then的第二个参数捕获错误。
Promise 的底层实现:
Promise 的底层实现涉及状态管理、异步任务队列和回调函数管理等机制:
-
状态管理: Promise 内部通过状态(Pending、Fulfilled、Rejected)来管理异步操作的状态变化,并使用
resolve
和reject
函数来改变状态。 -
异步任务队列: Promise 内部维护了两个队列,分别用于存储成功态(Fulfilled)和失败态(Rejected)的回调函数。当 Promise 的状态改变时,会依次执行对应状态的回调函数。
-
链式调用和异步处理: Promise 的
.then()
方法返回一个新的 Promise,使得可以通过链式调用来处理异步操作的结果。同时,Promise 的设计可以很好地处理嵌套的异步操作,避免了回调地狱。 -
错误处理: Promise 使用
.catch()
方法来捕获链中任何位置发生的错误,使得错误处理更加方便和统一。
7.async和await该如何使用?
回答:async和await通常都是搭配一起使用,让异步代码写起来像同步代码。async函数会返回一个Promise对象,如何在async内部的函数执行完毕后,await关键字等待Promise对象的状态变化
8.Promise和async/await的关系。
回答:async/await属于Promise的语法糖,使用起来更加简洁。
9.JS加载过程阻塞,解决方法有哪些?
回答:可以异步加载JS(defer、async属性加在script上)、动态加载脚本(js中创建script标签),或者将js代码拆分,用webpack进行代码拆分按需加载动态导入。或者在link标签中预加载重要的资源、或者使用WebWorkers将复杂代码放在后台线程运行,避免阻塞主线程。
2.5 事件循环
1.解释一下js的事件循环机制。
回答:JS事件循环机制,主要是要了解主线程、微任务队列、宏任务队列、同步任务、异步任务。JS是单线程的,所以在同步任务中,是按照顺序执行的。遇到异步任务时会区分宏任务和微任务,然后按照顺序放在微任务队列和宏任务队列。在同步任务执行完后,将微任务队列的任务依次放在主线程执行,然后再将宏任务队列的任务放在主线程执行。然后重复执行微任务队列、宏任务队列中的任务直到结束,这就是事件循环机制。关于异步任务的分类,Promise、process.nextTick是微任务,而.then、setTimeout、setInterval、ajax请求、回调函数、事件绑定、nodejs中fs的文件IO操作都属于宏任务。
3.DOM操作
3.1 DOM结构和节点操作
1.DOM节点的Attribute和Property有何区别?
回答:Attribute是指标签上的属性与属性值和Property是Attribute对应Dom节点的对象属性。当属性值不属于属性支持的范围内,Attribute不会修正而Property会自动修正。并且Attribute能自定义,而Property不能。
2.DOM结构操作怎样添加、移除、移动、复制、创建和查找节点?
回答:通过createElement创建新元素、appendChild()添加到对应位置实现节点添加。先查询找到节点然后removeChild()可以实现移除节点、移除加添加就可以实现移动,复制的话有cloneNode()方法,传入参数true实现深度复制、默认false。查询节点有许多方法我常用的是document.querySelector('css选择器')可以查找对应节点。
3.如何获取元素的位置?
回答:先查询获得节点,然后配合ClientHeight、ClientTop获得元素内容区域的高度,不包括边框、外边距和滚动条的宽度。或者scrollTop、scrollHeight表示元素的内容高度,包括由于溢出而未显示的部分,不包括边框、外边距和内边距。还有offsetHeight、offsetTop表示元素在页面中占据的高度,包括内容区域、内边距(padding)、边框(border),但不包括外边距(margin)和滚动条。
4.document.write和innerHTML的区别?
回答:write会导致回流、重绘,他会直接修改html文档。innerHTML是往元素添加HTML内容,不会影响文档解析。实际操作中更建议使用innerHTML
3.2 事件处理(事件绑定、事件冒泡和捕获)
1.如何给一个按钮绑定两个onclick事件?
回答:使用最新的事件监听器。先查找按钮元素,然后可以多次绑定相同类型事件。
<button class="mybutton">点击</button>
<script>
const mybutton = document.quertSelector('.mybutton') // 查询节点
mybutton.addEventListener('click',()=>{
console.log('绑定了事件一')
})
mybutton.addEventListener('click',()=>{
console.log('绑定了事件二')
})
</script>
2.什么是事件冒泡?它是如何工作的?如何阻止事件冒泡?
回答:事件冒泡就是触发DOM元素上特定事件后,该事件沿着DOM树从最深的节点向父级节点传播的过程。如果一个父盒子套着子盒子,触发子盒子点击事件的同时会冒泡到父盒子上并且触发父盒子的点击事件。阻止冒泡一般就是在事件定义时,执行函数传入event参数,然后设置event.stopPropagation()就能阻止事件冒泡。
3.什么是事件捕获?它是如何工作的?
回答:事件捕获和事件冒泡相对应,是另一个阶段。也就是事件捕获阶段,事件会从顶层节点向下传播,逐级触发每个祖先节点上注册的相同类型事件处理函数,直到触发事件的目标节点为止。也就是点击子盒子,触发子盒子的点击事件,但是执行顺序是先执行父盒子的点击事件,再执行子盒子(目标)的点击事件。要想设置捕获,需要在事件绑定时,传入除了事件名、处理函数外的第三个参数,设置为true,则打开捕获。
4.如何让一个事件先冒泡再捕获?
回答:真不会,要不就设置两个相同效果事件,第一个设置为false不捕获,第二个设置true捕获。这种违背常理的还是不推荐去这么做。
5.聊一下DOM的事件模型?
回答:DOM三种事件模型、标准事件模型(事件捕获、事件处理、事件冒泡)同一个元素可以绑定同一种事件的多个监听。
6.事件三要素是什么?
回答:事件三要素就是事件类型(怎么触发)、事件目标(是谁)、处理函数(怎么做)。这三要素构成了事件。
7.如何绑定事件?如何解除事件?
回答:获取到事件元素后通过addEventListener()绑定事件,通过removeEventListener()移除事件
8.对于事件委托的理解?
回答:事件委托是一种很常见的事件处理方式,我们通过在父元素上绑定事件取代在一个个子节点上绑定重复的事件,让代码更加简洁。事件委托本身利用到了冒泡的原理,当一个事件发生在子元素上时,事件不会直接在子元素上触发,而是在其父元素(或祖先元素)上触发,然后通过冒泡阶段传播到父元素,最终在父元素上被捕获并处理。
9.元素拖动如何实现,原理是什么?
回答:要实现元素拖动,本质就是通过鼠标事件获取实时的位置,然后设置元素绝对定位的top和lfet值。主要用到mousedown开始拖动,设置个正在拖动的状态。mousemove鼠标移动事件实时获取鼠标位置ClientX和ClientY减去拖拽元素的offsetLeft和OffsetTop获取移动距离。然后通过mouseup事件释放,结束拖动。
// 假设已经有一个类名为moveDiv的盒子并设置列绝对定位。
const mybox = document.querySelector('.moveDiv')
let moving = false // 表示是否被拖拽
let xBuffer = 0 // 鼠标点击的位置与盒子左方的位置差
let yBuffer = 0 // 鼠标点击的位置与盒子上方的位置差
document.addEventListener('mousedown',(e)=>{
if(moving && e.target.className !== moveDiv) {return}
moving = true
const {offsetLeft,offsetTop} = box // 获取盒子的位置
const {clientX,clientY} = e // 获取鼠标的位置
xBuffer = clientX - offsetLeft
yBuffer = clientY - offsetTop
})
document.addEventListener('mouseover',(e)=>{
if(!moving) {return}
mybox.style.left = (e.clientX - xBuffer) + 'px'
mybox.style.top = (e.clientY - yBuffer) + 'px'
})
document.addEventListener('mouseup',(e)=>{
if(!moving) {return}
moving = false
})
10.mouseover和mouseenter的区别?
回答:mouseover有冒泡而mouseenter没有冒泡。如果鼠标指针从子元素移动到父元素会触发父元素mouseover事件,但是mouseenter不会。mouseover 是一个冒泡事件,会在鼠标进入元素或其子元素时触发,而mouseenter 则只会在鼠标首次进入元素边界时触发,不会冒泡到父元素。
11.谈一谈节流和防抖。
回答:节流是短时间多次触发,只会执行一次函数,并且只有在执行完后才能再次触发执行。而节流则是短时间内反复触发,它只会执行最后一次触发。节流常用在滚动事件、resize事件、输入框搜索等等情况,防抖常用在输入框输入事件、按键点击事件等需要用户停止操作再执行的场景。这两种方法都能有效地减少不必要的函数触发,提升用户体验与页面性能。常将节流比作游戏中放技能需要等cd、防抖看作回城必须要站着不动才能回城。
12.三种事件模型指的是什么?
回答:三种事件模型分别是原始事件模型(每个事件只能绑定一个处理啊函数,不能移除)、IE事件模型(只有冒泡和事件处理)、标准事件模型(DOM事件模型,有捕获冒泡和处理、并且能绑定多个处理函数,能移除事件)现在都是标准事件模型。
3.3 AJAX和Fetch API
1.Ajax解决浏览器缓存问题。
回答:关于Ajax解决浏览器缓存问题,浏览器缓存问题就是当我们发送请求时,浏览器会缓存先前的响应数据,导致无法获得最新的数据。这可能会影响到应用程序的正确性和实时性。有以下几种解决方法:1.添加一个随机参数或者时间戳到ajax请求中,放置浏览器缓存此次请求。2.设置请求头并禁用缓存,3.使用post请求,post请求不会被缓存。4.设置响应头,在服务端设置响应头指示浏览器不要缓存响应数据。5.使用缓存控制策略,设置一个缓存过期时间。
2.将原生的ajax封装成Promise。
// 封装ajax,实现promise异步请求
function myajax(url,method,data){
return new Promise(function(resolve,reject){
let xhr = new XMLHttpRequest()
xhr.open(method,url)
xhr.setRequestHeader('Content-Type','application/json')
xhr.onload = function(){
if(xhr.status >= 200 && xhr.status <= 300){
resolve(xhr.response)
} else {
reject({
status : xhr.status,
statusText : xhr.statusText
})
}
}
xhr.onerror = function(){
reject({
status : xhr.status
statusText : xhr.statusText
})
}
xhr.send(JSON.stringify(data))
})
}
// 使用例子
myajax('https://api.example.com/data', 'get').then(function(response){
console.log('请求成功', response)
}).catch(function(error){
console.error('请求失败', error)
})
4. ES6+新特性
4.1 箭头函数
1.箭头函数中this指向。
回答:箭头函数中的this指向外层作用域(父级),如果当前父级也是箭头函数,就再向外直到找到为止,如果是在全局作用域中定义的箭头函数,其this指向全局对象(浏览器环境的window)
4.2 模板字符串
4.3 解构赋值
4.4 新的数据类型(Map、Set)
4.5 类与模块化
1.Class、extends是什么,有什么作用?
回答:Class和extends是ES新增的特性,class用来定义类,是构造函数的语法糖,可以更清晰简洁的创建对象。extends用来继承父类的所有属性和方法并创建字类。
// class
class Person{
// 类中的构造函数
constructor(name,age){
this.name = name
this.age = age
}
sayHello() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`)
}
}
const person = new Person('Tom',30)
person.sayHello()
// extends
class Student extends Person {
constructor(name,age,grade){
super(name,age) // 通过super方法调用父类的构造函数
this.grade = grade
}
sayHello(){
super.sayHello() // 调用父类的sayHello方法 也可以直接重写
console.log(`I am in grade ${this.grade}.`)
}
}
const student = new Student('Alice',15,9)
student.sayHello()
2.说一下类的创建和继承,列举一下知道的继承方法。
回答:类的创建是为了更加简单清晰的创建出具有相同特征的对象,继承方法可以在一个类的基础上继承并衍生多种相似的类以便更准确的创建对象。关于继承的方法有一下几种1.原型链继承、2.构早函数继承、3.组合继承(原型链+构造函数)、4.原型式继承、5.寄生式继承、6.寄生组合式继承。关于extends关键字,本质上也是使用寄生组合式继承。
// 原型链继承方法
// 缺点:子类的两个实例会相互影响 原因:两个实例用的同一个原型对象,其空间是共享的
function Person(name,age){
this.name = name
this.age = age
this.play = [1,2,3]
}
function Student(){
this.type = 'student'
}
Student.prototype = new Person()
console.log(new Student('Tom',18))
let s1 = new Student('Tom',18)
let s2 = new Student('Bob',18)
s1.play.push(4) // 两个的play都变成[1,2,3,4]
console.log(s1,s2)
// 构造函数继承
// 缺点:父类的原型对象中的方法无法继承给子类
function Parent1(){
this.anme = 'parent1'
}
Parent1.protytype.getName=function(){
return this.name
}
function Child1(){
Parent1.call(this)
this.type = 'Child'
}
let child = new Child1()
console.log(child.getName) // 父类的原型对象中的方法无法继承给子类
// 组合继承 结合两种方法
// 缺点:每有一个字类实例,父类会多构造一次
function Parent3(){
this.name = 'parent3'
this.play = [1,2,3]
}
Parent3.prototype.getName=function(){
return this.name
}
function Child3(){
// 第二次调用Parent3()
Parent3.call(this)
this.type = 'child3'
}
// 第一次调用Parent3()
Child3.prototype = new Parent3()
// 需要手动调整构造器指向自身构造函数
Child3.prototype.constructor = Child3
let s3 = new Child3()
let s4 = new Child3()
s3.play.push(4)
console.log(s3.play,s4.play) // 不会相互影响
console.log(s3.getName()) // 可以使用父类原型上的方法
console.log(s4.getName()) // 可以使用父类原型上的方法
上述方法都是围绕着构造函数,下面则是对于js普通对象的继承
// 原型式继承 Object.create() 两个参数,
// 第一个作为新对象的原型对象,第二个是新对象定义额外属性的对象
// 缺点:多个实例的引用类型属性会指向相同的内存,存在篡改的可能
let parent4 = {
name:'parent4',
friends:['p1','p2','p3'],
getName:function(){
return this.name
}
}
let person4 = Object.create(parent4)
person4.name= 'Tom'
person4.friends.push('BOB')
let person5 = Object.create(parent4)
person5.friends.push('Lucy')
console.log(person4.name === parent4.getName())
console.log(person5.name)
console.log(person4.friends) // 多个实例的引用类型指向同一地址
console.log(person5.friends) // 多个实例的引用类型指向同一地址
// 寄生式继承
// 在原型式继承的基础上,增加一些方法 缺点与原型式继承一样
let parent5 = {
name:'parent5',
friends:['p1','p2','p3'],
getName:function(){
return this.name
}
}
function clone(original){
let clone = Object.create(original)
clone.getFriends = function(){
return this.friends
}
return clone
}
let person6 = clone(parent5)
console.log(person6.getName())
console.log(person6.getFriends())
接下来最重要的寄生组合式继承方法
function clone (parent,child){
// 此处使用Object.create(parent.prototype)可以减少组合继承中多进行一次构造的过程
child.prototype = Object.create(parent.prototype)
child.prototype.constructor = child
}
function Parent6(){
this.name = 'parent6'
this.play = [1,2,3]
}
Parent6.prototype.getName = function(){
return this.name
}
function Child6(){
Parent6.call(this)
this.friends = 'child5'
}
clone(Parent6,Child6)
Child6.prototype.getFriends = function(){
return this.friends
}
let person6 = new Child6()
console.log(person6)
console.log(person6.getName())
console.log(person6.getFriends())
5.性能优化与调试
5.1 代码优化技巧(减少重绘和回流)
1.什么是重绘(repaint)?什么是回流(reflow)?如何最小化重绘和回流?
回答:重绘是指渲染树中的一些元素需要更新样式,但这些样式属性只修改元素的外观,并不影响布局。而回流更严重,回流是渲染树中元素的规模尺寸,布局,隐藏要改变而导致重构,则产生回流。回流的代价大于重绘。要想最小化重绘和回流,1.需要尽量避免修改dom样式和内容,可以将多次操作合并在一次(要改变样式时,可以将多个要修改的放在一个css类中,通过添加类来实现)。2.减少使用table布局(table布局一点改动就会导致重新布局)、3.使用文档碎片,一次性将所有元素添加在文档中,而不是逐个增加。4.让DOM脱离文档流(多使用absolute、fixed)等方法
2.延迟加载的方法有哪些?
回答:延迟加载常用在图片、js文件、或者css。延迟加载图片有懒加载:将图片的src先设置占位符,当图片进入用户视野(检查视口坐标),然后通过js再给图片真的src属性。还有无限滚动加载,等到用户滚动到页面底部,在进行下一步请求或加载实现动态加载。关于js文件的延迟加载,可以用js控制增加script标签、设置script标签defer和async属性异步加载,或者link标签先预加载。而CSS也是使用link标签或者js来控制延迟加载。
3.说一下图片的懒加载和预加载?
回答:图片的懒加载,将图片的src先设置占位符,当图片进入用户视野(检查视口坐标),然后通过js再给图片真的src属性。还有无限滚动加载,等到用户滚动到页面底部,在进行下一步请求或加载实现动态加载。使用link标签预加载。
4.怎么控制一次加载一张图片,加载完后在加载下一张?
回答:通过js将图片地址存在数组中,然后async异步按顺序加载图片(新建IMG对象配合onload)。或许人为设置一个定时器,等待一段时间后再发起请求。
5.2 内存管理和垃圾回收
1.垃圾回收机制有哪些?具体怎么执行的?
回答:垃圾回收机制有两个,标记清除法、引用记数。标记清除法:从对象开始递归遍历,标记活动对象,然后垃圾回收机制会清除所有没被标记的对象。清除所有标记,等待下次回收。还有引用计数法:计算一个对象将引用的次数。每被调用一次,计数就加一,引用结束后就减一,直到为0就清除。但是如果俩个对象互相引用,将无法清除,导致内存无法释放。导致内存泄露
2.什么是内存泄漏?
回答:内存泄漏就是指程序申请了一块内存空间,使用完毕后还没有释放。导致这块内存既不能被再次使用,又不能重新分配。导致内存泄漏的情况是1.闭包、2.定时器没清理、3.dom引用没清除,移除dom中节点后,没有删除事件监听或全局变量。4.两个对象或多个对象互相引用东芝无法回收、5.一个对象被引用,但是不被需要的时候要手动释放为null。
5.3 前端性能监测与调试工具的使用
1.平时如何调试JS?
回答:编辑器中打断点、console打印要检查的值、debugger关键字、浏览器自带的工具(开发者模式检查代码)
6.其他
1.JavaScript动画和CSS3动画有什么区别?
回答:JS动画可以控制的更加精细,动画效果也更加丰富且兼容性更好,缺点就是js加载动画可能出现阻塞导致丢帧、并且代码复杂度高于CSS。CSS3使用动画会更加便捷、代码相对简单对于帧数表现不好的浏览器,css3动画能降级。缺点:控制性较弱,只能停止而是不卡住一个点停下,并且要实现复杂动画其实代码比JS更加复杂。
2.ES5和ES6的区别,ES6新增了什么?
回答:ES6新增了箭头函数、模板字符串、Promise对象处理异步、let、const声明关键字,解构赋值、模块化语法export、import、展开运算符...、数组和字符串加了新方法includes()、startWith()等等、还允许函数定义中指定传入参数的默认值、还引入了类和继承的概念。
3.new操作符做了哪些事?
回答:new操作符其实做了四件事1.创建一个空对象、2.将空对象的对象原型指向构造函数的原型对象、3.改变this指向、4.对构造函数进行判断处理。
function mynew(fn, ...args){
let obj = {}
obj.__proto__ = fn.prototype
let res = fn.apply(obj,args)
return res instanceof Object ? res : obj
}
function Person(age,name){
this.age = age
this.name = name
}
console.log(mynew(Person,18,'张三')) // Person {age: 18, name: '张三'}
console.log(new Person(18,'李四')) // Person {age: 18, name: '李四'}
4.改变函数内部this指针的指向函数(bind、apply、call的区别)内在分别是如何实现的?
回答:区别:apply需要传入的第二个参数是数组、而bind和call传参数就行。bind不会执行函数,而call和apply会自动执行一次,bind还可以在改变执行后传入更多参数。其实内部的思路是一致的,通过创建一个新函数,新函数调用的时候将绑定的对象作为this的值,然后传参数执行(bind不执行)。
// 手写call、apply、bind
Function.prototype.myCall = function(thisArg, ...args) {
const key = Symbol('key')
thisArg[key] = this
const res = thisArg[key](..args)
delete thisArg[key]
return res
}
Function.prototype.myApply = function(thisArg,args){
const key = Symbol('key')
thisArg[key] = this
const res = thisArg[key](args)
delete thisArg[key]
return res
}
Function.prototype.myBind = function(thisArg,...args){
return (...newargs) => {
const key = Symbol('key')
thisArg[key] = this
return thisArg[key](..args,...newargs)
}
}
5.JS的各个位置,比如clientHeight、scrollHeight、offsetHeight,以及clientTop、scrollTop,offsetTop的区别。
回答:client的属性都是只读的,语法是element.clientHeight。Width\top\left同理。clientWidth和Height只包含padding和内容部分,且内联元素的值为0。clientLeft表示元素左边框的宽度(内容溢出时滚动条也算),不包含外边距和内边距。clientTop表示元素顶部边框宽度,不包含内外边距。而offset用法与client属性一样,offsetWidth包含width,border,padding以及竖直方向滚动条、Height包含height,border,padding以及水平方向滚动条高度。而offsetLeft是元素左上角相对父级左边界的偏移,offsetTop则是相对父元素顶部内边距的距离。元素隐藏时返回0。scroll使用语法也一样,它是可读可设置的,scrollLeft指元素内容水平的像素数(滚动条到元素左边的距离,滚动条会位于最右侧(内容开始处),并且scrollLeft值为 0。此时,当你从右到左拖动滚动条时,scrollLeft 会从 0 变为负数。)、scrollTop(一个元素的内容垂直滚动的像素数,当一个元素的内容没有产生垂直方向的滚动条,那么它的 scrollTop 值为0)、scrollWidth只读,(是一个元素内容宽度的度量,包括由于溢出导致的视图不可见内容。),scrollHeight只读,(是一个元素内容高度的度量,包括由于溢出导致的视图不可见内容。)
6.eval是做什么的?
回答:eval是很强大的功能,它可以将接受的字符串解析成js代码并执行,然后返回执行结果。并且它的作用域实在它所在的范围内生效,如果是window.eval()则会设置在全局作用域。虽然强大但是很消耗性能,不推荐使用。
7.JS中实现监听对象属性的改变。
回答:ES6之前使用的defineProperty,这个方法有明显的缺陷,只能监听单个属性的变化,而且需要两个方法get\set分别监听修改和赋值。ES6之后使用Proxy代理,这个方法更加简单有效,一个方法就能监听对象中所有的属性的修改、读写。
// ES5 defineProperty
let obj = {
name: 'John',
age: 30
}
Object.defineProperty(obj,'name',{
get:function(){
// 此处使用_.name是为了避免get和set方法内部直接访问name属性,避免死循环
console.log('获取属性name的值')
return obj._name
},
set:function(value){
console.log(`设置属性name的值为${value}`)
return obj._name = value
}
})
obj.name = 'haha'
console.log(obj.name)
obj.age = 22
console.log(obj.age)
// ES6 proxy
let target = {
name: 'jone',
age: 30
}
let handler = {
set: function(target,key,value){
console.log(`设置属性 ${key} 的值为 ${value}`)
target[key]=value
return true
},
get: function(target,key){
console.log(`获取属性${key}`)
return target[key]
}
}
let proxy = new Proxy(target,handler)
proxy.name = 'Tom'
proxy.age = 88
console.log(proxy.name)
8.谈一谈所知道的JS语言特性。
回答:1.JS是一个动态类型语言,变量的的类型是在运行时赋值决定,并且可以随时修改。2.弱类型,变量声明时不用声明类型,类似由引擎根据上下文决定。3.原型继承,js使用原型继承来实现对象之间的继承关系,每个对象都有一个原型对象,可以继承原型对象的属性和方法。4.函数方面,js中的函数可以被传递和赋值,可以作为参数传递给其他函数,也可以作为函数的返回值。5.js支持闭包,函数可以访问定义在自身作用域外的变量,这个特性让js函数具有更强的封装性和灵活性。6.js是事件驱动,通过事件处理函数来响应用户的交互操作。7.支持异步编程。
9.书写代码完成JS的全排列。
// 递归、回溯 实现全排列
let permute = (nums) => {
let res = []
dfs([])
function dfs(path) {
if(path.length === nums.length) {
res.push([...path])
return
}
for(let i = 0;i<nums.length;i++){
if(path.includes(nums[i])){
continue
}
path.push(nums[i])
dfs(path)
path.pop()
}
}
return res
}
10.谈谈你所了解的跨域问题,为什么会有这个问题?如何解决?
回答:我了解的跨域问题是资源共享的策略,出于安全考虑,产生了浏览器的同源策略,不是统一个域名不能访问(同源:协议、域名、端口号都要一致)。解决方法:1.动态创建script标签,利用script标签不受同源策略影响的特性,但是这不安全。2.CORS,让后端设置跨域资源共享的请求头,设置Access-Control-Allow-Origin、Access-Control-Allow-Methods、等等如果是nodejs,可以添加cors中间件,cors的使用,默认使用是允许全部,如果要限制域名,可以给cors中的origin设置域名。3.前端设置中转代理服务器,vue中的vite支持设置代理,修改vite.config.js中的config配置下的proxy,将代理的api发送请求路径改变为真实的后端请求路径。4.自己创建服务器规避这个问题。
11.JS中string的startwith和indexof方法的区别。
回答:indexof返回是否包含该字符串有就返回首个出现的下标,没有就返回-1。startWith返回布尔值,表示字符串是否在原字符串的头部,也可以传第二个参数n表示从第n个下标开始查。
12.知道那些ES6、ES7的语法?
回答:ES6引入了class类、模块化(export、import)、箭头函数、模板字符串、解构赋值、展开操作符、Promise、let和const。ES7新增了Array.prototype.includes()方法、还有指数操作符**
13.轮播的实现原理?如果一个页面有两个轮播,你会如何实现?
回答:轮播的原理基本就是多个图片,一定时间内,改变图片的位置或者显示隐藏来实现图片的切换。如果一个页面有多个轮播,可以封装一个轮播功能函数,实现复用,不过要保证彼此不相互影响。
14.LocalStorage、SessionStorage、cookie的区别?
回答:存储大小:LocalStorage存储的内存和SessionStroage一样5-10MB,Cookie大小通常被限制在4kb。存储时长:LocalStroage会永久存储在浏览器,除非你手动清除或网站进行清除,或者设置一个属性存储有效时间过期就删除。SessionStroage会在关闭标签页或浏览器后清除。Cookie可以设置过期时间,不然默认会话结束清除。数据访问的权限:LocalStorage、SessionStroage数据可以在同源窗口(相同协议、域名和端口)中共享。Cookie也可以设置为同源窗口和跨域共享(通过设置 cookie 的 domain 属性)。服务器通信方面:LocalStorage、SessionStroage只存在客户端,不与服务器通信。Cookie每次请求都会携带过去服务器。安全性:LocalStorage、SessionStroage相对安全,不容易被盗取,Cookie因为会在请求中发送,容易受到CSRF(跨站请求伪造)攻击。实际具体使用情况选择相应的缓存。
15.解释一下webworker。
回答:js本身是单线程模式,而webworker的出现,创造了多线程的环境。在主线程中创建Worker对象传入要解析的js文件,通过postMessage,onmessage向Worker发送信息和接收消息。js文件中用onmessage监听主线程发送的消息,如何postMessage发送回去。通过这样的额工作模式,实现栏多线程工作,各个线程相互独立,通过通信机制来传递信息。不过webworker无法访问DOM、不能使用全局对象和方法、不能直接访问主线程的变量和函数。
16.介绍一下V8隐藏类。
回答:隐藏类是v8引擎创建具有相同类似结构的对象节省内存的方法,如果两个对象的结构、属性一致,V8引擎会用一个类来创建,直到其中一个对象添加新方法或属性,V8才会为其创建一个新的类结构。
17.AMD和CMD规范的区别?说一下CommonJS、AMD和CMD。
回答:AMD和CMD都是模块化的规范,AMD定义模块是通过define函数,可以接收模块标识符、依赖数组和工厂函数,并且在加载后立即执行。CMD在define基础上增加了require函数获取模块依赖实现按需加载,并且CMD认为模块不应该在定义时就执行,而是需要时才执行。AMD和CMD都是异步的、模块化加载,只是在语法和加载时机上不同。CommonJS是服务器端模块化的规范,主要是Nodejs环境,特点是同步加载模块。通过module.export定义,通过require导入。
18.谈一谈JS的运行机制。
回答:JS的运行机制有几个重要概念:1.执行上下文、2.作用域链、3.闭包、4.事件循环、5.异步编程。JS运行机制是由单线程的事件循环驱动的,意味着JS同一事件只能执行一个任务,但是可以通过异步编程处理大量并发任务。