第一章 面试题基础篇(JavaScript)

1.3 JavaScript考题

1. js延迟加载的方式有哪些?

参考网址:async vs defer attributes - Growing with the Web

延迟(异步)加载:async、defer,例如:

<script async src='script.js'></script>

<script defer src='script.js'></script>
  • defer : 等html全部解析完成,才会执行js代码,顺次执行js脚本。
  • async : async是和html解析同步的(一起的),执行js会暂停html解析,并且不是顺次执行js脚本(谁先加载完谁先执行)。

2. js数据类型有哪些?

基本类型:string、number、boolean、undefined、null、symbol、bigint
引用类型:object

NaN是一个数值类型,但是不是一个具体的数字。

关于数据类型的考题

a. 考题一:
console.log( true + 1 ); //2
console.log( 'name'+true ); //nameture
console.log( undefined + 1 );//NaN
console.log( typeof undefined );//undefined
b. 考题二:
console.log( typeof(NaN) ); //number
console.log( typeof(null) );//object

3. null和undefined的区别

一、奇怪点
有点奇怪的是,JavaScript语言居然有两个表示"无"的值:undefined和null。这是为什么?
二、历史原因

1995年JavaScript诞生时,最初像Java一样,只设置了null作为表示"无"的值。根据C语言的传统,null被设计成可以自动转为0。

但是,JavaScript的设计者,觉得这样做还不够,主要有以下两个原因:

1. null像在Java里一样,被当成一个对象。但是,JavaScript的数据类型分成原始类型(primitive)和合成类型(complex)两大类,作者觉得表示"无"的值最好不是对象。

2. JavaScript的最初版本没有包括错误处理机制,发生数据类型不匹配时,往往是自动转换类型或者默默地失败。作者觉得,如果null自动转为0,很不容易发现错误。

因此,作者又设计了一个undefined。

这里注意:先有null后有undefined,出来undefined是为了填补之前的坑。

三、具体区别

JavaScript的最初版本是这样区分的:null是一个表示"无"的对象(空对象指针),转为数值时为0;undefined是一个表示"无"的原始值,转为数值时为NaN。

四、总结:
1. 作者在设计js的都是先设计的null(为什么设计了null:最初设计js的时候借鉴了java的语言)
2. null会被隐式转换成0,很不容易发现错误。
3. 先有null后有undefined,出来undefined是为了填补之前的坑。

具体区别:JavaScript的最初版本是这样区分的:null是一个表示"无"的对象(空对象指针),转为数值时为0;undefined是一个表示"无"的原始值,转为数值时为NaN。

4. typeof和instanceof的区别

typeof 		 用于判断数据类型,返回一个字符串,适用于原始数据类型。
instanceof 用于判断对象是否是某个类的实例,适用于引用类型的判断。

关于instanceof一道考题:如何实现一个自己的instanceof

<script type="text/javascript">

const instanceofs = (target,obj)=>{
    let p = target;
    while( p ){
        if( p == obj.prototype ){
            return true;
        }

        p = p.__proto__;
    }
    return false;
}

console.log( instanceofs( [1,2,3] , Object ) )
    
</script>

5. JS判断变量是不是数组,你能写出哪些方法?

方式一:Array.isArray()
const arr = [1, 2, 3];
console.log(Array.isArray(arr)); 
方式二:isPrototypeOf():用于判断当前对象是否为另外一个对象的原型,如果是就返回 true,否则就返回 false。
var arr = [1,2,3];
console.log(  Array.prototype.isPrototypeOf(arr) )
方式三:constructor
var arr = [1,2,3];
console.log(  arr.constructor.toString().indexOf('Array') > -1 )
方式四:Object.prototype.toString.call()
const arr = [1, 2, 3];
console.log(Object.prototype.toString.call(arr) === '[object Array]'); 
方式五:instanceof
const arr = [1, 2, 3];
console.log(arr instanceof Array); 

6. ==和===区别

基本情况

==  :  比较的是值
        当使用 == 运算符比较两个值时,如果两个值的数据类型不同,JavaScript 会尝试进行隐式类型转换,将其中一个值转换为另一个值的数据类型,然后再进行比较。
        通过valueOf转换(valueOf() 方法通常由 JavaScript 在后台自动调用,并不显式地出现在代码中。)

=== : 除了比较值,还比较类型

总结区别

1. 相等运算符(==):判断两个值是否相等,但不考虑数据类型的差异。如果两个值的数据类型不同,== 会尝试进行类型转换,然后再比较值是否相等。

2. 严格相等运算符(===):也用于比较两个值是否相等,但会严格比较值和数据类型。如果两个值的数据类型不同,=== 不会进行类型转换,而是直接返回 false。

另外提示

相等运算符(==)和严格相等运算符(===)。它们都有缺点,前者会自动转换数据类型,后者的NaN不等于自身,以及+0等于-0,所以ES6新增了Object.is()方法。

+0 === -0 //true
NaN === NaN // false

Object.is(+0, -0) // false
Object.is(NaN, NaN) // true

7. js运行机制考题

a. js运行机制了解吗?

JavaScript 的运行机制是指 JavaScript 引擎如何执行代码的过程,其中包括了事件循环(Event Loop)和任务队列(Task Queue)等概念。下面是 JavaScript 的运行机制的简要介绍:

ⅰ. 同步和异步任务:JavaScript 是单线程的,意味着一次只能执行一个任务。任务分为同步和异步两种:
同步任务会按照代码顺序依次执行,中间不会被其他任务中断。
异步任务会在后台执行,不会阻塞后续代码的执行。
ⅱ. 事件循环(Event Loop):
JavaScript 引擎通过事件循环来管理任务的执行顺序。
事件循环的目的是确保任务按照正确的顺序执行,并且处理异步任务的执行顺序。
JS分为同步任务和异步任务
同步任务都在主线程上执行,形成一个执行栈
主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件。
一旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。
ⅲ. 任务队列(Task Queue):
任务队列分为宏任务队列(macrotask queue)和微任务队列(microtask queue)两种。
宏任务队列用于存放宏任务,如定时器回调、事件监听回调等。
微任务队列用于存放微任务,如 Promise 的 then() 和 catch() 方法产生的回调等。
ⅳ. 执行过程:
当 JavaScript 代码执行时,同步任务会立即执行,异步任务会被放入任务队列中。
当前任务执行完毕后,事件循环会从任务队列中取出任务执行,执行完毕后检查微任务队列是否有任务,有则立即执行微任务。
循环上述过程,直到所有任务执行完毕。

- 主线程运行时会产生执行栈, 栈中的代码调用某些api时,它们会在事件队列中添加各种事件(当满足触发条件后,如ajax请求完毕)
- 而栈中的代码执行完毕,就会读取事件队列中的事件,去执行那些回调
- 如此循环
- 注意,总是要等待栈中的代码执行完毕后才会去读取事件队列中的事件
b. js微任务和宏任务有哪些?

常见的微任务包括:

Promise 的 then() 和 catch() 方法产生的回调。
async/await 中的 await 关键字后面的代码。

常见的宏任务包括:

定时器(setTimeout、setInterval)的回调。
事件监听(如点击事件、键盘事件)的回调。
I/O 操作(如文件读写、网络请求)的回调。
渲染事件(requestAnimationFrame)的回调。

8. js数组考题

a. 什么是类数组?怎么转换成真正的数组?

类数组是一种类似数组的对象,但它不是真正的JavaScript数组。 类数组的主要特点是:

  • 它具有一个length属性,这个属性表示了类数组中元素的数量。
  • 类数组可以包含从0开始的自然递增整数作为键名。
  • 类数组可以像真正的数组一样进行遍历,例如使用for循环。
  • 但是,类数组不能调用一些真正的数组方法,如push、pop等,因为它们没有这些方法。
Array.from(new Set([1,2,1,2,3]))
Array.from( arguments )
b. js数组去重
ⅰ. new Set
const array = [1, 2, 2, 3, 4, 4, 5];
const uniqueArray = [...new Set(array)];
console.log(uniqueArray); // [1, 2, 3, 4, 5]
ⅱ. 使用 Array.filter() 和 indexOf()
const array = [1, 2, 2, 3, 4, 4, 5];
const uniqueArray = array.filter((value, index, self) => self.indexOf(value) === index);
console.log(uniqueArray); // [1, 2, 3, 4, 5]
ⅲ. 使用 Array.reduce() 和 includes()
const array = [1, 2, 2, 3, 4, 4, 5];
const uniqueArray = array.reduce((acc, currentValue) => acc.includes(currentValue) ? acc : [...acc, currentValue], []);
console.log(uniqueArray); // [1, 2, 3, 4, 5]
ⅳ. 双重循环
const array = [1, 2, 2, 3, 4, 4, 5];
const uniqueArray = [];
for (let i = 0; i < array.length; i++) {
    if (uniqueArray.indexOf(array[i]) === -1) {
        uniqueArray.push(array[i]);
    }
}
console.log(uniqueArray); // [1, 2, 3, 4, 5]
ⅴ. 数组进行扁平化,并且去重
const nestedArray = [1, 2, [3, 4], [5, 6, [7, 8]], 9, 9, 2];

// 扁平化
const flattenedArray = nestedArray.flat(Infinity);

// 去重
const uniqueArray = [...new Set(flattenedArray)];

console.log(uniqueArray); // Output: [1, 2, 3, 4, 5, 6, 7, 8, 9]
ⅵ. 在数组中,找出重复次数最多的元素
function findMostFrequentElement(arr) {
    let countMap = {};
    let maxCount = 0;
    let mostFrequentElement = null;

    arr.forEach((element) => {
        countMap[element] = (countMap[element] || 0) + 1;
        if (countMap[element] > maxCount) {
            maxCount = countMap[element];
            mostFrequentElement = element;
        }
    });

    return mostFrequentElement;
}

// 示例数组
const array = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4];
const mostFrequentElement = findMostFrequentElement(array);
console.log(`重复次数最多的元素是: ${mostFrequentElement}`);
c. js数组常用方法
push(): 向数组末尾添加一个或多个元素,并返回新的长度。
pop(): 删除数组末尾的元素,并返回被删除的元素。
shift(): 删除数组头部的元素,并返回被删除的元素。
unshift(): 向数组头部添加一个或多个元素,并返回新的长度。
concat(): 连接两个或多个数组,返回一个新数组。
slice(): 返回数组的指定部分,不会改变原数组。
splice(): 从指定位置开始删除或插入元素,可以改变原数组。
forEach(): 遍历数组并对每个元素执行指定操作。
map(): 遍历数组并生成一个新数组,新数组的元素是对原数组中每个元素调用指定函数的结果。
filter(): 遍历数组并返回符合条件的元素组成的新数组。
find(): 返回数组中第一个满足条件的元素。
findIndex(): 返回数组中第一个满足条件的元素的索引。
reduce(): 对数组中的每个元素执行一个累加器函数,将其结果汇总为单个返回值。
some(): 检测数组中是否至少有一个元素满足指定条件。
every(): 检测数组中的所有元素是否满足指定条件。
d. 数组转字符串的方法
join()
const arr = [1, 2, 3, 4, 5];
const str = arr.join(); // "1,2,3,4,5"
const strWithDash = arr.join('-'); // "1-2-3-4-5"
toString()
const arr = [1, 2, 3, 4, 5];
const str = arr.toString(); // "1,2,3,4,5"
JSON.stringify()
const arr = [1, 2, 3, 4, 5];
const str = JSON.stringify(arr); // "[1,2,3,4,5]"
e. js数组长度为10,删除比如第五个怎么办?
方式一:
let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// 删除第五个元素(索引为4)
arr.splice(4, 1);

console.log(arr); // [1, 2, 3, 4, 6, 7, 8, 9, 10]
方式二:
let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

let newArr = arr.slice(0, 4).concat(arr.slice(5));

console.log(arr); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
console.log(newArr); // [1, 2, 3, 4, 6, 7, 8, 9, 10]
f. slice是干嘛的,splice是否会改变原数组
ⅰ. slice() 方法:
  • slice() 方法返回一个新数组,其中包含从开始索引到结束索引(不包括结束索引)的元素,原始数组不会被修改。
  • slice() 方法接受两个参数,第一个参数是开始索引(包含),第二个参数是结束索引(不包含)。
    如果省略第二个参数,则 slice() 方法会一直提取到数组末尾。
  • slice() 方法不改变原数组,而是返回一个新的数组。
const arr = [1, 2, 3, 4, 5];
const slicedArr = arr.slice(1, 4); // 返回 [2, 3, 4]
console.log(arr); // [1, 2, 3, 4, 5],原数组不变
ⅱ. splice() 方法:
  • splice() 方法用于插入、删除或替换数组的元素,并可以改变原数组。
  • splice() 方法接受多个参数,第一个参数是起始索引,第二个参数是要删除的元素个数(如果为0,则不删除任何元素),从第三个参数开始是要插入的元素。
  • splice() 方法返回一个包含被删除元素的数组。
  • splice() 方法会直接修改原数组。
let arr = [1, 2, 3, 4, 5];
let removed = arr.splice(1, 2); // 删除索引位置1和2的元素
console.log(arr); // [1, 4, 5],原数组被修改
console.log(removed); // [2, 3],被删除的元素
总结:
slice() 方法用于提取原数组的一部分,不改变原数组。
splice() 方法用于对原数组进行插入、删除或替换操作,会改变原数组。
g. map和forEach的区别
  • 返回值:
    map 方法会返回一个新的数组,该数组包含经过回调函数处理后的每个元素。
    forEach 方法不返回任何值(没有返回值),它仅用于遍历数组中的每个元素。
  • 对原数组的影响:

map 不会改变原数组,它会返回一个新的数组。

forEach 不会返回新数组,但它会对原数组进行操作,修改原数组。

使用场景:

map 通常用于对数组中的每个元素进行转换,并返回一个新的数组。

forEach 用于遍历数组中的每个元素,但通常不用于创建新数组,而是用于执行对数组中元素的操作。

总结:

使用 map 时,如果需要对数组中的每个元素进行转换并获得一个新的数组,可以使用 map。
使用 forEach 时,如果只需要遍历数组中的元素并执行操作,不需要返回新数组,可以使用 forEach。
h. find和filter的区别

filter方法:
filter方法用于过滤数组中满足指定条件的所有元素,并返回一个新的数组,该数组包含所有满足条件的元素。
filter方法不会改变原始数组,而是返回一个新的数组,其中包含符合条件的元素。

const numbers = [1, 2, 3, 4, 5];
const evenNumbers = numbers.filter(num => num % 2 === 0); // 返回 [2, 4]

find方法:
find方法用于查找数组中满足指定条件的第一个元素,并返回该元素,如果找不到符合条件的元素,则返回undefined。
find方法通常用于查找数组中满足特定条件的单个元素。

const numbers = [1, 2, 3, 4, 5];
const firstEvenNumber = numbers.find(num => num % 2 === 0); // 返回 2
总结:
filter方法用于过滤数组中的元素,返回一个新数组包含所有满足条件的元素,如果不满足条件返回空数组
find方法用于查找数组中第一个满足条件的元素并返回,如果找不到则返回undefined。
i. some和every的区别

some 方法:
some 方法用于检查数组中是否至少有一个元素满足指定条件,如果至少有一个元素满足条件,则返回 true,否则返回 false。
some 方法会遍历数组中的每个元素,并对每个元素应用指定的条件,只要有一个元素满足条件,即返回 true。

const numbers = [1, 2, 3, 4, 5];
const hasEvenNumber = numbers.some(num => num % 2 === 0); // 返回 true,因为数组中有偶数

every 方法:
every 方法用于检查数组中的所有元素是否都满足指定条件,如果所有元素都满足条件,则返回 true,否则返回 false。
every 方法会遍历数组中的每个元素,并对每个元素应用指定的条件,只有当所有元素都满足条件时才返回 true。

const numbers = [2, 4, 6, 8, 10];
const allEvenNumbers = numbers.every(num => num % 2 === 0); // 返回 true,因为数组中所有元素都是偶数
总结:
some 方法用于检查数组中是否至少有一个元素满足条件,而 every 方法用于检查数组中的所有元素是否都满足条件。根据具体的需求,可以选择使用其中之一来进行条件检查。

9. js字符串考题

a. 字符串转换为数组的方法

可以使用 split() 方法将字符串转换为数组。
split()方法将字符串分割成子串,并返回一个包含分割后的子串的数组。

const str = "apple,banana,orange";
const arr = str.split(",");
console.log(arr); // ["apple", "banana", "orange"]
b. 给字符串新增方法实现功能

给字符串对象定义一个addPrefix函数,当传入一个字符串str时,它会返回新的带有指定前缀的字符串,例如:

console.log( 'world'.addPrefix('hello') )  

//控制台会输出helloworld

答案:

String.prototype.addPrefix = function( value ){
    return value + this
}	

console.log( 'world'.addPrefix('hello') )  
c. 找出字符串出现最多次数的字符以及次数
function findMostFrequentChar(str) {
    // 使用对象来存储字符及其出现的次数
    let charMap = {};
    let maxChar = '';
    let maxCount = 0;

    // 遍历字符串,统计每个字符出现的次数
    for (let char of str) {
        charMap[char] = (charMap[char] || 0) + 1;
        if (charMap[char] > maxCount) {
            maxChar = char;
            maxCount = charMap[char];
        }
    }

    return { char: maxChar, count: maxCount };
}

// 输入字符串
let inputString = "Your input string here";
let result = findMostFrequentChar(inputString);
console.log(`The most common character is '${result.char}' with a count of ${result.count}.`);

10. 闭包

什么是闭包:闭包(Closure)是指有权访问另一个函数作用域中的变量的函数。当一个函数能够访问并记住在其外部函数作用域中定义的变量,即使外部函数已经返回,这些变量也依然存在,这就形成了一个闭包。

闭包的优点:

记忆功能:闭包可以保存状态,如计数器、缓存等。
延长变量生命周期:闭包使得变量的生命周期与函数的执行时间相关,而不是仅在声明时。
实现高阶函数:闭包常用于创建柯里化函数或函数工厂。

闭包的缺点:

内存消耗:由于闭包会持有外部变量,可能会导致内存泄漏,尤其是在循环中创建大量闭包。
性能影响:闭包会增加代码复杂性,可能导致额外的开销。
难以调试:由于闭包内部的变量不易察觉,可能会影响代码的可读性和维护性。

闭包的使用场景:

模块化开发:通过闭包创建私有变量和方法,实现模块化的代码结构。
函数式编程:高阶函数、柯里化、延迟执行等场景。
缓存和回调:例如,实现异步操作(如AJAX请求)的回调函数,可以使用闭包保存状态。
事件处理程序:在事件监听器中,可以使用闭包存储与特定事件相关的状态。
计数器和迭代器:闭包可以用来维护循环中的状态,实现迭代器或计数器。

11. 防抖(Debounce):

防抖确保函数在用户停止触发事件后的一段时间内只调用一次。如果用户在设定的时间间隔内再次触发,那么计时器会被重置,直到用户停止触发才会再次调用。

function debounce(func, delay) {
  let timeoutId;
  return function() {
    const context = this;
    const args = arguments;
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
      func.apply(context, args);
    }, delay);
  };
}
// 使用防抖处理输入框的自动完成
document.getElementById('searchInput').addEventListener('input', debounce(handleSearch, 300));

12. 防抖和节流

前端防抖(Debounce)和节流(Throttle)是两种常用的性能优化技术,用于控制事件触发频率,避免过多的事件触发导致性能问题。

防抖(Debounce):

在事件触发后等待一定时间再执行处理函数,如果在等待时间内再次触发了事件,则重新计时。防抖常用于输入框输入事件、滚动事件等频繁触发的事件。

function debounce(func, delay) {
    let timeoutId;
    
    return function() {
        const context = this;
        const args = arguments;
        
        clearTimeout(timeoutId);
        timeoutId = setTimeout(() => {
            func.apply(context, args);
        }, delay);
    };
}

// 使用示例
const debouncedFunction = debounce(() => {
    console.log('Debounced function is called');
}, 300);

// 调用防抖函数
debouncedFunction();

节流(Throttle):

在一定时间内只允许事件触发一次,如果在这段时间内多次触发了事件,只有第一次触发的事件会被处理。节流常用于滚动触发加载更多数据、窗口大小改变事件等。

function throttle(func, delay) {
    let shouldExecute = true;
    
    return function() {
        if (!shouldExecute) return;
        
        shouldExecute = false;
        func.apply(this, arguments);
        
        setTimeout(() => {
            shouldExecute = true;
        }, delay);
    };
}

// 使用示例
const throttledFunction = throttle(() => {
    console.log('Throttled function is called');
}, 300);

// 调用节流函数
throttledFunction();

13. 原型和原型链了解吗?

原型(prototype):

在 JavaScript 中,每个对象都有一个原型(prototype)。对象可以继承另一个对象的属性和方法,这是通过原型实现的。每个对象都有一个指向它的原型的内部链接。当你试图访问一个对象的属性时,如果这个对象本身没有这个属性,JavaScript 就会去查找原型链上的对象。

原型链(prototype chain):

原型链是一种机制,用于在 JavaScript 中实现对象之间的继承。当你访问一个对象的属性或方法时,如果这个对象本身没有这个属性或方法,JavaScript 就会沿着原型链向上查找。原型链是一种链表结构,原型链顶端是null。

14. js实现继承的方式

a. 原型链继承:

缺点:原型中包含的引用值会在所有实例间共享 这是因为 在使用原型实现继承时,原型实际上变成了另一个类型的实例

function Parent(){
    this.age = 20;
}
function Child(){
    this.name = '张三'
}
Child.prototype = new Parent();
let o2 = new Child();
console.log( o2,o2.name,o2.age );
b. 借用构造函数继承:

缺点:只能继承父类的实例属性和方法,不能访问父类原型上定义的方法

function Parent(){
    this.age = 22;
}
function Child(){
    this.name = '张三'
    Parent.call(this);
}
let o3 = new Child();
console.log( o3,o3.name,o3.age );
c. 组合继承(原型链继承 + 借用构造函数继承):

缺点:解决原型链继承和借用构造函数继承的缺点,但是造成了多构造一次的性能开销

function Parent(){
    this.age = 100;
}
function Child(){
    Parent.call(this);
    this.name = '张三'
}
Child.prototype = new Parent();
let o4 = new Child();
console.log( o4,o4.name,o4.age );
d. ES6 类继承:
class Parent{
    constructor(){
        this.age = 18;
    }
}

class Child extends Parent{
    constructor(){
        super();
        this.name = '张三';
    }
}
let o1 = new Child();
console.log( o1,o1.name,o1.age );

15. new操作符做了什么?

  1. 创建了一个空的对象
  2. 将空对象的原型,指向于构造函数的原型
  3. 将空对象作为构造函数的上下文(改变this指向)
  4. 对构造函数有返回值的处理判断
function Fun( age,name ){
    this.age = age;
    this.name = name;
}
function create( fn , ...args ){
    //1. 创建了一个空的对象
    var obj = {}; //var obj = Object.create({})
    //2. 将空对象的原型,指向于构造函数的原型
    Object.setPrototypeOf(obj,fn.prototype);
    //3. 将空对象作为构造函数的上下文(改变this指向)
    var result = fn.apply(obj,args);
    //4. 对构造函数有返回值的处理判断
    return result instanceof Object ? result : obj;
}
console.log( create(Fun,18,'张三')   )

16. js改变this指向的方式有哪些?

在 JavaScript 中,有几种常见的方式可以改变函数中 this 的指向:

使用 call() 方法:

call() 方法允许你显式指定函数执行时的 this 值,同时可以传入参数列表。
例如:func.call(thisArg, arg1, arg2, ...)

使用 apply() 方法:

apply() 方法和 call() 类似,不同之处在于它接收一个参数数组而不是一系列参数。
例如:func.apply(thisArg, [arg1, arg2, ...])

使用 bind() 方法:

bind() 方法创建一个新的函数,其中 this 的值被设置为传递给 bind() 的第一个参数,而其他参数将作为新函数的参数传递。
例如:var newFunc = func.bind(thisArg);

17. 说一下call、apply、bind区别

在 JavaScript 中,call、apply 和 bind 是用来改变函数执行时的上下文(即 this 的指向)的方法。它们的主要区别在于传入参数的方式和返回值。

call 方法:

call 方法允许你调用一个函数,同时可以指定函数内部的 this 指向,并且可以传入单个或多个参数。
语法:function.call(thisArg, arg1, arg2, ...)
thisArg 为函数执行时 this 的值,后面的参数是传给函数的参数列表。
call 方法会立即执行函数。

apply 方法:

apply 和 call 作用相同,不同之处在于传入参数的方式。
语法:function.apply(thisArg, [argsArray])
thisArg 为函数执行时 this 的值,argsArray 是一个数组,包含传给函数的参数。
apply 方法会立即执行函数。

bind 方法:

bind 方法会创建一个新的函数,其中 this 的值被绑定在 bind 的第一个参数上。
语法:function.bind(thisArg, arg1, arg2, ...)
thisArg 为函数执行时 this 的值,后面的参数是被绑定的参数。
bind 方法不会立即执行函数,而是返回一个新函数,你可以稍后调用该函数。
总结:
call 和 apply 的作用是立即执行函数,改变函数内部的 this 指向,并传入参数。
bind 方法会创建一个新函数,其中 this 被永久绑定,你可以稍后调用这个函数。

1. 返回的区别:call和apply是立即执行函数,而bind会创建一个新函数(要执行这个函数)
2. 参数的区别:call和bind的参数是单个多个,而apply第二个参数是数组

18. 深拷贝和浅拷贝区别?浅拷贝有哪些?请实现一个深拷贝

概念:深拷贝(Deep Copy)和浅拷贝(Shallow Copy)是在编程中经常遇到的概念,它们之间的区别在于拷贝的深度。在 JavaScript 中,通常用于复制对象或数组。

浅拷贝(Shallow Copy):

浅拷贝只复制对象或数组的引用,而不是对象或数组本身。这意味着,如果原始对象中有引用类型的数据(如对象、数组),则浅拷贝后的对象中的引用类型数据仍然是原始对象中的引用,修改拷贝后对象中的引用类型数据会影响到原始对象。

常见的浅拷贝方法包括:Object.assign()、展开运算符等等。

//Object.assign()
const original = { a: 1, b: { c: 2 } };
const copy = Object.assign({}, original);
original.b.c = 3;
console.log(copy.b.c); // 输出 3,copy.b 仍然引用原对象的 b

//展开运算符 (...)
const original = { a: 1, b: { c: 2 } };
const copy = { ...original };

original.b.c = 3;
console.log(copy.b.c); // 输出 3

深拷贝(Deep Copy):

深拷贝会递归地复制所有的引用类型数据,包括对象中的对象、数组中的数组等,从而生成一个全新的对象或数组,对拷贝后的对象的修改不会影响到原始对象。

常见的深拷贝方法包括:JSON.parse() 和 JSON.stringify()、使用 lodash 库的、手动递归复制

//JSON.parse() 和 JSON.stringify()
const original = {
    a: 1,
    b: { c: 2, d: [3, 4] }
};

const deepCopy = JSON.parse(JSON.stringify(original));

original.b.c = 5;
console.log(deepCopy.b.c); // 输出 2,深拷贝成功

简单的实现一个深拷贝的函数示例:

function deepCopy(obj) {
    if (obj === null || typeof obj !== 'object') {
        return obj;
    }

    let copy = Array.isArray(obj) ? [] : {};

    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            copy[key] = deepCopy(obj[key]);
        }
    }

    return copy;
}

// 使用示例
let obj = {
    a: 1,
    b: {
        c: 2
    }
};

let objCopy = deepCopy(obj);
objCopy.b.c = 3;

console.log(obj.b.c); // 输出 2,深拷贝后修改拷贝对象不会影响原始对象

19. js如何比较2个相同的键值是不是完全一样

使用 JSON.stringify()

function areObjectsEqual(obj1, obj2) {
  return JSON.stringify(obj1) === JSON.stringify(obj2);
}

// 示例
const objA = { a: 1, b: { c: 2 } };
const objB = { a: 1, b: { c: 2 } };
const objC = { a: 1, b: { c: 3 } };

console.log(areObjectsEqual(objA, objB)); // true
console.log(areObjectsEqual(objA, objC)); // false

使用 lodash 库

const _ = require('lodash');

const obj1 = { a: { x: 1 }, b: 2 };
const obj2 = { a: { x: 1 }, b: 2 };
const obj3 = { a: { x: 2 }, b: 2 };

console.log(_.isEqual(obj1, obj2)); // true
console.log(_.isEqual(obj1, obj3)); // false

20. 本地存储考题

a. localStorage、sessionStorage、cookie的区别

localStorage、sessionStorage 和 cookie 都是Web存储技术,用于在用户的浏览器中存储数据。

  1. 存储容量
    localStorage: 通常可以存储 5MB 到 10MB 的数据,具体取决于浏览器。
    sessionStorage: 通常与 localStorage 相同,也可以存储 5MB 到 10MB 的数据。
    cookie: 每个 cookie 的大小限制是 4KB,且每个域名下的 cookie 数量通常限制在 20 个左右。
  2. 生命周期
    localStorage: 数据存储是持久的,除非显式删除,否则数据会一直保存在用户的浏览器中。
    sessionStorage: 数据仅在当前会话中有效,窗口或标签页关闭后,数据会被清除。
    cookie: 通过设置 expires 属性,可以定义 cookie 的过期时间,过期后会被删除;如果没有设置,cookie 会在会话结束时过期(类似于 sessionStorage)。
  3. 作用域
    localStorage: 数据在同一源(同一协议、主机和端口)下的所有页面中共享。
    sessionStorage: 数据仅在创建该会话的窗口或标签页中可用,不同窗口或标签页之间的数据不共享。
    cookie: cookie 在同一域名下的所有页面中共享,因此可以在不同的页面之间传递。
  4. 数据格式
    localStorage 和 sessionStorage: 存储的数据都是以键值对的形式存在,值是字符串类型,可以通过 JSON.stringify() 和 JSON.parse() 来存储和读取对象。
    cookie: 数据以字符串形式存储,通常需要手动进行编码和解码(例如使用 encodeURIComponent 和 decodeURIComponent)。
  5. 性能
    localStorage 和 sessionStorage: 相对较快,适合于存储大量数据。
    cookie: 每次请求都会自动发送到服务器,可能导致性能问题,尤其是存储较大的数据时。
  6. 安全性
    localStorage 和 sessionStorage: 数据不会自动发送到服务器,较为安全,适合存储不敏感的数据。
    cookie: 可以设置 HttpOnly 属性以防止 JavaScript 访问,增加安全性;还可以设置 Secure 属性,只在 HTTPS 连接中发送。
b. localStorage存储超过最大值怎么办?

方案一:lz-string压缩库(通过压缩的形式)

import LZString from 'lz-string'

写入:
localStorage.setItem('data', LZString.compress('Hello'));

读取:
LZString.decompress(localStorage.getItem('data'));

方案二:indexedDB(通过 localforage 库操作 indexedDB 存储数据)

import localForage from 'localforage';
const firstIndexedDB = localForage.createInstance({
    name: "Forage",// 数据库名称
    storeName: "forage_store_name"// 表名称
});

const userList = [
  {name:'张三'},{name:'李四'}
]

// 存储
firstIndexedDB.setItem('userList', userList).then(val => {
    val // 数组数据 [{...}, {...}, {...}]
}).catch(err => {
    console.log(err);
});

// 获取
firstIndexedDB.getItem('userList').then(val => {
    val // 数组数据 [{...}, {...}, {...}]
}).catch(err => {
    console.log(err);
});

21. JS对象考题

JS对象注意点:

1. 对象是通过new操作符构建出来的,所以对象之间不想等(除了引用外);
2. 对象注意:引用类型(共同一个地址);
3. 对象的key都是字符串类型;
4. 对象如何找属性|方法;
    查找规则:先在对象本身找 ===> 构造函数中找 ===> 对象原型中找 ===> 构造函数原型中找 ===> 对象上一层原型查找
考题一:
 [1,2,3] === [1,2,3]   //false
考题二:
var obj1 = {
    a:'hellow'
}
var obj2 = obj1;
obj2.a = 'world';
console.log(obj1); 	//{a:world}
(function(){
    console.log(a); 	//undefined
    var a = 1;
})();
考题三:
var a = {}
var b = {
    key:'a'
}
var c = {
    key:'c'
}

a[b] = '123';
a[c] = '456';

console.log( a[b] ); // 456

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值