js相关面试题
- 1.js的数据类型及判断及如何强制转换
- 基本数据类型
- 存储区别
- 数据类型的判断
- 如何转化数据类型
- 2.说说变量提升
- 3.数组的基本方法
- 4.数组去重
- 5.什么是尾递归
- 6.数组扁平化
- 7.1+'2'和1-'2'是什么
- 8.清除字符串前后空格和去除所有空格
- 1.去除字符串首尾的空格
- 2.去除字符串中所有的空格
- 3.使用正则去除你想去掉的空格
- 9.new干了什么
- 10.事件委托和事件代理
- 事件冒泡和捕获
- 开启捕获(`addEventListener` )
- 阻止冒泡( event.stopPropagation())
- 10.es6中class的理解
- 11.class中的静态属性和静态方法
- 12.用class类封装一个选项卡(面向对象)
- 13.js继承有哪些方法
- 14.什么是闭包,优缺点?
- 15.闭包的沙箱模式
- 16.如何判断this指向,箭头函数的this指向?
- 17.深拷贝和浅拷贝原理,实现方式?
- 浅拷贝 实现方法
- 深拷贝的实现方式
- 18.1+2===3返回false,为什么,如何解决?
- 19.防抖节流如何实现?
- 20.call,apply,bind原理,区别?
- 21.数组中取最大值?sort排序?
- 22.promise状态?all 和reace是谁的方法?
- 23.Promise.all如何局部处理
- 24.$(document).ready()方法和window.onload有什么区别
- 25.axios的核心对象,如何封装?
- 26.axios中get和post区别
- 27.==和===区别
- 28.usestate中如何用闭包实现?
- 29.promise封装ajax实现then链式调用
- 30.fetch和axios区别 ,优缺点?
- 区别
- 优缺点
- 31.http和https存在跨域吗?为什么
- 如何解决跨域
- 正向代理和反向代理
- 32.谈谈垃圾回收机制的方式及内存管理
- **一、垃圾回收机制—GC**
- **二、内存管理**
- 33.什么是事件流?
- 34.js内置对象
- 35.原型,构造函数,实例之间的关系
- 36.es6新增
- 37.js有哪些循环
- 38.生成器和迭代器
- 39.怎么添加、移除、复制、创建、和查找节点(1)创建新节点
- 40.箭头函数和普通函数
- 41.ajax状态码
- 42.web安全和防护
- 43.js输出的方式
- document.write () 和 innerHTML 区别
- 44.判断img加载完毕
- 45.封装轮播图
- 46.如何判断css3动画结束
- 47.prototype和proto区别,关系
- 48.单词首字母大写
- 49.promise手写
- 50.for in 和 for of区别
- 区别
- 51.null和undefined区别
- **null**
- **undefined**
- 52.call和apply和bind区别?
- 53.ES6对Array数组类型做了很多升级优化,以下是一些常用的:
- 54.虚拟列表如何实现
- 55.轮询(有哪些,优缺点)
- 56.作用域分为几种
- 作用域链
- 57.给某个资源的链接,如 https://www.baidu.com/index.html ,请实现一个方法,获取该资源的后缀,如 html
- 58.map循环和for循环谁的效率高
- 59.js进制转化在JavaScript中,可以使用内置的方法来进行进制转换。下面是几个常用的进制转换方法:
- 60.map和filter的区别
- 61.如何顺序执行10个异步任务
1.js的数据类型及判断及如何强制转换
基本数据类型
- 基本数据类型(原始类型):
undefined
:未定义的值null
:表示空值或不存在的对象boolean
:布尔值,即true
或false
number
:数字,包括整数和浮点数string
:字符串,用单引号或双引号括起来的文本
- 引用数据类型(对象类型):
object
:普通对象,包括对象字面量、数组、函数等symbol
:唯一且不可改变的数据类型,通常作为对象属性的标识符function
:表示可执行的代码块,可以被调用
存储区别
- 基本数据类型(原始类型):
- 基本数据类型的值直接存储在变量访问的位置上。
- 当创建一个基本数据类型的变量时,会为该变量分配固定大小的内存空间来存储对应的值。
- 对于相同值的基本数据类型变量,它们之间是相互独立的,修改其中一个变量不会影响其他变量。每个变量都包含自己的值。
- 当将一个基本数据类型的值赋给另一个变量时,会创建一个新的值的副本,它们之间是完全独立的。
- 引用数据类型(对象类型):
- 引用数据类型的值存储在堆内存中。
- 当创建一个引用数据类型的变量时,实际上是在栈内存中创建了一个指针(存储地址),指向堆内存中存储的实际数据。
- 不同变量可以引用同一个对象,它们共享同一个存储的数据。
- 当修改一个引用数据类型的属性或通过某个变量改变对象的内容时,其他引用该对象的变量也会反映出这些改变,因为它们指向同一个对象。
数据类型的判断
-
使用
typeof
运算符: 可以使用typeof
运算符来确定一个值的数据类型。它返回一个表示数据类型的字符串。例如:console.log(typeof 42); // 输出 "number" console.log(typeof "Hello"); // 输出 "string" console.log(typeof true); // 输出 "boolean"
-
使用
instanceof
运算符:instanceof
运算符可以用来检查对象是否属于某个特定的构造函数。例如:const arr = [1, 2, 3]; console.log(arr instanceof Array); // 输出 "true"
-
使用
Array.isArray()
方法:Array.isArray()
方法用于检查一个值是否为数组类型。例如:console.log(Array.isArray([1, 2, 3])); // 输出 "true"
-
使用
Object.prototype.toString
方法: 可以通过调用Object.prototype.toString
方法并将要检查的值作为参数传入,来获取其精确的数据类型信息。例如:console.log(Object.prototype.toString.call(42)); // 输出 "[object Number]" console.log(Object.prototype.toString.call("Hello")); // 输出 "[object String]"
如何转化数据类型
-
转换为字符串(String)类型:
-
使用toString()和String()方法:toString不可以转undefined,null
let num = 42; let str1 = String(num); let str2 = num.toString();
-
-
转换为数字(Number)类型:
-
使用Number()和parseInt()等方法:
let str = "42"; let num1 = Number(str); let num2 = parseInt(str); // 解析整数 let num3 = parseFloat("3.14"); // 解析浮点数
-
-
转换为布尔(Boolean)类型:
-
使用Boolean()函数:
let a = 42; let b = ""; // 空字符串 let bool1 = Boolean(a); // true let bool2 = Boolean(b); // false
-
-
转换为数组(Array)类型:
-
使用Array.from()方法将类似数组的对象转换为真正的数组:
let arrayLikeObj = { 0: "a", 1: "b", length: 2 }; let arr = Array.from(arrayLikeObj); // ["a", "b"]
-
-
转换为对象(Object)类型:
-
使用对象字面量或构造函数进行对象的创建:
let obj1 = {}; // 空对象 let obj2 = new Object();
-
2.说说变量提升
变量提升是 JavaScript 中的一种特性,它是指在代码执行前,JavaScript 引擎会将变量和函数的声明提升到其所在作用域的顶部。这意味着可以在声明之前访问这些变量或函数。
在 JavaScript 中,变量提升主要包括两个方面:
-
变量声明的提升: 变量声明使用
var
、let
或const
关键字进行,而变量的赋值操作则不会被提升。如果在代码中先使用了一个变量,然后才进行声明,JavaScript 会将该声明提升到作用域的顶部。但是变量的初始值仍然是undefined
。例如:console.log(foo); // 输出 undefined var foo = "Hello";
-
函数声明的提升: JavaScript 中的函数可以先使用后声明,因为函数声明也会被提升到作用域的顶部。这意味着你可以在函数声明之前调用函数。例如:
greet(); // 输出 "Hello" function greet() {console.log("Hello"); }
需要注意的是,变量提升只会提升声明,而不会提升赋值操作。如果变量没有使用 var
、let
或 const
声明,它将被视为全局对象的属性。此外,使用 let
和 const
声明的变量在块级作用域内存在暂时性死区(TDZ),也会影响到变量的提升行为。
3.数组的基本方法
push()
:向数组末尾添加一个或多个元素,并返回新数组的长度。pop()
:删除并返回数组的最后一个元素。unshift()
:向数组开头添加一个或多个元素,并返回新数组的长度。shift()
:删除并返回数组的第一个元素。concat()
:将两个或多个数组合并为一个新数组。slice()
:返回数组的指定部分(浅拷贝)。splice()
:从数组中添加/删除元素,或替换元素。forEach()
:对数组的每个元素执行提供的函数。map()
:创建一个新数组,其结果是对原数组的每个元素应用提供的函数。filter()
:创建一个新数组,其中包含通过提供函数的测试的所有元素。indexOf()
: 返回指定元素在数组中第一次出现的索引,如果不存在则返回-1。例如:includes()
: 判断数组是否包含指定元素,返回布尔值。例如:find()
: 返回数组中满足条件的第一个元素,如果没有找到则返回 undefined。该方法接受一个回调函数作为参数,在回调函数中定义匹配条件。例如:findIndex()
: 返回数组中满足条件的第一个元素的索引,如果没有找到则返回 -1。与find()
方法类似,也接受一个回调函数作为参数。例如:
4.数组去重
-
使用 Set 数据结构:Set 对象存储唯一值的集合,可以将数组转换为 Set,然后再将 Set 转回数组即可去重。例如:
const arr = [1, 2, 2, 3, 3, 4, 5, 5]; const uniqueArr = [...new Set(arr)]; console.log(uniqueArr); // 输出 [1, 2, 3, 4, 5]
-
使用 filter() 方法结合 indexOf() 方法:利用数组的 filter() 方法和 indexOf() 方法,对数组进行遍历筛选,只保留首次出现的元素。例如:
const arr = [1, 2, 2, 3, 3, 4, 5, 5]; const uniqueArr = arr.filter((item, index) => arr.indexOf(item) === index); console.log(uniqueArr); // 输出 [1, 2, 3, 4, 5]
-
使用 reduce() 方法:利用数组的 reduce() 方法,遍历数组,将不重复的元素放入新的数组。例如:
-
创建一个空对象 (或者使用已存在的对象)。
const arr = [1, 2, 2, 3, 3, 4, 5, 5]; const uniqueArr = arr.reduce((acc, curr) => {if (!acc.includes(curr)) {acc.push(curr);}return acc; }, []); console.log(uniqueArr); // 输出 [1, 2, 3, 4, 5]
-
遍历数组,对于每个元素进行以下操作:
- 将数组元素作为对象的属性名。
- 将任意值(比如 true)作为属性值,以标记该元素已经出现过。
- 提取对象的所有属性名,组成新的数组,即为去重后的数组。
javascript复制代码const arr = [1, 2, 2, 3, 3, 4, 5, 5];
const obj = {};
const uniqueArr = [];arr.forEach(item => {if (!obj[item]) {obj[item] = true;uniqueArr.push(item);}
});console.log(uniqueArr); // 输出 [1, 2, 3, 4, 5]
在上述代码中,obj
对象用于存储数组元素是否已经出现过。遍历数组时,如果当前元素不在 obj
对象中,则将其添加到 uniqueArr
数组中,并将 obj[item]
设置为 true
标记已经出现过。这样就可以通过对象的属性来实现数组去重。
5.什么是尾递归
尾递归(Tail Recursion)是指一个函数在其最后一步调用自身的递归形式。在尾递归中,递归调用是整个函数的最后一条语句,不会有其他的操作或表达式需要在递归调用之后执行。
尾递归有一个重要的特性:在每次递归调用之前,不会再进行任何其他的操作或计算,而是直接将递归调用的结果作为当前调用的结果返回。这样的尾递归称为尾递归优化(Tail Call Optimization)。尾递归优化可以避免由于递归过深导致的堆栈溢出问题,因为每次递归调用都会复用当前函数的栈帧,不会产生额外的堆栈空间消耗。
尾递归和普通递归不同,在普通递归中,每次递归调用都会形成一个新的调用帧,保存当前状态和上下文信息。而尾递归只保留一个调用帧,可以减少内存的使用和函数调用的开销。
尾递归常见的应用是实现阶乘、斐波那契数列等递归算法。通过使用尾递归优化,可以提高性能并避免堆栈溢出。
需要注意的是,并非所有的编程语言和编译器都对尾递归进行优化。一些编程语言和编译器提供了对尾递归的优化支持,而另一些并没有进行优化,仍然可能存在堆栈溢出的问题。在使用尾递归时,需要考虑目标平台和编译器的特性。
6.数组扁平化
数组扁平化是指将多维数组转换为一维数组的操作。在 JavaScript 中,我们可以使用多种方式来实现数组的扁平化。
以下是几种常用的方法:
- 使用递归:
function flattenArray(arr) {let result = [];arr.forEach(item => {if (Array.isArray(item)) {// 如果当前元素是数组,则递归调用flattenArray函数进行扁平化处理result = result.concat(flattenArray(item));} else {// 否则将当前元素添加到结果数组中result.push(item);}});return result;
}
2.使用Array.flat()
方法(ES2019):
const arr = [1, [2, [3, 4]]];
const flattenedArray = arr.flat(Infinity);console.log(flattenedArray); // 输出: [1, 2, 3, 4]
3.使用reduce()
方法和递归:
function flattenArray(arr) {return arr.reduce((result, item) => {if (Array.isArray(item)) {// 如果当前元素是数组,则递归调用flattenArray函数进行扁平化处理,并将结果与累加器合并result = result.concat(flattenArray(item));} else {// 否则将当前元素添加到累加器中result.push(item);}return result;}, []);
}
7.1+'2’和1-'2’是什么
1 + '2'
: 这个表达式中,1
是一个数字类型,而'2'
是一个字符串类型。当+
运算符的一个操作数是字符串时,它会将另一个操作数也转换为字符串,并执行字符串拼接操作。因此,结果是一个字符串'12'
。1 - '2'
: 在这个表达式中,1
是一个数字类型,而'2'
也是一个字符串类型。当-
运算符的操作数都是字符串或包含非数字字符时,它会尝试将这些字符串转换为数字并执行减法操作。在这种情况下,'2'
可以被成功转换为数字2
,然后进行减法运算。因此,结果是一个数字-1
。
综上所述,1 + '2'
的结果是字符串 '12'
,而1 - '2'
的结果是数字 -1
。
8.清除字符串前后空格和去除所有空格
1.去除字符串首尾的空格
trim()
2.去除字符串中所有的空格
相当于去除字符串中所有的空字符,可以使用 replace(),
3.使用正则去除你想去掉的空格
正则表达式(RegEx)
9.new干了什么
- 创建一个空的普通JavaScript对象。
- 将新创建的对象的原型连接到构造函数的原型(即继承构造函数的原型)。
- 使用新创建的对象作为构造函数的上下文(
this
)调用构造函数。 - 如果构造函数没有明确返回一个对象,则返回新创建的对象。
10.事件委托和事件代理
事件委托(event delegation)和事件代理(event delegation)是指同一种概念,在 JavaScript 中常用于处理事件的优化和性能提升。
事件委托/代理基本思想如下:
- 将事件绑定到一个父元素上,而不是将事件直接绑定到每个子元素上。
- 通过冒泡机制,当事件在子元素上触发时,会逐层向上冒泡到父元素,然后被父元素捕获并处理。
通过事件委托/代理的方式,可以实现以下几个好处:
- 减少事件处理程序的数量:只需将事件绑定到父元素上,而不需要为每个子元素分别绑定事件处理程序。
- 动态添加/删除子元素时仍可正常工作:由于事件是在父元素上处理的,因此对子元素的添加/删除不会影响事件的处理。
- 提升性能:减少了事件处理程序的个数,可以减少内存消耗,并提高事件的处理效率。
事件冒泡和捕获
事件冒泡:
- 在事件冒泡中,当一个元素触发了某个事件,该事件会从触发的元素开始向父元素逐层传播,直至传播到最顶层的文档对象。
- 也就是说,事件首先由触发事件的元素处理,然后从内部向外部传递,一层层向上冒泡。
- 通过事件冒泡,可以在父元素或祖先元素上捕获并处理事件。
事件捕获:
- 在事件捕获中,当一个元素触发了某个事件,该事件会从最顶层的文档对象开始向下传播至触发事件的元素。
- 即事件首先从最顶层的元素向下传递,直至传递到触发事件的元素。
- 通过事件捕获,可以在事件到达目标元素之前的任何祖先元素上捕获并处理事件
开启捕获(addEventListener
)
在 JavaScript 中,事件捕获是事件传播的一种阶段,它发生在事件冒泡之前。要开启事件捕获阶段,可以使用 addEventListener
方法的第三个参数,将其设置为 true
。
示例代码如下所示:
var element = document.getElementById('myElement');element.addEventListener('click', function(event) {console.log('捕获阶段');
}, true);
在上述示例中,我们通过将 addEventListener
的第三个参数设置为 true
,即开启了捕获阶段。当在 myElement
元素上触发 click 事件时,会先执行在捕获阶段注册的事件处理函数。
需要注意的是,大多数情况下不必显式开启捕获阶段,因为默认情况下事件会在冒泡阶段进行处理。捕获阶段和冒泡阶段共同构成了事件传播的过程。
总结起来:
- 捕获阶段:从最外层的祖先元素向目标元素传播,执行捕获阶段注册的事件处理函数。
- 目标阶段:到达目标元素本身,执行目标元素上注册的事件处理函数。
- 冒泡阶段:从目标元素向最外层的祖先元素传播,执行冒泡阶段注册的事件处理函数。
默认情况下,事件会在冒泡阶段进行处理。如果需要开启捕获阶段,可以将 addEventListener
方法的第三个参数设置为 true
。
阻止冒泡( event.stopPropagation())
在 JavaScript 中,要阻止事件冒泡(即停止事件向父元素的传播),可以使用 event.stopPropagation()
方法。
示例代码如下所示:
var element = document.getElementById('myElement');element.addEventListener('click', function(event) {event.stopPropagation(); // 阻止事件冒泡console.log('点击了子元素');
});document.addEventListener('click', function(event) {console.log('点击了文档');
});
在上述示例中,当点击 myElement
元素时,会触发子元素的点击事件处理函数,并输出 “点击了子元素”。由于调用了 event.stopPropagation()
,事件不会继续向上层元素传播,因此不会触发文档上的点击事件处理函数。
需要注意的是,event.stopPropagation()
只能阻止事件在当前元素及其祖先元素之间的传播,无法阻止事件在同级元素之间的传播。如果希望完全取消事件的传播,包括同级元素,可以使用 event.stopImmediatePropagation()
方法。
总结起来:
event.stopPropagation()
用于阻止事件冒泡,只影响当前元素及其祖先元素之间的传播。event.stopImmediatePropagation()
用于完全取消事件的传播,包括同级元素。
请注意,以上操作是按照默认的事件冒泡阶段进行处理的。如果在捕获阶段注册了事件处理函数,并且希望阻止捕获阶段中的事件传播,可以在捕获阶段的事件处理函数中使用 event.stopPropagation()
或 event.stopImmediatePropagation()
方法。
10.es6中class的理解
ES6(ECMAScript 2015)引入了类(Class)的概念,使得 JavaScript 中面向对象编程更加直观和易用。类是一种特殊的函数,用于创建对象,具有属性和方法。
以下是关于 ES6 类的主要理解:
- 类的定义:使用
class
关键字来定义一个类,后跟类名。类名通常采用大写字母开头的驼峰命名法。 - 构造函数:类中可以定义一个特殊的方法叫做构造函数,使用
constructor
关键字来声明。构造函数在使用new
关键字实例化类时自动调用,用于初始化对象的属性。 - 属性和方法:类内部可以定义属性和方法。属性是类的状态信息,存储对象的数据;方法是类的行为,用于操作对象。
- 实例化:通过使用
new
关键字和类名,可以创建类的实例(对象)。实例会继承类的属性和方法,并且每个实例拥有自己的独立数据。 - 继承:类可以继承另一个类的属性和方法,通过使用
extends
关键字,子类可以继承父类的所有成员。子类可以覆盖父类的方法或添加新的方法。 super
关键字:super
关键字用于从子类访问父类的属性和方法,可以在子类的构造函数和方法中调用。- 静态方法:类可以定义静态方法,使用
static
关键字来声明。静态方法属于类本身而不是实例,无需实例化即可调用。
使用 ES6 类的优势在于提供了更加面向对象的编程方式,使代码更结构化和易读,同时也方便了继承和代码复用。它更符合传统面向对象编程语言的习惯,让 JavaScript 开发者更容易理解和使用。
11.class中的静态属性和静态方法
静态属性是指属于类本身而不是类的实例的属性。可以通过在类中直接定义变量来创建静态属性。静态属性在所有实例之间共享相同的值,可以在类内部或外部访问。
静态方法是属于类本身的方法,而不是类的实例。可以使用 static
关键字来声明静态方法。静态方法可以在类的实例化过程中使用,并且无法访问实例的属性和方法。它们通常用于执行与类相关但不依赖于实例状态的操作。
下面是一个示例代码来说明静态属性和静态方法的用法:
class MyClass {// 静态属性static myStaticProperty = 'This is a static property';// 静态方法static myStaticMethod() {console.log('This is a static method');}// 实例方法myMethod() {console.log('This is an instance method');}
}// 访问静态属性
console.log(MyClass.myStaticProperty); // 输出: "This is a static property"// 调用静态方法
MyClass.myStaticMethod(); // 输出: "This is a static method"// 创建类的实例
const myObject = new MyClass();// 调用实例方法
myObject.myMethod(); // 输出: "This is an instance method"
注意,在调用静态属性和静态方法时,不需要实例化类。可以直接通过类名进行访问。
静态属性和静态方法提供了一种在类级别上处理数据和操作的方式,并且可以在不创建类的实例的情况下使用它们。适用于那些与实例无关但与类相关的功能。
12.用class类封装一个选项卡(面向对象)
封装一个选项卡的面向对象原理是将选项卡相关的属性和方法封装在一个类中,使得创建和管理选项卡变得更加方便和可复用。以下是使用类封装选项卡的基本原理:
- 创建一个类:首先,创建一个类来表示选项卡组件,可以使用ES6的
class
关键字定义一个类。
class Tab {// 构造函数constructor(container) {this.container = container;this.tabs = container.querySelectorAll('.tab');this.tabContents = container.querySelectorAll('.tab-content');// 其他属性...}// 方法...
}
- 定义属性:在构造函数中,设置选项卡组件的属性。这些属性可以包括容器元素、选项卡元素和内容元素的引用,以及其他与选项卡相关的属性。
- 定义方法:根据选项卡功能的需求,在类中定义相应的方法。例如,可以定义一个
activateTab
方法来激活指定索引的选项卡。
activateTab(index) {// 移除当前激活状态this.tabs.forEach((tab) => {tab.classList.remove('active');});this.tabContents.forEach((content) => {content.classList.remove('active');});// 激活指定索引的选项卡this.tabs[index].classList.add('active');this.tabContents[index].classList.add('active');
}
- 实例化类:在页面中创建一个容器元素,并使用该容器元素实例化选项卡类。这样就可以通过类的实例来使用选项卡组件。
const tabContainer = document.getElementById('tab-container');
const tabInstance = new Tab(tabContainer);
13.js继承有哪些方法
- 原型链继承: 原型链继承是通过将父类的实例作为子类的原型来实现继承。子类通过原型链可以访问到父类的属性和方法。但是,原型链继承存在的问题是所有子类实例共享同一个父类实例,导致对父类实例的修改会影响所有子类实例。
- 构造函数继承(借用构造函数): 构造函数继承通过在子类的构造函数中调用父类的构造函数来实现继承。这样可以实现每个子类实例都拥有独立的父类属性。然而,构造函数继承只能继承父类的实例属性和方法,不能继承父类原型上的属性和方法。
- 组合继承(经典继承): 组合继承结合了原型链继承和构造函数继承的优点。通过调用父类构造函数继承实例属性,并将父类的原型赋值给子类的原型来继承父类的原型属性和方法。这样既保证了实例属性的独立性,又继承了原型的属性和方法。但是,组合继承会导致父类构造函数被调用两次,一次是在继承实例属性时,另一次是在子类的原型赋值时。
- 原型式继承: 原型式继承通过创建一个临时构造函数,并将传入的对象作为该构造函数的原型来实现继承。这样可以直接继承传入对象的属性和方法。然而,原型式继承存在引用类型共享的问题。
- 寄生式继承: 寄生式继承在原型式继承的基础上增加了对继承过程的扩展或增
- class类继承:使用super继承父类的属性
14.什么是闭包,优缺点?
闭包(Closure)是指函数与其相关的引用环境组合而成的实体。它是由函数以及在该函数被创建时可访问的变量环境组合而成的包裹(或封闭)的范围。简单地说,闭包是一个函数可以访问和操作其外部作用域中的变量,即使在函数被调用之后仍然有效。
闭包的优点:
- 保护变量:闭包可以隐藏变量,对外部代码不可见,只能通过闭包函数内部的方法进行访问和修改。这种封装机制提供了一种更安全的方式来操作变量,防止对变量的意外修改和污染。
- 保持状态:闭包可以存储函数执行时的上下文环境,使得函数在后续调用时可以记住之前的状态。这使得闭包很适合用于创建局部作用域,并且可以在函数执行完毕后仍然保留数据和状态。
闭包的缺点:
- 内存占用:闭包会导致函数中引用的变量无法被释放,因为函数的执行环境被闭包所引用。如果过多使用闭包,可能会导致内存占用过多。
- 性能损耗:由于闭包需要保存额外的函数上下文信息,在一些特定场景下,使用闭包可能会导致性能下降。
- 容易造成内存泄漏:如果闭包函数中引用了全局变量或者其他无用的变量,这些变量将无法被垃圾回收机制清理,容易导致内存泄漏。
总体而言,闭包是一项强大的功能,可以在JavaScript中实现许多有用的模式和特性。但在使用闭包时,需要谨慎避免产生不必要的内存开销和潜在的性能问题。
15.闭包的沙箱模式
闭包的沙箱模式是指通过闭包创建一个独立的执行环境,用于封装和保护代码,以防止外部代码对其中的变量进行干扰或访问。
在沙箱模式下,使用闭包将函数和相关的变量封装在一个作用域内,形成一个私有的执行环境。这个执行环境类似于一个隔离的沙箱,它与外部环境相互独立,内部的变量和函数不会被外部环境所污染或访问。
沙箱模式的特点包括:
- 封装性:通过闭包,函数内部的变量和函数无法被外部直接访问,只能通过内部提供的接口间接操作。
- 私有性:函数内部的变量和函数具有私有性,外部无法获取或修改这些数据,实现了数据的保护和隐私。
- 函数隔离:每次调用函数都会创建一个新的闭包环境,函数的执行互不干扰,函数之间的变量不会相互影响。
- 数据持久性:通过闭包可以使得函数在执行完毕后仍然保留数据和状态,用于保存上一次执行时的结果或状态。
下面是一个简单的示例代码,演示了闭包的沙箱模式:
function createSandbox() {var data = 'Secret information';function privateFunction() {console.log(data);}return privateFunction;
}// 创建沙箱
var sandbox = createSandbox();// 调用沙箱内部的私有函数,输出 "Secret information"
sandbox();
在上述示例中,createSandbox
函数返回一个内部的私有函数 privateFunction
。这个内部函数可以访问和操作外部函数中的变量 data
,但其他代码无法直接获取或修改该变量。调用 createSandbox
创建了一个沙箱,并将内部函数赋值给变量 sandbox
。通过调用 sandbox()
可以执行沙箱内部的私有函数并输出结果。
沙箱模式提供了一种隔离代码执行环境的方式,可以保
16.如何判断this指向,箭头函数的this指向?
1.谁作为拥有者调用它就指向谁
function a() { console.log(this);
}
var b = {};
b.hehe = a;
b.hehe();
//这时候this指向b//常见的就是绑定事件
2.bind谁就指向谁
function a() { console.log(this);
}
var b = {};
var c = {};
b.hehe = a.bind(c);
b.hehe();
//这时候this指向c//如果你用bind的话
3.没有拥有者,直接调用,就指向window
function a() { console.log(this);
}
a();
//this指向window
4.call谁就是谁,apply谁就是谁,其实bind就是通过call和apply实现的
箭头函数不绑定this,会捕获其所在的上下文的this值,作为自己的this值
17.深拷贝和浅拷贝原理,实现方式?
浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。浅拷贝只复制对象的第一层属性
但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。对对象的属性进行递归复制
浅拷贝 实现方法
1、可以通过简单的赋值实现
类似上面的例子,当然,我们也可以封装一个简单的函数,如下:
function simpleClone(initalObj) { var obj = {}; for ( var i in initalObj) {obj[i] = initalObj[i];} return obj;}var obj = {a: "hello",b:{a: "world",b: 21},c:["Bob", "Tom", "Jenny"],d:function() {alert("hello world");}}var cloneObj = simpleClone(obj); console.log(cloneObj.b); console.log(cloneObj.c);console.log(cloneObj.d);cloneObj.b.a = "changed";cloneObj.c = [1, 2, 3];cloneObj.d = function() { alert("changed"); };console.log(obj.b);console.log(obj.c);console.log(obj.d);
2、Object.assign()实现
Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。但是 Object.assign() 进行的是浅拷贝,拷贝的是对象的属性的引用,而不是对象本身。
var obj = { a: {a: "hello", b: 21} };var initalObj = Object.assign({}, obj);initalObj.a.a = "changed";console.log(obj.a.a); // "changed"
注意:当object只有一层的时候,是深拷贝,例如如下:
var obj1 = { a: 10, b: 20, c: 30 };
var obj2 = Object.assign({}, obj1);
obj2.b = 100;
console.log(obj1);
// { a: 10, b: 20, c: 30 } <-- 沒被改到
console.log(obj2);
// { a: 10, b: 100, c: 30 }
深拷贝的实现方式
1、对象只有一层的话可以使用上面的:Object.assign()函数
2、转成 JSON 再转回来
var obj1 = { body: { a: 10 } };
var obj2 = JSON.parse(JSON.stringify(obj1));
obj2.body.a = 20;
console.log(obj1);
// { body: { a: 10 } } <-- 沒被改到
console.log(obj2);
// { body: { a: 20 } }
console.log(obj1 === obj2);
// false
console.log(obj1.body === obj2.body);
// false
用JSON.stringify把对象转成字符串,再用JSON.parse把字符串转成新的对象。
可以封装如下函数
var cloneObj = function(obj){var str, newobj = obj.constructor === Array ? [] : {};if(typeof obj !== 'object'){return;} else if(window.JSON){str = JSON.stringify(obj), //系列化对象newobj = JSON.parse(str); //还原} else {for(var i in obj){newobj[i] = typeof obj[i] === 'object' ? cloneObj(obj[i]) : obj[i]; }}return newobj;
};
3、递归拷贝
function deepClone(initalObj, finalObj) { var obj = finalObj || {}; for (var i in initalObj) { var prop = initalObj[i]; // 避免相互引用对象导致死循环,如initalObj.a = initalObj的情况if(prop === obj) { continue;} if (typeof prop === 'object') {obj[i] = (prop.constructor === Array) ? [] : {}; arguments.callee(prop, obj[i]);} else {obj[i] = prop;}} return obj;
}
var str = {};
var obj = { a: {a: "hello", b: 21} };
deepClone(obj, str);
console.log(str.a);
4、使用Object.create()方法
直接使用var newObj = Object.create(oldObj),可以达到深拷贝的效果。
function deepClone(initalObj, finalObj) { var obj = finalObj || {}; for (var i in initalObj) { var prop = initalObj[i]; // 避免相互引用对象导致死循环,如initalObj.a = initalObj的情况if(prop === obj) { continue;} if (typeof prop === 'object') {obj[i] = (prop.constructor === Array) ? [] : Object.create(prop);} else {obj[i] = prop;}} return obj;
}
5、jquery
jquery 有提供一个$.extend可以用来做 Deep Copy。
var $ = require('jquery');
var obj1 = {a: 1,b: { f: { g: 1 } },c: [1, 2, 3]
};
var obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f);
// false
6、lodash
另外一个很热门的函数库lodash,也有提供_.cloneDeep用来做 Deep Copy。
var _ = require('lodash');
var obj1 = {a: 1,b: { f: { g: 1 } },c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);
// false
18.1+2===3返回false,为什么,如何解决?
在 JavaScript 中,1 + 2 === 3
应该返回 true
,因为它是一个等值比较表达式,左边的表达式 1 + 2
的结果是 3
,而右边的表达式 3
是一个数值。
如果这个表达式返回 false
,可能有以下几种可能的原因:
- 类型不匹配:在进行比较时,JavaScript 会先尝试将操作数转换为相同的类型,然后再进行比较。如果其中一个操作数是字符串类型而另一个是数值类型,那么恒等比较
===
会返回false
。
示例:
"3" === 3; // false
- 浮点数精度问题:在 JavaScript 中,浮点数的计算存在精度问题。由于浮点数的内部表示方式,某些计算可能导致微小的舍入误差。因此,当涉及浮点数计算时,直接比较结果可能会产生不准确的结果。
示例:
0.1 + 0.2 === 0.3; // false
为了解决这个问题,可以使用舍入函数或者适当的精度处理方法来比较浮点数。
19.防抖节流如何实现?
-
防抖(Debounce): 防抖的原理是,在某个时间段内,如果事件持续触发,则重新计时,直到事件触发停止后再执行最后一次操作。可以通过以下步骤实现防抖功能:
- 设置一个定时器变量,用于记录延迟执行的操作。
- 每次触发事件时,先清除之前的定时器。
- 再设置一个新的定时器,延迟一定的时间后执行操作函数。
例如,以下是使用 JavaScript 实现防抖的示例代码:
function debounce(func, delay) {let timer;return function() {clearTimeout(timer);timer = setTimeout(() => {func.apply(this, arguments);}, delay);}; }// 使用防抖包装事件处理函数 const debounceFn = debounce(function() {// 执行需要防抖处理的操作 }, 300);// 监听事件,并应用防抖处理 element.addEventListener('input', debounceFn);
-
节流(Throttle): 节流的原理是,在某个时间段内,无论事件触发多少次,只执行一次操作。可以通过以下步骤实现节流功能:
- 设置一个标记变量,用于记录是否正在执行操作。
- 在操作执行完成后,将标记变量重置。
- 每次触发事件时,如果标记变量为真,则直接返回,不执行操作;否则执行操作并将标记变量设置为真。
例如,以下是使用 JavaScript 实现节流的示例代码:
function throttle(func, delay) {let isThrottled = false;return function() {if (isThrottled) {return;}isThrottled = true;setTimeout(() => {func.apply(this, arguments);isThrottled = false;}, delay);}; }// 使用节流包装事件处理函数 const throttleFn = throttle(function() {// 执行需要节流处理的操作 }, 300);// 监听事件,并应用节流处理 element.addEventListener('scroll', throttleFn);
通过防抖和节流的应用,可以有效地控制事件的触发频率,避免过多地执行操作,提高页面性能和用户体验。具体选择防抖还是节流要根据具体的场景和需求来决定。
20.call,apply,bind原理,区别?
call
, apply
, 和 bind
是 JavaScript 中用于修改函数执行上下文(即 this
的指向)的方法。
-
call
方法:call
方法允许你调用一个具有指定this
值和参数的函数。它的基本语法是function.call(thisArg, arg1, arg2, ...)
,其中:thisArg
:函数执行时的上下文对象,即函数中的this
的值。arg1, arg2, ...
:传递给函数的参数列表。
call
方法会立即执行函数,并将函数中的this
设置为thisArg
,并且可以传入多个参数作为函数的实参。例如:function greet(name) {console.log(`Hello, ${name}!`);console.log(this); }const person = {firstName: 'John',lastName: 'Doe' };greet.call(person, 'Alice');
输出:
Hello, Alice! { firstName: 'John', lastName: 'Doe' }
在上述示例中,通过
call
方法将greet
函数的执行上下文设置为person
对象,并传入'Alice'
作为函数的参数。 -
apply
方法:apply
方法与call
方法类似,也允许你调用一个具有指定this
值和参数的函数,但参数传递方式有所不同。它的基本语法是function.apply(thisArg, [argsArray])
,其中:thisArg
:函数执行时的上下文对象,即函数中的this
的值。argsArray
:传递给函数的参数组成的数组。
apply
方法会立即执行函数,并将函数中的this
设置为thisArg
,并且可以通过数组形式传入参数列表。例如:function greet(name) {console.log(`Hello, ${name}!`);console.log(this); }const person = {firstName: 'John',lastName: 'Doe' };greet.apply(person, ['Alice']);
输出:
Hello, Alice! { firstName: 'John', lastName: 'Doe' }
在上述示例中,通过
apply
方法将greet
函数的执行上下文设置为person
对象,并使用参数数组['Alice']
作为函数的参数。 -
bind
方法:bind
方法不同于call
和apply
方法,它并不立即执行函数,而是创建一个新的绑定函数,可以稍后调用执行。它的基本语法是function.bind(thisArg, arg1, arg2, ...)
,其中:thisArg
:函数执行时的上下文对象,即函数中的this
的值。arg1, arg2, ...
:预先绑定到函数的参数列表。
bind
方法返回一个绑定函数,函数中的this
被设置为thisArg
,并且可以预先绑定一部分参数。例如:function greet(name, message) {console.log(`${message}, ${name}!`);console.log(this); }const person = {firstName: 'John',lastName: 'Doe' };const sayHello = greet.bind(person, 'Hello'); sayHello('Alice');
输出:
Hello, Alice! { firstName: 'John', lastName: 'Doe' }
在上述示例中,通过
bind
方法将greet
函数的执行上下文设置为person
对象,并预先绑定'Hello'
作为函数的第一个参数。之后,创建的绑定
21.数组中取最大值?sort排序?
在 JavaScript 中,可以使用 Math.max()
方法或数组的 sort()
方法来获取数组中的最大值。
-
使用
Math.max()
方法:Math.max()
方法接受一组数字作为参数,并返回其中的最大值。但是,Math.max()
方法不接受数组作为参数,需要使用展开运算符...
将数组元素展开为单独的参数传递给该方法。示例代码如下:
const numbers = [10, 5, 8, 3, 12]; const maxNumber = Math.max(...numbers); console.log(maxNumber); // 输出:12
-
使用
sort()
方法:sort()
方法用于对数组进行排序,默认是按照字符编码的顺序排序。通过自定义比较函数,可以实现对数字类型的排序。使用sort()
方法将数组排序后,最后一个元素即为数组中的最大值。示例代码如下:
const numbers = [10, 5, 8, 3, 12]; numbers.sort((a, b) => a - b); // 升序排序 const maxNumber = numbers[numbers.length - 1]; console.log(maxNumber); // 输出:12
请注意,使用 sort()
方法会改变原始数组的顺序,如果不想改变原数组,可以先创建副本进行操作。
以上就是获取数组中最大值的方法,根据需求选择合适的方式进行处理。
22.promise状态?all 和reace是谁的方法?
在 JavaScript 中,.promise
表示一个 Promise 对象的状态。Promise 是一种用于处理异步操作的对象,它可以处于以下三种状态之一:
- Pending(进行中):Promise 对象初始状态为 pending(进行中),表示异步操作尚未完成。
- Fulfilled(已完成):当异步操作成功完成时,Promise 对象的状态会变为 fulfilled(已完成)。在这种状态下,Promise 的
then()
方法会被调用,并且可以获取到异步操作返回的结果值。 - Rejected(已拒绝):当异步操作失败或出错时,Promise 对象的状态会变为 rejected(已拒绝)。在这种状态下,Promise 的
catch()
或finally()
方法会被调用,并且可以处理错误或清理资源。
接下来是关于 .all()
和 .race()
方法的说明:
.all()
方法:Promise.all(iterable)
是一个静态方法,用于接收一个可迭代对象(如数组或类数组对象),并返回一个新的 Promise 对象。该新 Promise 对象在所有输入的 Promise 对象都变为 fulfilled 状态时才会变为 fulfilled。如果其中一个 Promise 对象变为 rejected 状态,那么该新的 Promise 对象将立即变为 rejected 状态。
示例:
const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = Promise.resolve(3);Promise.all([promise1, promise2, promise3]).then(values => {console.log(values); // [1, 2, 3]}).catch(error => {console.error(error);});
.race()
方法:Promise.race(iterable)
是一个静态方法,同样接收一个可迭代对象,并返回一个新的 Promise 对象。该新 Promise 对象在输入的 Promise 对象中有任意一个变为 fulfilled 或 rejected 状态时就会变为对应的状态。
示例:
const promise1 = new Promise((resolve) => setTimeout(resolve, 1000, 'Hello'));
const promise2 = new Promise((resolve) => setTimeout(resolve, 2000, 'World'));Promise.race([promise1, promise2]).then(value => {console.log(value); // 'Hello' (更快完成的 Promise)}).catch(error => {console.error(error);});
.all()
和 .race()
方法是 Promise 类的静态方法,可以通过 Promise.all()
和 Promise.race()
的方式来调用。
23.Promise.all如何局部处理
果在使用 Promise.all()
方法时需要对每个 Promise 对象的执行结果进行局部处理,可以将每个 Promise 对象包装在一个函数中,并使用 .map()
方法来创建一个由这些函数包装的 Promise 数组。然后,可以通过在 .then()
回调中处理该数组来获取每个 Promise 的局部结果。
下面是一个示例代码:
const promises = [Promise.resolve(1),Promise.resolve(2),Promise.reject(new Error('Error occurred')),Promise.resolve(3)
];const wrappedPromises = promises.map(p => {return p.then(value => {// 在这里对每个 Promise 对象的结果进行局部处理return value * 2; // 这里简单地将结果乘以 2 来演示局部处理}).catch(error => {// 在这里对每个 Promise 对象的错误进行局部处理return error.message; // 这里简单地返回错误消息字符串来演示局部处理错误});
});Promise.all(wrappedPromises).then(results => {console.log(results); // [2, 4, 'Error occurred', 6]}).catch(error => {console.error(error);});
在上述示例中,promises
数组包含了四个 Promise 对象。通过 .map()
方法,我们将每个 Promise 对象包装在一个函数中,并在每个函数中进行局部处理。在这里,我们简单地将 Promise 对象的结果乘以 2 或捕获错误并返回错误消息字符串。最后,使用 Promise.all()
方法来等待所有的 Promise 对象完成,并通过 .then()
回调处理局部结果。
24.$(document).ready()方法和window.onload有什么区别
(1):window.onload方法是在网页中所有的元素(包括元素的所有关联文件)完全加载到浏览器后才执行的。只能有一个
(2)😒(document).ready() 方法可以在DOM载入就绪时就对其进行操纵,并调用执行绑定的函数。可以有多个
25.axios的核心对象,如何封装?
Ajax
的原理简单来说是在用户和服务器之间加了—个中间层(AJAX
引擎),通过XmlHttpRequest
对象来向服务器发异步请求,从服务器获得数据,然后用javascrip
t来操作DOM
而更新页面。使用户操作与服务器响应异步化。这其中最关键的一步就是从服务器获得请求数据Ajax
的过程只涉及JavaScript
、XMLHttpRequest
和DOM
。XMLHttpRequest
是aja
x的核心机制
//1. 创建连接var xhr = null;xhr = new XMLHttpRequest()// 2. 连接服务器xhr.open('get', url, true)// 3. 发送请求xhr.send(null);// 4. 接受请求xhr.onreadystatechange = function(){if(xhr.readyState == 4){if(xhr.status == 200){success(xhr.responseText);} else { // failfail && fail(xhr.status);}}}
26.axios中get和post区别
- 参数位置:GET请求的参数通常以查询字符串的形式包含在URL的末尾,例如
http://example.com/api?param1=value1¶m2=value2
。而POST请求的参数通常以消息体(payload)的形式发送给服务器,不会直接显示在URL中。 - 安全性:由于GET请求的参数会出现在URL中,相对来说更容易被拦截和篡改,因此不适合传输敏感信息。而POST请求将参数放在消息体中,相对安全一些。
- 请求语义:GET请求用于从服务器获取数据,不应对服务器的状态产生任何影响,即具有幂等性(idempotent)。多次发送相同的GET请求应该得到相同的响应结果,不会对服务器状态造成任何改变。而POST请求用于向服务器提交数据,可能会对服务器的状态产生影响,比如创建新资源、更新数据等。
- 数据大小限制:GET请求对URL长度有限制,不同浏览器对URL长度的支持情况也不同,大概在几千个字符左右。而POST请求的参数可以放在消息体中,理论上对数据大小没有严格限制。
27.和=区别
1.===:三个等号我们称为等同符,当等号两边的值为相同类型的时候,直接比较等号两边的值,值相同则返回true,若等号两边的值类型不同时直接返回false。
例:100===“100” //返回false
abc===“abc” //返回false
‘abc’===“abc” //返回true
NaN===NaN //返回false
false===false //返回true
2.==:两个等号我们称为等值符,当等号两边的值为相同类型时比较值是否相同,类型不同时会发生类型的自动转换,转换为相同的类型后再作比较。
28.usestate中如何用闭包实现?
在React中,useState是一种常用的状态管理钩子,用于在函数组件中声明和更新状态。通常情况下,我们可以使用简单的方式来定义和更新状态,例如:
import React, { useState } from 'react';function Example() {const [count, setCount] = useState(0);return (<div><p>{count}</p><button onClick={() => setCount(count + 1)}>Increment</button></div>);
}
这里的useState返回一个由初始值和更新函数组成的数组。然而,你也可以使用闭包来实现useState。以下是使用闭包实现的示例代码:
import React from 'react';function Example() {const countState = useClosureState(0);const increment = () => {countState.setValue(countState.getValue() + 1);};return (<div><p>{countState.getValue()}</p><button onClick={increment}>Increment</button></div>);
}// 自定义的闭包状态管理钩子
function useClosureState(initialValue) {let value = initialValue;const getValue = () => value;const setValue = (newValue) => {value = newValue;// 触发组件重新渲染// 此处省略具体的重新渲染逻辑};return {getValue,setValue};
}
在上述代码中,我们自定义了一个名为useClosureState的闭包状态管理钩子。通过闭包特性,我们在自定义钩子中创建了一个value变量,并通过getValue和setValue来访问和更新该变量的值。在组件中,我们使用countState来代替useState返回的数组,然后通过countState.getValue()获取状态值,通过countState.setValue()更新状态值。
需要注意的是,闭包方式并不兼容React的更新机制,当调用setValue更新状态值时,组件并不会自动重新渲染。因此,在实际场景中,还需要补充相应的重新渲染逻辑,以确保组件正确显示最新的状态值。
29.promise封装ajax实现then链式调用
function ajaxRequest(url, method, data) {return new Promise((resolve, reject) => {const xhr = new XMLHttpRequest();xhr.open(method, url);xhr.setRequestHeader('Content-Type', 'application/json');xhr.onload = function() {if (xhr.status >= 200 && xhr.status < 300) {resolve(JSON.parse(xhr.responseText));} else {reject(new Error(xhr.statusText));}};xhr.onerror = function() {reject(new Error('Network error'));};xhr.send(JSON.stringify(data));});
}// 调用示例
ajaxRequest('/api/user', 'GET', { id: 1 }).then(data => {console.log('请求成功:', data);// 在这里处理返回的数据}).catch(error => {console.log('请求失败:', error);// 在这里处理请求失败的情况});
在上述示例中,ajaxRequest()
函数使用原生的XMLHttpRequest对象发送请求。它接受三个参数:URL、请求方法和要发送的数据。
在创建XHR对象后,我们设置了请求头并定义了onload
和onerror
事件处理程序。如果请求成功(状态码为2xx),将解析响应文本并通过resolve()
传递给下游的.then()
方法。如果请求失败或出现网络错误,将通过reject()
将错误对象传递给下游的.catch()
方法。
通过使用这种封装,你可以在调用ajaxRequest()
时链式地使用.then()
和.catch()
来处理异步请求的结果。
30.fetch和axios区别 ,优缺点?
区别
- API设计:Fetch使用了新的Promise-based标准API,而Axios则是基于XMLHttpRequest构建的,并提供了一种基于Promise的API。Fetch使用了原生的JavaScript Fetch API,因此在支持该API的现代浏览器中不需要额外的依赖,而Axios是一个独立的第三方库。
- 浏览器兼容性:由于Fetch是基于Fetch API实现的,所以在不支持Fetch API的旧版浏览器中可能无法正常工作,需要使用Polyfill或额外的代码进行兼容处理。而Axios则可以在更广泛的浏览器环境中使用,包括旧版本浏览器。
- 功能丰富性:Axios在功能方面更加丰富,例如支持请求和响应的拦截、取消请求、自动转换JSON等功能。它还提供了更多的配置选项,如设置请求超时时间、认证等。Fetch虽然只提供了基本的请求功能,但可以使用扩展插件来实现类似的功能。
- 文件上传和下载:Axios提供了直接处理文件上传和下载的功能,可以更方便地发送FormData和处理响应的二进制数据。Fetch也可以处理文件上传和下载,但相对来说实现起来相对复杂一些。
- 默认设置:Axios在默认情况下会自动将响应数据解析为JSON格式,而Fetch需要手动调用
response.json()
方法来解析响应。在Axios中还可以设置默认的请求头、拦截器等,而Fetch需要手动进行配置。
优缺点
Fetch的优点:
- 原生支持Promise,可以使用async/await进行更简洁的异步处理。
- 内置在现代浏览器中,无需引入额外的库或依赖。
- 提供了Response对象,可以直接操作响应数据。
Fetch的缺点:
- API相对较低级,需要手动处理请求和响应,如设置请求头、解析JSON等。
- 在某些旧版浏览器中不被支持,需要使用Polyfill进行兼容处理。
- 不支持请求和响应的拦截器,以及请求的取消等高级功能。
Axios的优点:
- 面向开发者友好,提供简明易懂的API,并且提供了丰富的功能选项,如拦截器、取消请求、自动转换JSON等。
- 支持所有主流浏览器,包括旧版浏览器。
- 提供了更好的错误处理机制,如果请求失败会抛出异常,方便统一处理。
Axios的缺点:
- 需要额外引入第三方库。
- 与Fetch相比,体积稍大。
选择使用Fetch还是Axios取决于具体需求和项目环境。如果在现代浏览器中工作且对体积要求较高,可以选择使用Fetch。如果需要更多的功能选项、兼容旧版浏览器或更友好的API,则可以选择Axios。
31.http和https存在跨域吗?为什么
HTTP和HTTPS本身并不引起跨域问题。跨域问题是由浏览器的同源策略产生的。
同源策略是一种安全机制,限制了来自不同源(协议、域名、端口)的文档或脚本之间的交互。当浏览器执行包含JavaScript的网页时,该策略会阻止对其他源的请求进行读取或操作。
如果一个网页使用HTTP协议加载,而另一个网页使用HTTPS协议加载,则它们被视为不同源。这是因为协议部分不同,符合同源策略的定义。因此,从使用HTTP加载的网页向使用HTTPS加载的网页发送请求,会遇到跨域问题。
为了实现在不同源之间的安全通信,可以通过使用CORS(跨源资源共享)头部信息、代理服务器或者JSONP等技术来克服跨域限制。
需要注意的是,即使两个网页的协议相同,但如果它们的域名或端口号不匹配,仍然被认为是不同源,可能会引发跨域问题。
总结起来,HTTP和HTTPS本身不会导致跨域问题,跨域问题是由浏览器的同源策略引起的。
如何解决跨域
- CORS(跨源资源共享):在服务器端设置响应头部信息,允许跨域请求。通过设置
Access-Control-Allow-Origin
头部字段,将允许访问的域名添加到响应中。同时还可以设置其他相关的头部字段,如Access-Control-Allow-Methods
和Access-Control-Allow-Headers
来控制允许的请求方法和头部信息。 - 代理服务器:在同一个域名下设置一个代理服务器,用于转发跨域请求。前端将请求发送给代理服务器,再由代理服务器向目标服务器发起请求,最后将响应结果返回给前端。这样前端就可以绕过浏览器的同源策略限制。
- JSONP(JSON with Padding):利用
标签没有跨域限制的特性,实现跨域请求。通过在页面中动态创建
标签,将远程URL作为其src属性值,并提供一个回调函数作为参数传递到远程服务器。远程服务器在返回数据时,将数据作为参数传递给回调函数,从而实现跨域数据获取。 - WebSocket:使用WebSocket协议进行双向通信,不受同源策略限制。WebSocket提供了一个持久化的连接,可以在客户端和服务器之间实时地传输数据。
正向代理和反向代理
正向代理和反向代理是常用的代理服务器配置方式,它们在应用场景、功能和部署位置上有所不同:
- 正向代理(Forward Proxy):
- 定义:客户端通过正向代理服务器访问互联网资源。
- 功能:代理服务器代表客户端发送请求,接收响应,并将响应返回给客户端。客户端对代理服务器的存在是透明的。
- 场景:主要用于增加网络安全性和保护客户端隐私,隐藏客户端真实IP地址,突破访问限制,加速访问, 或者缓存网页内容等。
- 示例:A想访问互联网,但由于某些原因无法直接访问,A配置了一个正向代理B,A的请求经过B转发到互联网上的资源。
- 反向代理(Reverse Proxy):
- 定义:客户端通过反向代理服务器访问内部资源。
- 功能:代理服务器代表内部服务器接收请求,处理请求,并将响应返回给客户端。客户端对服务器的存在是透明的。
- 场景:主要用于负载均衡、高可用性、安全性和缓存等。能够隐藏后端服务器的真实信息,提供更强的安全保护,并且可以根据请求的内容进行智能路由。
- 示例:客户端向反向代理服务器发送请求,反向代理服务器根据配置的规则将请求转发给后端多台服务器中的一台, 并将后端服务器的响应返回给客户端。
总结: 正向代理和反向代理都是通过代理服务器来实现请求转发和响应处理的。区别在于正向代理面向客户端,隐藏客户端身份; 而反向代理面向内部服务器,隐藏服务器身份。它们在网络安全、负载均衡、加速访问等方面都有重要作用,根据具体需求选择合适的代理方式。
32.谈谈垃圾回收机制的方式及内存管理
一、垃圾回收机制—GC
Javascript具有自动垃圾回收机制(GC:Garbage Collecation),也就是说,执行环境会负责管理代码执行过程中使用的内存。
原理:垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存
通常情况下有两种实现方式:标记清除和引用计数
- 标记清除: js中最常用的垃圾回收方式就是标记清除。
当变量进入环境时,例如,在函数中声明一个变量,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。而当变量离开环境时,则将其标记为“离开环境”。
- 引用计数的含义是跟踪记录每个值被引用的次数。
当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1。如果同一个值又被赋给另一个变量,则该值的引用次数加1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减1。当这个值的引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾回收器下次再运行时,它就会释放那些引用次数为0的值所占用的内存。
二、内存管理
1)、Javascript引擎基础GC方案是(simple GC):mark and sweep(标记清除),即:
- (1)遍历所有可访问的对象。
- (2)回收已不可访问的对象。
2)、GC的缺陷
和其他语言一样,javascript的GC策略也无法避免一个问题:GC时,停止响应其他操作,这是为了安全考虑。而Javascript的GC在100ms甚至以上,对一般的应用还好,但对于JS游戏,动画对连贯性要求比较高的应用,就麻烦了。这就是新引擎需要优化的点:避免GC造成的长时间停止响应。
3)、GC优化策略
- 1)分代回收(Generation GC)
这个和Java回收策略思想是一致的。目的是通过区分“临时”与“持久”对象;多回收“临时对象”区(young generation),少回收“持久对象”区(tenured generation),减少每次需遍历的对象,从而减少每次GC的耗时 - 2)增量GC
这个方案的思想很简单,就是“每次处理一点,下次再处理一点,如此类推”
33.什么是事件流?
事件流(Event Flow)是指在交互式的用户界面中,事件从发生到处理的整个过程。在Web开发中,事件流决定了事件从被触发到最终被处理的路径和顺序。
在传统的事件模型中,存在两种主要的事件流模型:冒泡(Bubbling)和捕获(Capturing)。这两种模型描述了事件在DOM树中的传播方式:
- 冒泡(Bubbling): 事件首先在触发的节点上被处理,然后沿着DOM树向上传播,依次触发父节点的相同事件处理程序,直至传播到最顶层的节点(通常是
window
对象)。冒泡阶段允许父级元素捕获和处理子元素的事件。 - 捕获(Capturing): 事件首先从最顶层的节点(通常是
window
对象)开始传播,沿着DOM树向下传播,直至到达触发事件的节点。捕获阶段允许更早地捕获和处理事件,但在实践中较少使用。
在现代的Web开发中,大多数浏览器都采用了事件委托(Event Delegation)的方式来优化事件处理性能。事件委托利用了事件冒泡的机制,在父级元素上注册事件处理程序,通过冒泡阶段将事件处理委托给子元素,从而减少了重复注册事件处理程序的数量,提高了性能和代码的可维护性。
总结来说,事件流描述了事件在DOM树中从触发到处理的传播过程,可以通过捕获和冒泡阶段进行事件处理,并借助事件委托优化性能。
34.js内置对象
JavaScript内置对象是指在JavaScript语言中提供的一些预定义对象,可以直接在代码中使用而无需进行额外的声明或导入。以下是一些常见的JavaScript内置对象:
- Object:表示一个普通的JavaScript对象,是所有对象的基类。
- Array:用于表示数组,可以存储多个值,并提供了一系列操作数组的方法。
- String:用于表示字符串,提供了许多用于操作字符串的方法。
- Number:用于表示数字,提供了许多用于处理数字的方法和属性。
- Boolean:用于表示布尔值,即true或false。
- Math:提供了一组数学运算的方法和常量,如三角函数、对数函数等。
- Date:用于表示日期和时间,提供了获取和设置日期时间的方法。
- RegExp:用于进行正则表达式匹配的对象,提供了各种匹配和替换的方法。
- Function:表示一个函数对象,可以通过函数名或匿名函数来创建。
- JSON:提供了解析JSON格式数据和序列化JavaScript对象为JSON字符串的方法。
除了以上这些常见的内置对象,JavaScript还提供了其他许多有用的内置对象,如Error(表示错误对象)、Promise(表示异步操作的状态和结果)等。这些内置对象提供了丰富的功能和方法,可以帮助开发者更方便地进行各种操作和处理。
35.原型,构造函数,实例之间的关系
在JavaScript中,原型(prototype)、构造函数(constructor)和实例之间存在着密切的关系。
- 原型(prototype):每个函数对象(包括内置函数和自定义函数)都有一个名为 prototype 的属性,它指向一个对象。这个对象被称为原型对象,也简称为原型。原型对象是用来存储共享的属性和方法,供该构造函数创建的实例对象进行访问和继承。
- 构造函数(constructor):构造函数是用于创建对象的函数。构造函数可以通过
new
关键字调用,并且在调用时会创建一个新的对象作为其实例。构造函数可以设置实例的初始状态,以及定义实例特有的属性和方法。 - 实例:实例是通过构造函数创建的具体对象。每个实例都有自己的属性和方法,并且可以访问其构造函数的原型对象上的共享属性和方法。通过原型链机制,实例可以沿着原型链查找并继承原型对象的属性和方法。
下面是一个示例代码:
// 构造函数
function Person(name) {this.name = name;
}// 在原型对象上定义方法
Person.prototype.sayHello = function() {console.log("Hello, my name is " + this.name);
};// 创建实例
var person1 = new Person("Alice");
var person2 = new Person("Bob");person1.sayHello(); // 输出 "Hello, my name is Alice"
person2.sayHello(); // 输出 "Hello, my name is Bob"
在这个例子中,Person
是一个构造函数,它定义了一个 name
属性和一个 sayHello
方法。通过 new Person("Alice")
创建的对象 person1
是 Person
的一个实例,它具有自己的 name
属性,并可以访问 Person.prototype
上的 sayHello
方法。
综上所述,原型对象存储构造函数的共享属性和方法,构造函数用于创建实例对象,而实例对象通过原型链机制可以访问并继承原型对象的属性和方法。
36.es6新增
- 块级作用域(Block Scope):引入了
let
和const
关键字,允许在块级作用域内声明变量,并且在作用域之外无法访问。 - 箭头函数(Arrow Functions):引入了箭头函数的简洁语法,可以更方便地声明匿名函数,并且自动绑定了当前上下文的
this
值。 - 默认参数值(Default Parameters):函数参数可以设置默认值,当调用函数时没有传递对应参数时,将使用默认值。
- 模板字符串(Template Strings):通过反引号
和占位符
${}` 的组合,可以更方便地拼接字符串和插入变量。 - 解构赋值(Destructuring Assignment):可以从数组或对象中提取值,并赋值给对应的变量。
- 类和继承(Class and Inheritance):引入了
class
关键字和类的概念,可以更直观地定义对象、原型和类的关系,并通过extends
实现继承。 - 模块化(Modules):支持模块化的导入和导出语法,可以更好地组织和管理代码。
- 迭代器和生成器(Iterators and Generators):引入了可迭代对象和生成器函数的概念,可以更灵活地遍历数据和实现异步操作。
- Promise 对象:提供了一种更优雅处理异步操作的方式,避免回调地狱。
- 新的数据类型:新增了
Map
、Set
、Symbol
等新的数据类型,扩展了 JavaScript 的数据处理能力。
37.js有哪些循环
for
循环:通过指定初始条件、循环终止条件和递增/递减表达式,来重复执行一段代码块。
for (初始化; 终止条件; 递增/递减) {// 执行的代码块
}
while
循环:只在给定条件为真时重复执行代码块,没有明确的计数器变量。
hile (条件) {// 执行的代码块
}
do...while
循环:先执行一次代码块,然后只要给定条件为真,就重复执行。
do {// 执行的代码块
} while (条件);
for...in
循环:遍历对象的可枚举属性,在每次迭代中将属性赋值给一个变量。
for (变量 in 对象) {// 执行的代码块
}
for...of
循环:以可迭代对象的元素作为循环变量,可以遍历数组、字符串、Set、Map等可迭代的数据结构。
for (变量 of 可迭代对象) {// 执行的代码块
}
38.生成器和迭代器
生成器和迭代器是 JavaScript 中用于处理可迭代对象的特殊工具。
迭代器(Iterator)是一个对象,它实现了指定的迭代器协议。这个协议包括一个
next()
方法,每次调用该方法时都会返回一个包含value
和done
属性的对象。
value
表示迭代器当前所指向的值。done
是一个布尔值,表示迭代是否已经完成。生成器(Generator)是一个特殊类型的函数,使用
function*
语法声明。生成器函数内部可以通过yield
关键字来暂停函数的执行,并返回一个中间结果。当生成器函数再次被调用时,会从上一次yield
的位置继续执行,直到遇到下一个yield
或函数结束。生成器函数与普通函数的不同之处在于,生成器函数返回的是一个迭代器对象,通过调用生成器函数可以获取到这个迭代器对象。我们可以使用这个迭代器对象按需逐步获取生成器函数中产生的值。
以下是一个简单的示例:
function* generatorFunction() {yield 'Hello';yield 'World';yield '!'; }const generator = generatorFunction(); // 获取生成器的迭代器console.log(generator.next()); // { value: 'Hello', done: false } console.log(generator.next()); // { value: 'World', done: false } console.log(generator.next()); // { value: '!', done: false } console.log(generator.next()); // { value: undefined, done: true }
在上面的示例中,
generatorFunction()
是一个生成器函数,调用它返回一个迭代器对象generator
。通过不断调用generator.next()
方法可以依次遍历生成器函数中产生的值。需要注意的是,当生成器函数执行完毕后再次调用
next()
方法会得到{ value: undefined, done: true }
的结果,表示迭代已经完成。迭代器和生成器提供了一种更灵活和可控的方式来处理可迭代对象,使得我们可以按需获取想要的数据,而无需一次性加载全部数据。
39.怎么添加、移除、复制、创建、和查找节点(1)创建新节点
createDocumentFragment() //创建一个DOM片段
createElement() //创建一个具体的元素
createTextNode() //创建一个文本节点
(2)添加、移除、替换、插入
appendChild()
removeChild()
replaceChild()
insertBefore()
(3)查找
getElementsByTagName() //通过标签名称
getElementsByName() //通过元素的Name属性的值
getElementById() //通过元素Id,唯一性
40.箭头函数和普通函数
箭头函数(Arrow Function)是 ES6 引入的一种新的函数语法,相比普通函数(也称为命名函数表达式),箭头函数具有以下特点:
- 语法简洁:箭头函数的语法更加简洁,可以通过去除 function 关键字和使用箭头符号(=>)来定义函数。
- 自动绑定 this:箭头函数没有自己的 this 绑定,而是继承外层作用域中的 this 值。这意味着在箭头函数内部使用的 this 指向的是定义箭头函数时的上下文对象,而不是函数被调用时的上下文对象。
- 没有 arguments 对象:箭头函数内部没有 arguments 对象,但可以使用剩余参数(rest parameter)来获取传入的所有参数。
- 不能作为构造函数:箭头函数不能被用作构造函数,也就是不能使用 new 运算符实例化一个类。
- 没有原型:由于箭头函数不能创建自己的 this 值和 arguments 对象,它们也没有 prototype 对象。
普通函数则是传统的 JavaScript 函数定义方式,使用 function 关键字来声明函数。普通函数可以作为构造函数使用(通过 new 运算符实例化对象),并且拥有自己的 this 值和 arguments 对象。
下面是一个使用箭头函数和普通函数实现相同功能的示例:
// 箭头函数
const arrowFunction = (a, b) => {return a + b;
};// 普通函数
function normalFunction(a, b) {return a + b;
}console.log(arrowFunction(2, 3)); // 输出: 5
console.log(normalFunction(2, 3)); // 输出: 5
需要注意的是,使用箭头函数时需要注意 this 的指向和上下文对象的继承关系。在某些情况下,箭头函数可能会更方便和简洁;而在涉及到动态 this 绑定、原型方法等特殊需求的情况下,普通函数可能更适合使用。
41.ajax状态码
0 - (未初始化)还没有调用send()方法
1 - (载入)已调用send()方法,正在发送请求
2 - (载入完成)send()方法执行完成,已经接收到全部响应内容
3 - (交互)正在解析响应内容
4 - (完成)响应内容解析完成,可以在客户端调用了
42.web安全和防护
- 跨站脚本攻击(XSS):
- 防护措施:在输出到HTML页面之前对用户输入进行正确的编码和过滤,使用内容安全策略(CSP)限制可信任的内容来源。
- SQL注入攻击:
- 防护措施:使用参数化查询或预处理语句来构建数据库查询,避免直接拼接用户输入作为查询条件。
- 跨站请求伪造(CSRF):
- 防护措施:实施CSRF令牌验证,在关键操作中要求用户提供有效的令牌以验证其请求的合法性。
- 文件上传漏洞:
- 防护措施:对上传的文件进行严格的文件类型验证和文件内容检查,并将文件存储在安全的位置。
- 不安全的身份认证与会话管理:
- 防护措施:使用安全的密码哈希算法存储密码,实施多因素身份验证,确保会话标识符的安全传输和存储,定期更新会话密钥。
- 恶意爬虫和数据挖掘:
- 防护措施:使用验证码防止恶意爬虫程序自动填充表单或爬取网站数据。
- 服务器安全漏洞:
- 防护措施:定期更新服务器软件和框架,保持安全补丁的最新状态;限制和审查服务器上的文件和目录访问权限。
- DDos攻击(分布式拒绝服务攻击):
- 防护措施:使用网络防火墙、入侵检测系统(IDS)和入侵防御系统(IPS)来监测和阻止来自恶意IP地址的大量请求。
- 敏感信息泄露:
- 防护措施:对敏感数据进行加密存储;最小化收集个人身份信息,并仅在必要时处理;合规相关的数据保护措施,如GDPR等。
43.js输出的方式
使用 alert () 函数来弹出提示框;
使用 confirm () 函数来弹出一个对话框;
使用 document.write () 方法将内容写入到 HTML 文档中;
使用 innerHTML 将内容写入到 HTML 标签中;
使用 console.log () 在浏览器的控制台输出内容。
document.write () 和 innerHTML 区别
1.inner HTML
对于inner HTML,w3c给出的解释是:
inner HTML可以写入(改变)HTML元素内容
2.document.write()
对于document.write(),w3c给出的解释是
两者都是对HTML页面进行输出,但大多数情况下优先考虑inner HTML。
两者有主要区别:
inner HTML将内容写入某个DOM节点,document.write()直接输出内容(页面加载时) 或清除整个HTML页面,打开新的页面输出document.write()(页面加载后)
44.判断img加载完毕
- 1.onLoad 测试,所有浏览器都显示出了“loaded”,说明所有浏览器都支持img的load事件。
- readystatechange事件 readyState为complete和loaded则表明图片已经加载完毕。测试IE6-IE10支持该事件,其它浏览器不支持。
- img的complete属性 轮询不断监测img的complete属性,如果为true则表明图片已经加载完毕,停止轮询。该属性所有浏览器都支持。
45.封装轮播图
实现一个简单的 JavaScript 轮播图可以按照以下思路进行封装:
- 创建 HTML 结构:在 HTML 文件中创建一个容器元素用于显示轮播图,以及相应的前后切换按钮。
- 样式设计:使用 CSS 对轮播图容器和图片进行样式设置,包括尺寸、布局、动画效果等。
- 获取 DOM 元素:使用 JavaScript 获取轮播图容器元素和相关的按钮元素,并保存为变量。
- 数据准备:准备存储轮播图图片数据的数组,每个数据项包含图片路径、标题等信息。
- 初始化:编写一个初始化函数,在页面加载时执行,将首张图片显示在轮播图容器中,并绑定按钮的点击事件处理函数。
- 切换功能:编写一个函数用于切换图片显示,该函数需要根据当前显示的图片索引,更新轮播图容器内的图片和标题内容。
- 自动播放:编写一个函数用于自动播放轮播图,该函数通过定时器调用切换功能函数,实现自动切换图片。
- 添加事件:给切换按钮添加点击事件,分别调用上一张和下一张切换功能函数。
46.如何判断css3动画结束
transitionEnd事件
ransitionEnd事件会在CSS transition动画结束后触发。
47.prototype和proto区别,关系
在 JavaScript 中,prototype
和 __proto__
是两个与对象原型(prototype)相关的属性。它们之间有一些区别和关系。
prototype
属性:prototype
是函数对象特有的属性,用于定义或引用一个对象的原型。- 在创建函数时,默认会为该函数创建一个名为
prototype
的空对象,并将其赋值给函数的prototype
属性。 - 通过修改函数的
prototype
属性,可以添加方法和属性到所有使用该函数创建的对象实例的原型链上。 - 可以通过
new
关键字调用函数创建新对象时,将该函数的prototype
对象作为新对象的原型。
__proto__
属性:__proto__
是每个 JavaScript 对象都具有的隐藏属性,用于指向该对象的原型链上的原型(即它的构造函数的prototype
)。- 它提供了一种访问对象原型的方式,但是并不是标准的属性,不应直接使用它,而是通过
Object.getPrototypeOf(obj)
或obj.constructor.prototype
来获取对象的原型。 __proto__
属性在现代 JavaScript 中被废弃,推荐使用上述的方法来获取对象原型。
关系:
- 对象的
__proto__
属性指向对象的原型,即该对象的构造函数的prototype
属性指向的对象。 - 通过这种原型链的关系,对象可以访问和继承原型链上的方法和属性。
prototype
和__proto__
属性共同构成了 JavaScript 对象之间的原型继承机制。
简而言之,prototype
是函数对象特有的属性,用于定义对象的原型,而 __proto__
是每个对象都有的属性,用于指向对象的原型链上的原型。
48.单词首字母大写
在javascript中,可以使用slice()方法、toUpperCase()方法和toLowerCase()方法来设置首字母大写,确保字符串的首字母都大写,其余部分小写。
步骤:
● 使用slice()方法将字符串分成两部分:首字母字符部分,和其他子字符部分。
● 使用toUpperCase()方法将首字母转换为大写;使用toLowerCase()将其他子字符转换为小写。
● 使用“+”运算符,将两个部分重新拼接起来
function titleCase(str) {
newStr = str.slice(0,1).toUpperCase() +str.slice(1).toLowerCase();return newStr;
}
titleCase("hello World!");
49.promise手写
下面是一个手写的简化版 Promise 的实现,以帮助你理解 Promise 的原理:
class MyPromise {constructor(executor) {this.state = 'pending'; // Promise 的初始状态为 pendingthis.value = undefined; // Promise 的最终结果或错误信息this.callbacks = []; // 存储回调函数的数组const resolve = (value) => {if (this.state === 'pending') {this.state = 'fulfilled'; // 状态变为 fulfilledthis.value = value; // 存储结果值this.executeCallbacks(); // 执行所有成功的回调函数}};const reject = (reason) => {if (this.state === 'pending') {this.state = 'rejected'; // 状态变为 rejectedthis.value = reason; // 存储错误信息this.executeCallbacks(); // 执行所有失败的回调函数}};try {executor(resolve, reject); // 执行执行器函数,并传入 resolve 和 reject 函数} catch (error) {reject(error); // 如果执行器函数抛出异常,则将 Promise 状态设为 rejected}}then(onFulfilled, onRejected) {return new MyPromise((resolve, reject) => {const callback = {onFulfilled,onRejected,resolve,reject};// 根据 Promise 当前的状态,决定是立即执行回调还是存储到数组中待执行if (this.state === 'fulfilled') {this.executeCallback(callback);} else if (this.state === 'rejected') {this.executeCallback(callback);} else {this.callbacks.push(callback);}});}executeCallbacks() {this.callbacks.forEach((callback) => {this.executeCallback(callback);});this.callbacks = []; // 清空回调函数数组}executeCallback(callback) {const { onFulfilled, onRejected, resolve, reject } = callback;if (this.state === 'fulfilled') {if (typeof onFulfilled === 'function') {try {const result = onFulfilled(this.value); // 执行成功的回调函数resolve(result); // 将成功结果传递给下一个 Promise} catch (error) {reject(error); // 如果回调函数抛出异常,则将 Promise 状态设为 rejected}} else {resolve(this.value); // 如果没有成功的回调函数,则直接将结果传递给下一个 Promise}} else if (this.state === 'rejected') {if (typeof onRejected === 'function') {try {const result = onRejected(this.value); // 执行失败的回调函数resolve(result); // 将成功结果传递给下一个 Promise} catch (error) {reject(error); // 如果回调函数抛出异常,则将 Promise 状态设为 rejected}} else {reject(this.value); // 如果没有失败的回调函数,则直接将错误信息传递给下一个 Promise}}}catch(onRejected) {return this.then(null, onRejected);}
}
这个简化版的 Promise 实现了基本的异步处理和链式调用的功能。它可以通过 new MyPromise(executor)
来创建一个 Promise 对象,其中 executor
是一个函数,它接受两个参数 resolve
和 reject
。在执行器函数中,你可以调用 resolve(value)
来表示成功,并传递一个结果值给下一个 Promise;调用 reject(reason)
来表示失败,并传递一个错误信息给下一个 Promise。
Promise 的 then
方法用于注册成功和失败的回调函数,并返回一个新的 Promise 对象,以
50.for in 和 for of区别
for in
更适合遍历对象,当然也可以遍历数组,但是会存在一些问题,
for of
遍历的是数组元素值,而且for of
遍历的只是数组内的元素,不包括原型属性和索引
区别
for in
遍历的是数组的索引(即键名),而for of
遍历的是数组元素值
for in
总是得到对象的key
或数组、字符串的下标
for of
总是得到对象的value
或数组、字符串的值
51.null和undefined区别
null
值 null
是一个字面量,不像 undefined
,它不是全局对象的一个属性。null
是表示缺少的标识,指示变量未指向任何对象。把 null
作为尚未创建的对象,也许更好理解。在 API 中,null
常在返回类型应是一个对象,但没有关联的值的地方使用。
undefined
undefined
是 全局对象 的一个属性。也就是说,它是全局作用域的一个变量。undefined
的最初值就是原始数据类型 undefined
。
52.call和apply和bind区别?
bind、call 和 apply 都是用来改变函数中 this 指向的方法。
其中,call 和 apply 的作用相同,都是立即调用函数,只是传参的方式略有不同:
call(fn, arg1, arg2, …): 将函数以指定的对象(也就是 this)调用,并且把函数参数逐个列举出来
apply(fn, [argsArray]): 将函数以指定的对象(也就是 this)调用,并且把函数的参数放到一个数组中作为参数传入
而 bind 则不是立即执行函数,而是创建一个新函数,将原函数中的 this 绑定到指定对象上,并返回一个新的函数。新函数被调用时,它会以绑定的 this 对象作为上下文对象,以原函数的形式被调用,后面加上绑定的参数。
实现一个 bind 方法的思路如下:
返回一个函数。
函数内部使用 apply 或 call 改变函数执行时的 this 值,同时参数也要和绑定函数保持一致。
考虑到返回的函数可能存在构造函数的情况,需要判断绑定函数是否是用 new 关键字进行实例化的,如果是则返回当前绑定函数的实例,否则返回所需函数的执行结果。
代码实现如下:
Function.prototype.myBind = function (context) {if (typeof this !== "function") {throw new TypeError("只有函数才能调用 bind 方法");}var self = this;var args = Array.prototype.slice.call(arguments, 1);var fNOP = function() {}var fBound = function() {var bindArgs = Array.prototype.slice.call(arguments);return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));}fNOP.prototype = this.prototype;fBound.prototype = new fNOP();return fBound;
}
这样我们就可以使用 myBind 来代替原生的 bind 方法了。
53.ES6对Array数组类型做了很多升级优化,以下是一些常用的:
- 扩展运算符(Spread Operator): 可以将数组展开为号分隔的参数序列,方便地将一个数组复制到另一个数组中。
- Array.from()方法: 可以将类数组对象或可迭代对象转换为真正的
- Array.of()方法: 可以创建一个包含任意数量参数的新数组
- find()和findindex()方法: 可以在数组中查找符合条件的元素,并返回该元素或其索引。
- includes()方法: 可以判断数组中是否包含某个元素
- fill()方法: 可以用指定的值填充数组中的所有元素
- fat()和flatMap()方法: 可以将多维数组转换为一维数组,并且可以对数组中的每个元素执行一个函数。
- sort()方法的升级: 可以通过传递一个比较函数来实现更复杂的排序。
- forEach()方法的升级: 可以通过传递一个可选的上下文参数来指定回调函数中的this值。
- map()方法的升级: 可以通过传递一个可选的上下文参数来指定回调函数中的this值.
54.虚拟列表如何实现
虚拟列表(Virtual List)是一种优化技术,用于处理大规模数据的列表展示。它通过动态加载和渲染可视区域内的数据项,而不是一次性加载所有数据,从而节省内存和提高性能。
实现虚拟列表可以遵循以下步骤:
- 确定列表容器的高度:首先,确定列表容器的高度,即可视区域的高度。这个高度应该能够容纳列表中可见的数据项。
- 渲染可见的数据项:根据可视区域的高度和每个数据项的高度,计算出可见数据项的数量。只渲染这些可见的数据项到列表容器中。
- 监听滚动事件:添加滚动事件监听器,以便在滚动时检测可视区域的位置变化。
- 动态加载数据:根据滚动位置和可视区域的高度,计算出当前应该显示的数据范围。当滚动到需要加载新数据时,使用异步方式获取新数据,并将其追加到列表容器中。
- 虚拟化元素:为了减少内存占用,可以采用对象池或DOM重用等技术,使列表容器中仅保持有限数量的元素,随着滚动,对已超出可视区域的元素进行回收和重用。
实现虚拟列表通常需要依赖特定的前端框架或库,不同的框架和库可能有不同的具体实现方式。例如,在React中,可以使用react-virtualized、react-window、react-infinite-scroll等库来实现虚拟列表。
55.轮询(有哪些,优缺点)
轮询(Polling)是一种常用的网络通信方式,用于定期查询服务器或其他设备上是否有新数据或状态更新。在轮询中,客户端以固定的时间间隔发送请求给服务器,并等待服务器返回最新的数据。
以下是轮询的几种常见形式和对应的优缺点:
- 短轮询(Fixed Interval Polling):客户端按照固定的时间间隔发送请求给服务器。
- 优点:容易实现,适用于简单的场景,可以实时获取数据变化。
- 缺点:频繁的请求可能导致服务器负载增加,即使没有新数据也会产生不必要的网络流量和延迟。
- 长轮询(Long Polling):客户端发送请求给服务器,服务器保持连接并等待有新数据时返回响应,否则在一段时间后返回空响应,客户端再次发起请求。
- 优点:相比定时轮询,减少了无效请求次数,能够更快地响应数据变化。减少了网络流量和延迟。
- 缺点:实现较为复杂,需要服务器支持长连接。服务器端资源占用较高,在大规模应用中可能会出现性能问题。
- WebSockets 轮询:使用 WebSocket 技术建立客户端与服务器之间的双向通信。
- 优点:实时性好,能够实现服务器向客户端主动推送数据。比长轮询更加高效,减少了无效请求次数和网络开销。
- 缺点:相对于传统 HTTP 请求,实现和维护成本较高,需要特殊的服务器支持。
- Coment 轮询:利用 HTML 页面中广泛支持的 Comet 技术,通过隐藏的长连接或隐式帧技术来模拟双向通信。
- 优点:能够实现服务器向客户端主动推送数据,在浏览器中有较好的兼容性。
- 缺点:相对于 WebSocket,实现和维护成本较高,效率较低。
总体而言,轮询是一种简单粗暴的实时数据获取方式,适用于小规模数据更新,并且在一定程度上可以满足实时性要求。然而,由于其频繁的请求和服务器资源消耗,不适用于大规模应用或需要高效、实时通信的场景。在需要更高效的数据推送和实时通信需求时,可以考虑使用 WebSockets 或其他基于事件驱动的技术。
56.作用域分为几种
作用域(Scope)指的是在程序中定义变量时,该变量所存在的可访问范围。作用域可以分为以下几种类型:
- 全局作用域(Global Scope):全局作用域是在整个程序中都可访问的作用域,通常在程序的最外层定义的变量拥有全局作用域。在任何地方都可以引用和修改这些全局变量。
- 局部作用域(Local Scope):局部作用域是在特定代码块(如函数、循环、条件语句等)内定义的作用域。在局部作用域中声明的变量只能在其所属的代码块内被访问,超出该范围则无法访问。
- 块级作用域(Block Scope):块级作用域是在代码块(由一对花括号{}包围)中定义的作用域。在ES6之前,JavaScript中没有块级作用域,而是通过函数作用域来管理变量。ES6引入了let和const关键字,使得在代码块中也可以创建具有块级作用域的变量。
总结起来,作用域分为全局作用域、局部作用域和块级作用域。全局作用域中的变量在整个程序中都可访问,局部作用域中的变量仅在其所属的代码块内可访问,而块级作用域则是ES6引入的概念,使得在代码块中也可以创建具有作用域限制的变量。
作用域链
作用域链(Scope Chain)是指在程序中查找变量时所形成的嵌套作用域的链式结构。当访问一个变量时,JavaScript引擎会按照作用域链的顺序从内部向外部逐级查找对应的变量。
作用域链的形成过程如下:
- 当执行一个函数时,会创建一个新的执行环境(Execution Context)。
- 每个执行环境都包括一个变量对象(Variable Object),其中存储着该函数内部定义的变量和函数声明。
- 在创建执行环境时,JavaScript引擎会将该执行环境的变量对象添加到作用域链的前端。
- 如果在当前执行环境中无法找到需要的变量,JavaScript引擎会沿着作用域链继续向上查找,直至找到全局作用域。
- 如果在全局作用域中依然找不到目标变量,那么变量就会被认定为未定义。
通过作用域链,内部函数可以访问外部函数的变量,但外部函数无法访问内部函数的变量。这种嵌套的作用域链机制实现了变量的作用域和封闭性,确保代码块之间的变量不会相互干扰。
57.给某个资源的链接,如 https://www.baidu.com/index.html ,请实现一个方法,获取该资源的后缀,如 html
endsWith
spalice
58.map循环和for循环谁的效率高
在JavaScript中,map循环和for循环各有其适用的场景,并且它们在效率方面没有明显的优劣之分,因为它们的实现机制不同。
- map循环:map()是数组的一个高阶函数,使用它可以对数组中的每个元素进行遍历并进行特定操作,最后返回一个新的数组。它是基于回调函数实现的,可以提供一个简洁的语法来处理数据,并且具有函数式编程的特性。 由于map循环是在内部通过回调函数对数组元素逐个处理,因此适用于对数组进行转换、映射或过滤等操作,并且代码更加简洁易读。
- for循环:for循环是一种传统的迭代方式,通过定义循环变量进行控制,可以根据需要自由地对循环进行定制。它适用于需要对数组执行复杂的条件判断或涉及到索引的操作。 相比于map循环,for循环在运行时对性能的影响较小,因为它没有额外的函数调用开销。同时,对于大型数组或性能要求较高的场景,手动使用for循环可能会更有效率。
总结来说,map循环更适用于数组的转换、映射或过滤等操作,代码简洁易读;而for循环更适用于对数组进行复杂的条件判断或索引操作,并且在性能方面可能会略优于map循环。选择使用哪种方式应根据具体的需求和代码上下文来决定。
59.js进制转化在JavaScript中,可以使用内置的方法来进行进制转换。下面是几个常用的进制转换方法:
-
十进制转其他进制:可以使用
toString()
方法,接受一个参数表示目标进制的基数。例如,将十进制数13转换为二进制和十六进制:javascript复制代码let decimalNumber = 13; let binaryString = decimalNumber.toString(2); // 转换为二进制 let hexadecimalString = decimalNumber.toString(16); // 转换为十六进制console.log(binaryString); // 输出: "1101" console.log(hexadecimalString); // 输出: "d"
-
其他进制转十进制:可以使用
parseInt()
方法,接受两个参数,第一个参数是要转换的字符串,第二个参数是当前字符串的进制。例如,将二进制数1101和十六进制数d转换为十进制:javascript复制代码let binaryString = "1101"; let hexadecimalString = "d";let decimalFromBinary = parseInt(binaryString, 2); // 从二进制转换为十进制 let decimalFromHexadecimal = parseInt(hexadecimalString, 16); // 从十六进制转换为十进制console.log(decimalFromBinary); // 输出: 13 console.log(decimalFromHexadecimal); // 输出: 13
以上是在JavaScript中进行进制转换的常用方法。可以根据需要选择适合的方法来实现相应的转换。
60.map和filter的区别
在 JavaScript 中,map()
和 filter()
都是数组的高阶函数,用于处理数组元素并返回新的数组。它们的区别如下:
map()
函数:- 作用:
map()
函数会将原数组的每个元素都应用到一个回调函数,并返回一个新的数组,该数组的元素是原数组经过回调函数处理后的结果。 - 返回值:返回一个新的数组,长度与原数组相同。
- 使用场景:适用于需要对数组中的每个元素进行转换、映射的情况。
- 作用:
filter()
函数:- 作用:
filter()
函数会对原数组中的每个元素应用一个判断条件(回调函数),返回满足条件的元素所组成的新数组。 - 返回值:返回一个新的数组,其中包含使回调函数返回 true 的元素。
- 使用场景:适用于需要根据某个条件筛选出符合条件的元素的情况。
- 作用:
以下是使用 map()
和 filter()
的示例:
// 使用 map() 转换数组元素
let numbers = [1, 2, 3, 4, 5];
let squaredNumbers = numbers.map(num => num * num);
console.log(squaredNumbers); // 输出: [1, 4, 9, 16, 25]// 使用 filter() 筛选数组元素
let evenNumbers = numbers.filter(num => num % 2 === 0);
console.log(evenNumbers); // 输出: [2, 4]
在上述示例中,map()
函数将数组 numbers
中的每个元素都平方并返回一个新数组 squaredNumbers
。而 filter()
函数则从 numbers
中筛选出所有的偶数,并返回一个新数组 evenNumbers
。
总结一下,map()
和 filter()
的区别在于:
map()
将每个元素进行转换或映射,并返回一个由结果组成的新数组。filter()
根据条件对数组进行筛选,返回满足条件的元素所组成的新数组。
61.如何顺序执行10个异步任务
要顺序执行异步任务,可以使用异步编程技术,如回调函数、Promise、async/await 等。以下是使用 Promise 和 async/await 来顺序执行 10 个异步任务的示例:
使用 Promise:
function asyncTask(index) {return new Promise(resolve => {// 模拟异步操作,这里使用 setTimeout 延迟执行setTimeout(() => {console.log(`Async Task ${index} completed`);resolve();}, Math.random() * 1000);});
}function runTasksSequentially() {let promise = Promise.resolve();for (let i = 1; i <= 10; i++) {promise = promise.then(() => asyncTask(i));}
}runTasksSequentially();
使用 async/await:
function asyncTask(index) {return new Promise(resolve => {setTimeout(() => {console.log(`Async Task ${index} completed`);resolve();}, Math.random() * 1000);});
}async function runTasksSequentially() {for (let i = 1; i <= 10; i++) {await asyncTask(i);}
}runTasksSequentially();
上述示例中,asyncTask()
函数表示一个异步任务,它返回一个 Promise 对象。通过使用 Promise 的链式调用或 async/await 的方式,循环依次执行这些异步任务,保证它们按顺序执行。
无论是使用 Promise 还是 async/await,都会依次执行异步任务,并在每个任务完成后打印相应的输出。请注意,示例中的异步操作使用 setTimeout
来模拟,实际应用中的异步操作可以是网络请求、读写文件等。