1.简述JavaScript 中map 和forEach 区别?
-
返回值:
map会返回一个新数组,包含对原数组每个元素处理后的结果;forEach不返回任何值(返回undefined),仅用于遍历数组。 -
用途:
map适用于需要转换数组元素的场景;forEach适合单纯执行遍历操作,如打印或修改原数组元素。 -
链式调用:由于
map返回新数组,支持后续方法链式调用;forEach不支持这种方法链。 -
性能:在仅需遍历的情况下,
forEach通常比map性能略优,因为不需要创建新数组。
2.解释下JavaScript 中this 是如何工作的?
基本概念
在 JavaScript 中,this 是一个特殊的关键字,它在函数被调用时自动绑定,指向当前执行上下文中的对象。this 的指向取决于函数的调用方式,而不是定义位置。
this 的绑定规则
1. 默认绑定(独立函数调用)
当函数作为普通函数被调用时,this 默认指向全局对象(浏览器中是 window,Node.js 中是 global)。
function showThis() {
console.log(this); // 浏览器中输出 window 对象
}
showThis();
注意:在严格模式下('use strict'),默认绑定的 this 会是 undefined。
2. 隐式绑定(方法调用)
当函数作为对象的方法被调用时,this 指向调用该方法的对象。
const person = {
name: 'Alice',
greet: function() {
console.log(`Hello, I'm ${this.name}`);
}
};
person.greet(); // 输出 "Hello, I'm Alice" - this 指向 person 对象
3. 显式绑定(call/apply/bind)
使用 call(), apply() 或 bind() 可以显式地设置 this 的值。
function introduce(language) {
console.log(`I code in ${language} and my name is ${this.name}`);
}
const developer = { name: 'Bob' };
// 使用 call
introduce.call(developer, 'JavaScript'); // this 指向 developer 对象
// 使用 apply
introduce.apply(developer, ['Python']); // this 指向 developer 对象
// 使用 bind
const boundIntroduce = introduce.bind(developer);
boundIntroduce('Java'); // this 永久绑定到 developer 对象
4. new 绑定(构造函数调用)
使用 new 操作符调用构造函数时,this 指向新创建的对象实例。
function Person(name) {
this.name = name;
this.greet = function() {
console.log(`Hello from ${this.name}`);
};
}
const alice = new Person('Alice');
alice.greet(); // 输出 "Hello from Alice" - this 指向 alice 实例
特殊情况的 this
箭头函数中的 this
箭头函数没有自己的 this,它会捕获所在上下文的 this 值。
const obj = {
name: 'Object',
traditionalFunc: function() {
console.log(this.name); // this 指向 obj
},
arrowFunc: () => {
console.log(this.name); // this 指向外层上下文(可能是 window)
}
};
obj.traditionalFunc(); // 输出 "Object"
obj.arrowFunc(); // 可能输出 undefined(取决于外层上下文)
事件处理函数中的 this
在 DOM 事件处理函数中,this 通常指向触发事件的元素。
document.getElementById('myButton').addEventListener('click', function() {
console.log(this); // this 指向被点击的按钮元素
});
回调函数中的 this
回调函数中的 this 可能会丢失原绑定,常见解决方案:
- 使用箭头函数保留外层
this - 使用
bind()显式绑定 - 在回调外保存
this引用
class Timer {
constructor() {
this.seconds = 0;
// 解决方案1:使用箭头函数
setInterval(() => {
this.seconds++;
console.log(this.seconds);
}, 1000);
// 解决方案2:使用 bind
// setInterval(this.tick.bind(this), 1000);
}
tick() {
this.seconds++;
console.log(this.seconds);
}
}
总结
理解 JavaScript 中的 this 需要掌握以下几点:
this的指向取决于函数的调用方式- 四种基本绑定规则:默认、隐式、显式和 new 绑定
- 箭头函数不绑定自己的
this - 在回调或事件处理等场景中,
this可能会意外改变,需要特别注意
通过实践和掌握这些规则,可以更准确地预测和控制 JavaScript 代码中 this 的行为。
3.简述异步线程,轮询机制,宏任务微任务?
异步线程
JavaScript 是单线程语言,但通过异步线程(如浏览器的网络请求线程、定时器线程)实现非阻塞操作。异步任务(如setTimeout、fetch)由其他线程处理,完成后将回调函数推入任务队列,等待主线程调用。
轮询机制
事件循环(Event Loop)通过轮询机制管理任务执行:
- 执行栈:优先执行同步代码。
- 任务队列:异步任务完成后,回调函数按类型(宏任务/微任务)进入对应队列。
- 轮询顺序:
- 执行完当前宏任务后,立即清空微任务队列。
- 微任务执行完毕后再取下一个宏任务
先执行同步任务,在执行异步任务:而又把异步任务分为宏任务和微任务(先微任务,再宏任务)
注意 Promise本身是同步的,new Promise() 是同步方法,resolve才是异步方法。Promise是异步的,是指他的then()和catch()方法,
常见同步任务类型
普通函数
console.log
函数调用
常见的异步任务类型:
宏任务
setTimeout/setIntervalDOM事件(点击、滚动)I/O操作(文件读写、网络请求)
微任务
-
Promise.then -
Promise.catch -
Promise.finally -
process.nextTick(Node.js 环境) -
queueMicrotask(在现代浏览器中可用) -
async/await本质上是基于Promise的,因此它属于微任务。当await后面的Promise完成时,await之后的代码会被放入微任务队列中,等待当前宏任务执行完毕后执行。
4.javascipt阻止事件冒泡的方法?
在JavaScript中,阻止事件冒泡的常用方法包括:
-
event.stopPropagation()方法
这是最常用的方式,调用该方法可阻止事件继续向上传播 -
return false语句
在jQuery事件处理函数中使用时,相当于同时调用event.stopPropagation()和event.preventDefault() -
event.stopImmediatePropagation()方法
不仅阻止事件冒泡,还会阻止同一元素上其他事件处理函数的执行 -
在事件处理函数中设置
event.cancelBubble = true
这是较老的IE浏览器支持的方法,现代浏览器也兼容
推荐优先使用标准的event.stopPropagation()方法,它具有最好的浏览器兼容性和明确的语义。
5.JavaScript 阻止默认事件的方法?
在 JavaScript 中,可以通过以下方式阻止事件的默认行为:
-
event.preventDefault()
这是最常用的方法,调用该方法即可阻止事件的默认行为。 -
return false
在事件处理函数中直接返回 false(仅适用于 DOM0 级事件绑定)。 -
event.stopPropagation()
虽然主要作用是阻止事件冒泡,但也能间接阻止默认行为。
示例代码:
document.querySelector('a').addEventListener('click', function(e) {
e.preventDefault(); // 阻止链接跳转
});
注意:某些事件(如 scroll)的默认行为无法被阻止。
6.JavaScript 中怎么判断array 和 object ?
在 JavaScript 中,可以通过以下方法判断变量是否为数组或对象:
- 判断数组:
Array.isArray(value) // 推荐方法
value instanceof Array
Object.prototype.toString.call(value) === '[object Array]'
- 判断普通对象:
Object.prototype.toString.call(value) === '[object Object]'
typeof value === 'object' && value !== null && !Array.isArray(value)
注意:
- 使用 typeof 检查对象时会返回 'object',但 null 也会返回 'object'
- 数组本质上也是对象,所以需要优先判断是否为数组
7.简述JavaScript 盒子模型?
盒子模型是CSS中用于描述元素布局的基本概念,在JavaScript中我们可以通过DOM API来获取和操作这些盒子属性。每个HTML元素都可以看作是一个矩形的盒子,这个盒子由内到外依次包含:
- 内容区域(content) - 显示实际内容的部分
- 内边距(padding) - 内容与边框之间的透明区域
- 边框(border) - 围绕内边距和内容的边框
- 外边距(margin) - 盒子与其他元素之间的透明区域
盒子模型类型
在CSS中,盒子模型有两种类型:
-
标准盒子模型(默认)
- 元素的总宽度 = width + padding + border + margin
- 元素的总高度 = height + padding + border + margin
-
IE盒子模型(怪异模式)
- 元素的总宽度 = width(包含padding和border) + margin
- 元素的总高度 = height(包含padding和border) + margin
可以通过CSS的box-sizing属性来切换盒子模型类型:
box-sizing: content-box(标准盒子模型)box-sizing: border-box(IE盒子模型)
JavaScript中获取盒子模型属性
在JavaScript中,我们可以使用以下DOM属性来获取元素的盒子模型信息:
-
clientWidth/clientHeight
- 包含内容区域和内边距,但不包含边框和外边距
- 计算公式:width/height + padding
-
offsetWidth/offsetHeight
- 包含内容区域、内边距、边框,但不包含外边距
- 计算公式:width/height + padding + border
-
clientTop/clientLeft
- 表示元素上边框和左边框的宽度(只读)
-
scrollWidth/scrollHeight
- 包含元素的内容宽度/高度,包括由于溢出而不可见的部分
- 如果内容没有溢出,则等于clientWidth/clientHeight
实际应用示例
// 获取元素的盒子模型信息
const element = document.getElementById('example');
console.log('clientWidth:', element.clientWidth);
console.log('clientHeight:', element.clientHeight);
console.log('offsetWidth:', element.offsetWidth);
console.log('offsetHeight:', element.offsetHeight);
// 修改元素的内边距
element.style.padding = '20px';
// 使用getBoundingClientRect()获取更详细的盒子信息
const rect = element.getBoundingClientRect();
console.log('元素相对于视口的位置:', rect.top, rect.right, rect.bottom, rect.left);
console.log('元素的宽度和高度:', rect.width, rect.height);
注意事项
- 所有尺寸属性返回的都是数字(像素值),不带单位
- 这些属性都是只读的(除了通过style对象修改)
- 使用
getBoundingClientRect()方法可以获取元素相对于视口的位置信息 - 在动画和响应式布局中,正确理解盒子模型尤为重要
理解JavaScript盒子模型对于精确控制页面元素的布局和交互至关重要,特别是在开发动态网页和响应式设计时。
8.javascipt 中对象的key 能是数字吗?
在 JavaScript 中,对象的 key 可以是数字,但需要注意以下几点:
-
数字作为 key 时会被自动转换为字符串类型
- 例如:
{1: 'one'}实际上会被转换为{'1': 'one'} - 可以使用
typeof Object.keys(obj)[0]验证 key 的类型
- 例如:
-
访问数字 key 的方式:
- 点表示法:
obj.1是无效的(会报语法错误) - 必须使用方括号表示法:
obj[1]或obj['1']
- 点表示法:
-
实际应用场景:
const statusCodes = { 200: 'OK', 404: 'Not Found', 500: 'Server Error' }; console.log(statusCodes[404]); // 输出 "Not Found" -
特殊注意事项:
- 当使用数字作为 key 时,对象属性的遍历顺序会有所不同
- 数字 key 会优先按照数值大小升序排列,然后才是字符串 key
- 例如:
const obj = { '2': 'two', '1': 'one', 'name': 'John' }; console.log(Object.keys(obj)); // 输出: ['1', '2', 'name']
-
与 Map 数据结构的区别:
- 如果需要严格保持数字类型作为 key,可以考虑使用 Map
- Map 可以保持 key 的原始数据类型
- 示例:
const map = new Map(); map.set(1, 'one'); console.log(map.get(1)); // 输出 "one"
-
JSON 序列化时的表现:
- 当对象被序列化为 JSON 时,数字 key 会被转换为字符串
- 例如:
JSON.stringify({1: 'one'}); // 返回 '{"1":"one"}'
因此,虽然 JavaScript 允许使用数字作为对象 key,但在实际使用中它们会被转换为字符串,并且访问方式上有所限制。
9.JavaScript 中 async await 和 promise 和 generator 有什么区别?
1. Promise
Promise 是 ES6 引入的异步编程解决方案,用于处理异步操作。它代表一个尚未完成但预期将来会完成的操作。
特点:
- 三种状态:pending(进行中)、fulfilled(已成功)、rejected(已失败)
- 状态一旦改变,就不会再变
- 通过
.then()和.catch()方法链式调用
示例代码:
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Data fetched');
}, 1000);
});
}
fetchData()
.then(data => console.log(data))
.catch(error => console.error(error));
应用场景:
- 处理 AJAX 请求
- 文件读取操作
- 任何需要等待异步操作完成的场景
2. async/await
async/await 是 ES2017 引入的语法糖,基于 Promise 实现,使异步代码看起来像同步代码。
特点:
async函数总是返回一个 Promiseawait只能在 async 函数中使用- 代码更简洁,错误处理更方便(可以使用 try/catch)
- 解决了 Promise 的链式调用可能出现的"回调地狱"问题
示例代码:
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Error:', error);
}
}
fetchData();
应用场景:
- 需要顺序执行的异步操作
- 需要更清晰错误处理的异步代码
- 需要将多个 Promise 组合使用的情况
3. Generator
Generator 是 ES6 引入的特殊函数,可以暂停和恢复执行,通过 yield 关键字实现。
特点:
- 使用
function*语法定义 - 通过
yield暂停执行并返回一个值 - 通过
.next()方法恢复执行 - 可以与 Promise 结合使用来处理异步操作
示例代码:
function* generatorFunction() {
yield 1;
yield 2;
return 3;
}
const generator = generatorFunction();
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
console.log(generator.next()); // { value: 3, done: true }
与 Promise 结合处理异步:
function* asyncGenerator() {
const result = yield new Promise(resolve =>
setTimeout(() => resolve('Async data'), 1000)
);
console.log(result);
}
const gen = asyncGenerator();
const promise = gen.next().value;
promise.then(data => gen.next(data));
应用场景:
- 需要手动控制执行流程的函数
- 实现自定义迭代器
- 在 async/await 出现前用于处理异步流程
三者的主要区别
| 特性 | Promise | async/await | Generator |
|---|---|---|---|
| 引入版本 | ES6 (2015) | ES2017 | ES6 (2015) |
| 语法 | .then()链式调用 | 类似同步代码的语法 | function* 和 yield |
| 错误处理 | .catch()方法 | try/catch块 | 需要手动处理 |
| 执行控制 | 自动执行 | 自动执行 | 手动控制 |
| 返回值 | Promise对象 | Promise对象 | Iterator对象 |
| 主要用途 | 处理异步操作 | 更优雅地处理异步 | 控制函数执行流程 |
async/await 实际上是 Generator 和 Promise 的语法糖,它结合了两者的优点,提供了更简洁的异步代码编写方式。在大多数现代JavaScript开发中,async/await 已成为处理异步操作的首选方式。
10.JavaScript 中手写promise? promise.all 和 promise.race 区别?
手写 Promise 基本实现
class MyPromise {
constructor(executor) {
this.state = 'pending';
this.value = undefined;
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = (value) => {
if (this.state === 'pending') {
this.state = 'fulfilled';
this.value = value;
this.onFulfilledCallbacks.forEach(fn => fn());
}
};
const reject = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected';
this.value = reason;
this.onRejectedCallbacks.forEach(fn => fn());
}
};
try {
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
then(onFulfilled, onRejected) {
if (this.state === 'fulfilled') {
onFulfilled(this.value);
} else if (this.state === 'rejected') {
onRejected(this.value);
} else {
this.onFulfilledCallbacks.push(() => onFulfilled(this.value));
this.onRejectedCallbacks.push(() => onRejected(this.value));
}
return this;
}
}
Promise.all 与 Promise.race 的区别
Promise.all
- 接收一个 Promise 数组作为参数
- 当所有 Promise 都成功时,返回一个包含所有结果的数组
- 如果任一 Promise 失败,立即返回失败原因
- 适合需要等待所有异步操作完成的场景
Promise.race
- 接收一个 Promise 数组作为参数
- 返回第一个完成(无论成功或失败)的 Promise 的结果
- 适合需要快速获取第一个可用结果的场景
// Promise.all 示例
Promise.all([promise1, promise2, promise3])
.then(values => console.log(values))
.catch(err => console.error(err));
// Promise.race 示例
Promise.race([promise1, promise2, promise3])
.then(value => console.log(value))
.catch(err => console.error(err));
11.JavaScript解释什么是工厂模式?有什么优缺点?
什么是工厂模式
工厂模式是一种创建型设计模式,它提供了一种创建对象的接口,但允许子类决定实例化哪个类。简单来说,工厂模式就是将创建对象的过程封装起来,客户端不需要知道具体创建细节,只需要通过工厂方法来获取所需对象。
基本实现示例
// 简单工厂模式示例
function createCar(type) {
switch(type) {
case 'sedan':
return new Sedan();
case 'suv':
return new SUV();
case 'truck':
return new Truck();
default:
throw new Error('Unknown car type');
}
}
// 使用工厂
const myCar = createCar('sedan');
工厂模式的优点
- 封装创建逻辑:将对象创建过程集中管理,客户端代码与具体类解耦
- 扩展性强:添加新产品时只需修改工厂类,无需修改客户端代码
- 代码复用:相同的创建逻辑可以在多个地方复用
- 降低耦合度:客户端只依赖于抽象接口,不依赖于具体实现
- 便于维护:当创建逻辑变更时,只需修改工厂类一处
工厂模式的缺点
- 增加系统复杂度:引入额外的工厂类会增加代码复杂度
- 违背开闭原则:简单工厂模式在添加新产品时需要修改工厂类
- 性能开销:多了一层工厂方法的调用,可能带来轻微性能损失
- 调试困难:由于对象创建过程被隐藏,调试时可能不太直观
应用场景
- 当创建逻辑复杂或需要集中管理时
- 当需要根据运行时条件动态创建不同对象时
- 当系统需要支持多种相似产品的扩展时
- 当想要减少客户端与具体类的依赖时
高级工厂模式变体
-
工厂方法模式:定义一个创建对象的接口,但让子类决定实例化哪个类
class VehicleFactory { createVehicle() { throw new Error('Abstract method'); } } class CarFactory extends VehicleFactory { createVehicle() { return new Car(); } } -
抽象工厂模式:提供一个接口,用于创建相关或依赖对象的家族,而不需要指定具体类
class GUIFactory { createButton() { throw new Error('Abstract method'); } createCheckbox() { throw new Error('Abstract method'); } } class WindowsFactory extends GUIFactory { createButton() { return new WindowsButton(); } // ... }
工厂模式在JavaScript框架中广泛应用,如React的createElement、Vue的组件工厂等,都是工厂模式的典型应用。
12.JavaScript 图片/ 文件夹上传到后台是什么类型?
-
FormData 对象(最常用):
- 专门用于表单数据(包括文件)的封装
- 支持多文件上传
- 示例:
formData.append('files', fileInput.files[0])
-
Blob 对象:
- 二进制大对象
- 适合单个文件传输
-
File 对象(继承自 Blob):
- 包含文件元数据(名称、类型等)
- 通常通过
<input type="file">获取
-
Base64 编码字符串:
- 将二进制数据转为 ASCII 字符串
- 适合小文件或需要内联的场景
后台接收时,对应的 Content-Type 通常是:
multipart/form-data(FormData)application/octet-stream(二进制流)
13.JavaScript 浅拷贝/ 深拷贝的区别?
浅拷贝:
- 仅复制对象的顶层属性
- 如果属性是引用类型,则复制引用地址而非实际值
- 修改引用类型属性会影响原对象
- 常用方法:Object.assign()、展开运算符(...)
深拷贝:
- 递归复制对象的所有层级
- 完全创建新的内存空间存储所有值
- 修改任何属性都不会影响原对象
- 常用方法:JSON.parse(JSON.stringify())、第三方库如lodash.cloneDeep()
核心区别在于对引用类型属性的处理方式:浅拷贝共享引用,深拷贝创建独立副本。
14.JavaScript 闭包是什么? 闭包形成的原因和闭包的用途?
JavaScript 闭包详解
什么是闭包?
闭包(Closure)是JavaScript中一个重要的概念,指的是有权访问另一个函数作用域中的变量的函数。简单来说,当一个内部函数引用了外部函数的变量时,就形成了闭包。
闭包的特点是:即使外部函数已经执行完毕,内部函数仍然可以访问外部函数的变量。
闭包的形成原因
闭包的形成主要基于JavaScript的两个特性:
- 词法作用域(Lexical Scoping):函数能够访问定义时所处作用域中的变量
- 垃圾回收机制:当一个变量被引用时,不会被垃圾回收机制回收
具体来说,当函数A内部定义了函数B,且函数B引用了函数A的变量时,即使函数A执行完毕,函数B仍然可以访问这些变量,这就形成了闭包。
闭包的用途
闭包在JavaScript中有多种实际应用:
1. 创建私有变量
function createCounter() {
let count = 0; // 私有变量
return {
increment: function() {
count++;
return count;
},
decrement: function() {
count--;
return count;
},
getCount: function() {
return count;
}
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount()); // 2
2. 实现函数柯里化
function multiply(a) {
return function(b) {
return a * b;
};
}
const multiplyByTwo = multiply(2);
console.log(multiplyByTwo(5)); // 10
3. 模块化开发
const calculator = (function() {
let memory = 0;
function add(a, b) {
return a + b;
}
function store(value) {
memory = value;
}
function recall() {
return memory;
}
return {
add,
store,
recall
};
})();
console.log(calculator.add(2, 3)); // 5
calculator.store(10);
console.log(calculator.recall()); // 10
4. 事件处理和回调
function setupButtons() {
for (var i = 1; i <= 5; i++) {
(function(index) {
document.getElementById('button' + index)
.addEventListener('click', function() {
console.log('Button ' + index + ' clicked');
});
})(i);
}
}
闭包的注意事项
- 内存泄漏风险:闭包会导致变量无法被垃圾回收,不当使用可能导致内存泄漏
- 性能影响:闭包比普通函数占用更多内存,频繁创建闭包可能影响性能
- 变量共享问题:在循环中创建闭包时需要注意变量共享问题(如上面的按钮示例)
15.JavaScript 跨域的解决方案有哪些?
-
JSONP:利用
<script>标签不受同源策略限制的特性,通过动态创建 script 标签请求跨域数据。 -
CORS(跨域资源共享):服务端设置
Access-Control-Allow-Origin等响应头来允许跨域请求。 -
代理服务器:通过同域的后端服务转发跨域请求,规避浏览器限制。
-
WebSocket:使用 WebSocket 协议进行跨域通信。
-
postMessage:通过
window.postMessageAPI 实现跨窗口通信。 -
document.domain:适用于主域相同、子域不同的情况,通过设置相同主域来允许跨子域访问。
-
Nginx 反向代理:通过 Nginx 配置转发请求实现跨域访问。
-
iframe 跨域:结合
window.name或location.hash实现跨域数据传输。
16.http 协议详解 http 请求方式有哪些?http响应状态码?
HTTP 请求方法
HTTP 协议定义了多种请求方法(也称为"HTTP 动词"),常见的有:
- GET:请求获取指定资源
- POST:向指定资源提交数据进行处理(通常会导致状态变化)
- PUT:替换指定资源的所有当前表示
- DELETE:删除指定资源
- HEAD:类似 GET 请求,但只返回响应头
- OPTIONS:返回服务器支持的 HTTP 方法
- PATCH:对资源进行部分修改
- CONNECT:建立隧道连接(用于 HTTPS)
- TRACE:回显服务器收到的请求(用于测试)
HTTP 响应状态码
HTTP 状态码由三位数字组成,分为五类:
-
1xx(信息性状态码):请求已被接收,继续处理
- 100 Continue
- 101 Switching Protocols
-
2xx(成功状态码):请求已成功处理
- 200 OK
- 201 Created
- 204 No Content
-
3xx(重定向状态码):需要进一步操作完成请求
- 301 Moved Permanently
- 302 Found
- 304 Not Modified
-
4xx(客户端错误状态码):请求包含语法错误或无法完成
- 400 Bad Request
- 401 Unauthorized
- 403 Forbidden
- 404 Not Found
-
5xx(服务器错误状态码):服务器处理请求失败
- 500 Internal Server Error
- 502 Bad Gateway
- 503 Service Unavailable
17.简述页面从发送http 请求到渲染页面的全过程?
- DNS解析:浏览器查询域名对应的IP地址
- 建立TCP连接:与服务器通过三次握手建立可靠连接
- 发送HTTP请求:浏览器向服务器发送请求报文
- 服务器处理请求:服务器接收并解析请求,准备响应数据
- 返回HTTP响应:服务器发送包含状态码和资源的响应报文
- 浏览器解析:解析HTML构建DOM树,解析CSS构建CSSOM树
- 渲染树构建:合并DOM和CSSOM形成渲染树
- 布局计算:计算每个节点在屏幕中的精确位置(回流)
- 页面绘制:将渲染树转换为屏幕上的实际像素(重绘)
- 执行JS:解析执行JavaScript代码,可能触发DOM更新和重新渲染
18.JavaScript 什么是长连接?
什么是长连接
长连接(Long Polling)是一种客户端与服务器保持持久连接的通信技术,它允许服务器在有新数据可用时立即推送给客户端,而不需要客户端频繁发起请求。
长连接的工作原理
- 客户端发起请求:客户端向服务器发送一个HTTP请求
- 服务器保持连接:服务器不会立即响应,而是保持这个连接打开
- 数据推送:
- 当服务器有新数据时,立即通过这个连接发送给客户端
- 如果没有新数据,服务器会保持连接直到超时(通常是几十秒)
- 重复过程:在收到响应或超时后,客户端立即发起新的请求
长连接的优势
- 实时性更好:相比传统轮询,能更快地获取服务器更新
- 减少无效请求:只在有数据时才传输,减少网络开销
- 兼容性好:基于标准HTTP协议,不需要特殊的服务器支持
JavaScript 实现长连接的示例
function longPolling() {
fetch('/api/long-poll')
.then(response => response.json())
.then(data => {
// 处理接收到的数据
console.log('收到数据:', data);
// 立即发起下一次请求
longPolling();
})
.catch(error => {
console.error('长连接错误:', error);
// 错误后延迟重试
setTimeout(longPolling, 5000);
});
}
// 启动长连接
longPolling();
长连接的应用场景
- 实时聊天应用:即时消息推送
- 股票行情系统:实时价格更新
- 在线协作工具:文档同步编辑
- 监控系统:实时状态更新
- 游戏应用:实时分数和状态同步
长连接的替代方案
随着技术发展,现代Web应用通常使用更高效的实时通信方案:
- WebSocket:全双工通信协议
- Server-Sent Events (SSE):服务器推送技术
- WebRTC:点对点实时通信
注意事项
- 服务器资源消耗:长连接会占用服务器资源,需要合理设置超时时间
- 网络环境:在移动网络环境下可能不稳定
- 浏览器限制:需要考虑浏览器对并发连接数的限制
长连接是构建实时Web应用的一种实用技术,特别适合需要简单实现且对实时性要求不太极端的应用场景。
19.window.write 和 ducument.innerHtml 区别?
-
方法类型
window.write是文档写入方法innerHTML是 DOM 元素属性
-
使用时机
write()仅在文档加载阶段有效innerHTML可随时修改指定元素内容
-
作用范围
write()会覆盖整个文档innerHTML只影响所选的 DOM 元素
-
性能表现
- 频繁使用
write()会导致页面重载 innerHTML的局部更新更高效
- 频繁使用
-
安全风险
- 两者都存在 XSS 漏洞风险
- 都需要对输入内容进行转义处理
20.简述doctype 的作用?
DOCTYPE(文档类型声明)用于告知浏览器当前文档遵循的HTML版本规范,确保页面以标准模式正确渲染。它位于HTML文档最开头,帮助浏览器选择合适的解析方式,避免触发怪异模式。
21.JavaScript 中常用的数组方法?
1. 基本操作方法
push() 和 pop()
-
push(): 向数组末尾添加一个或多个元素,并返回数组的新长度
const fruits = ['apple', 'banana']; fruits.push('orange'); // 返回 3,数组变为 ['apple', 'banana', 'orange'] -
pop(): 删除数组的最后一个元素并返回该元素
const lastFruit = fruits.pop(); // 返回 'orange',数组变为 ['apple', 'banana']
shift() 和 unshift()
-
shift(): 删除数组的第一个元素并返回该元素
const firstFruit = fruits.shift(); // 返回 'apple',数组变为 ['banana'] -
unshift(): 向数组开头添加一个或多个元素,返回数组的新长度
fruits.unshift('kiwi', 'pear'); // 返回 3,数组变为 ['kiwi', 'pear', 'banana']
2. 迭代方法
forEach()
对数组的每个元素执行一次提供的函数
[1, 2, 3].forEach(num => console.log(num * 2));
// 输出: 2, 4, 6
map()
创建一个新数组,其结果是该数组中的每个元素调用一次提供的函数后的返回值
const doubled = [1, 2, 3].map(num => num * 2); // [2, 4, 6]
filter()
创建一个新数组,包含通过所提供函数测试的所有元素
const evens = [1, 2, 3, 4].filter(num => num % 2 === 0); // [2, 4]
find() 和 findIndex()
-
find(): 返回数组中满足提供的测试函数的第一个元素的值
const firstEven = [1, 2, 3, 4].find(num => num % 2 === 0); // 2 -
findIndex(): 返回数组中满足提供的测试函数的第一个元素的索引
const firstEvenIndex = [1, 2, 3, 4].findIndex(num => num % 2 === 0); // 1
3. 排序和反转
sort()
对数组元素进行排序
const numbers = [3, 1, 4, 2];
numbers.sort(); // [1, 2, 3, 4]
numbers.sort((a, b) => b - a); // 降序排序 [4, 3, 2, 1]
reverse()
反转数组中元素的顺序
const arr = [1, 2, 3];
arr.reverse(); // [3, 2, 1]
4. 连接和切片
concat()
合并两个或多个数组,返回一个新数组
const arr1 = [1, 2];
const arr2 = [3, 4];
const combined = arr1.concat(arr2); // [1, 2, 3, 4]
slice()
返回一个数组的浅拷贝部分
const animals = ['ant', 'bison', 'camel', 'duck', 'elephant'];
animals.slice(2); // ['camel', 'duck', 'elephant']
animals.slice(2, 4); // ['camel', 'duck']
splice()
通过删除或替换现有元素或者添加新元素来修改数组
const months = ['Jan', 'March', 'April', 'June'];
months.splice(1, 0, 'Feb'); // 在索引1处插入'Feb'
// ['Jan', 'Feb', 'March', 'April', 'June']
months.splice(4, 1, 'May'); // 替换索引4处的元素
// ['Jan', 'Feb', 'March', 'April', 'May']
5. 高阶方法
reduce()
对数组中的每个元素执行一个reducer函数,将其结果汇总为单个返回值
const sum = [1, 2, 3].reduce((acc, curr) => acc + curr, 0); // 6
every()
测试数组中的所有元素是否都通过了指定函数的测试
const allEven = [2, 4, 6].every(num => num % 2 === 0); // true
some()
测试数组中是否至少有一个元素通过了指定函数的测试
const hasEven = [1, 3, 5, 6].some(num => num % 2 === 0); // true
6. ES6+ 新增方法
includes()
判断数组是否包含某个元素
[1, 2, 3].includes(2); // true
flat() 和 flatMap()
-
flat(): 将嵌套数组"拉平",返回新数组
[1, [2, [3]]].flat(2); // [1, 2, 3] -
flatMap(): 首先使用映射函数映射每个元素,然后将结果压缩成一个新数组
[1, 2, 3].flatMap(x => [x, x * 2]); // [1, 2, 2, 4, 3, 6]
Array.from()
从类数组对象或可迭代对象创建一个新的数组实例
Array.from('foo'); // ['f', 'o', 'o']
Array.from([1, 2, 3], x => x + x); // [2, 4, 6]
这些方法极大地简化了数组操作,开发者可以根据具体需求选择合适的方法来处理数组数据。
22.JavaScript 字符串方法?
1. 访问字符串内容
-
charAt(index)- 返回指定位置的字符let str = "Hello"; console.log(str.charAt(1)); // 输出 "e" -
charCodeAt(index)- 返回指定位置字符的Unicode编码console.log(str.charCodeAt(1)); // 输出 101 -
[]索引访问(ES6新增)console.log(str[1]); // 输出 "e"
2. 字符串拼接
concat()- 连接多个字符串let str1 = "Hello"; let str2 = "World"; console.log(str1.concat(", ", str2)); // 输出 "Hello, World"
3. 字符串查找
-
indexOf(searchValue, fromIndex)- 返回子字符串首次出现的位置let str = "Hello World"; console.log(str.indexOf("o")); // 输出 4 console.log(str.indexOf("o", 5)); // 输出 7 -
lastIndexOf(searchValue, fromIndex)- 返回子字符串最后出现的位置console.log(str.lastIndexOf("o")); // 输出 7 -
includes(searchString, position)- 检查是否包含子字符串(ES6新增)console.log(str.includes("World")); // 输出 true
高级字符串操作方法
1. 字符串截取
-
slice(start, end)- 提取字符串的一部分let str = "Hello World"; console.log(str.slice(0, 5)); // 输出 "Hello" console.log(str.slice(-5)); // 输出 "World" -
substring(start, end)- 类似slice但不支持负数console.log(str.substring(6, 11)); // 输出 "World" -
substr(start, length)- 从指定位置提取指定长度的字符(已废弃)console.log(str.substr(6, 5)); // 输出 "World"
2. 字符串替换
-
replace(searchValue, replaceValue)- 替换匹配的子字符串console.log(str.replace("World", "JavaScript")); // 输出 "Hello JavaScript" -
replaceAll(searchValue, replaceValue)- 替换所有匹配的子字符串(ES2021新增)let str = "apple,apple,banana"; console.log(str.replaceAll("apple", "orange")); // 输出 "orange,orange,banana"
3. 字符串大小写转换
-
toLowerCase()- 转换为小写console.log("HELLO".toLowerCase()); // 输出 "hello" -
toUpperCase()- 转换为大写console.log("hello".toUpperCase()); // 输出 "HELLO"
现代ES6+字符串方法
1. 模板字符串
let name = "Alice";
let age = 25;
console.log(`My name is ${name} and I'm ${age} years old.`);
// 输出 "My name is Alice and I'm 25 years old."
2. 字符串填充
-
padStart(targetLength, padString)- 在开头填充字符串console.log("5".padStart(4, "0")); // 输出 "0005" -
padEnd(targetLength, padString)- 在结尾填充字符串console.log("5".padEnd(4, "0")); // 输出 "5000"
3. 字符串迭代
for...of循环遍历字符串
实用字符串处理示例
1. 字符串分割
let csv = "apple,orange,banana";
let fruits = csv.split(",");
console.log(fruits); // 输出 ["apple", "orange", "banana"]
2. 去除空白字符
let str = " Hello World ";
console.log(str.trim()); // 输出 "Hello World"
console.log(str.trimStart()); // 输出 "Hello World "
console.log(str.trimEnd()); // 输出 " Hello World"
3. 字符串重复
console.log("abc".repeat(3)); // 输出 "abcabcabc"
4. 字符串包含检查
let sentence = "The quick brown fox jumps over the lazy dog";
console.log(sentence.startsWith("The")); // true
console.log(sentence.endsWith("dog")); // true
23.手写 防抖,节流。并区别他们的区别?
防抖(debounce)实现
function debounce(fn, delay) {
let timer = null;
return function() {
const context = this;
const args = arguments;
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(context, args);
}, delay);
};
}
应用场景:
- 搜索框输入建议:当用户停止输入一段时间后再发送请求
- 窗口大小调整事件:在用户完成调整窗口后再计算布局
- 表单验证:在用户停止输入后再进行验证
节流(throttle)实现
function throttle(fn, interval) {
let lastTime = 0;
return function() {
const now = new Date().getTime();
const context = this;
const args = arguments;
if (now - lastTime >= interval) {
fn.apply(context, args);
lastTime = now;
}
};
}
应用场景:
- 滚动事件处理:固定时间间隔触发,避免频繁触发影响性能
- 鼠标移动事件:限制mousemove事件的触发频率
- 游戏中的按键处理:防止玩家连续快速按键导致异常
防抖与节流的区别
| 特性 | 防抖(debounce) | 节流(throttle) |
|---|---|---|
| 触发时机 | 在事件停止触发后一定时间执行 | 按照固定时间间隔执行 |
| 执行次数 | 只执行最后一次 | 均匀执行多次 |
| 应用场景 | 适合"等稳定"场景(如搜索建议) | 适合"限制频率"场景(如滚动事件) |
| 响应性 | 延迟响应 | 即时但有限制的响应 |
| 极端情况 | 如果事件持续触发,可能永不执行 | 即使事件持续触发,也会定期执行 |
示例说明: 假设有一个按钮点击事件:
- 使用防抖(500ms):如果用户在500ms内连续点击,只有最后一次点击会被处理
- 使用节流(500ms):无论用户点击多快,每500ms最多只会处理一次点击
24.JavaScript的typeof 返回哪些数据类型?
-
"undefined":当变量未定义时返回
- 示例:
typeof x(x 未声明) - 特殊场景:已声明但未赋值的变量也会返回此类型
- 示例:
-
"boolean":布尔值类型
- 示例:
typeof true、typeof false - 注意:通过 Boolean() 转换的值也属于此类型
- 示例:
-
"number":数字类型
- 包括:整数、浮点数、NaN(Not a Number)
- 示例:
typeof 42、typeof 3.14、typeof NaN - 特殊说明:虽然 NaN 表示"非数字",但其类型仍是"number"
-
"string":字符串类型
- 示例:
typeof "hello" - 包含:空字符串、模板字符串等所有字符串形式
- 示例:
-
"bigint"(ES2020 新增):大整数类型
- 示例:
typeof 10n - 使用场景:处理超出 Number 安全范围的整数
- 示例:
-
"symbol"(ES6 新增):符号类型
- 示例:
typeof Symbol() - 特点:唯一且不可变的原始值
- 示例:
-
"function":函数类型
- 示例:
typeof function(){}、typeof class{} - 注意:箭头函数、生成器函数等也返回此类型
- 示例:
-
"object":对象类型(包括 null 的特殊情况)
- 常规对象:
typeof {}、typeof []、typeof new Date() - 特殊值:
typeof null也返回 "object"(这是历史遗留问题) - 注意:数组、日期、正则表达式等都会返回 "object"
- 常规对象:
补充说明:
- 使用
typeof检测 null 会返回 "object",这是 JavaScript 的已知 bug - 要准确区分数组、日期等具体对象类型,建议使用
Object.prototype.toString.call() - 函数虽然是对象的一种,但
typeof会专门返回 "function" - 从 ES6 开始,
typeof对未声明的变量不再报错,而是返回 "undefined"
25.javascipt 数组方法 pop push unshift shift 简单描述?
pop() 方法
- 功能:移除数组的最后一个元素
- 返回值:被移除的元素
- 修改原数组:是
- 示例:
let fruits = ['apple', 'banana', 'orange'];
let lastFruit = fruits.pop(); // 'orange'
console.log(fruits); // ['apple', 'banana']
push() 方法
- 功能:向数组末尾添加一个或多个元素
- 返回值:新数组的长度
- 修改原数组:是
- 示例:
let numbers = [1, 2, 3];
let newLength = numbers.push(4, 5); // 5
console.log(numbers); // [1, 2, 3, 4, 5]
unshift() 方法
- 功能:向数组开头添加一个或多个元素
- 返回值:新数组的长度
- 修改原数组:是
- 示例:
let colors = ['green', 'blue'];
let newLength = colors.unshift('red'); // 3
console.log(colors); // ['red', 'green', 'blue']
shift() 方法
- 功能:移除数组的第一个元素
- 返回值:被移除的元素
- 修改原数组:是
- 示例:
let animals = ['dog', 'cat', 'bird'];
let firstAnimal = animals.shift(); // 'dog'
console.log(animals); // ['cat', 'bird']
对比总结
| 方法 | 操作位置 | 操作类型 | 返回值 | 修改原数组 |
|---|---|---|---|---|
| pop() | 末尾 | 移除 | 被移除的元素 | 是 |
| push() | 末尾 | 添加 | 新数组长度 | 是 |
| unshift() | 开头 | 添加 | 新数组长度 | 是 |
| shift() | 开头 | 移除 | 被移除的元素 | 是 |
应用场景
- 实现队列:使用 push() 和 shift() 组合
- 实现栈:使用 push() 和 pop() 组合
- 动态调整数组:在需要频繁在数组开头或结尾添加/删除元素时使用这些方法
26.JavaScript call apply bind 的区别?
JavaScript 中 call、apply 和 bind 的区别详解
基本概念
这三个方法都是 JavaScript 中 Function 对象原型上的方法,用于改变函数的执行上下文(this 指向)。它们的主要区别在于参数传递方式和执行时机。
详细对比
1. call 方法
- 语法:
func.call(thisArg, arg1, arg2, ...) - 特点:
- 立即执行函数
- 参数逐个传递
- 第一个参数指定 this 值,后续参数是函数参数
示例:
function greet(greeting, punctuation) {
console.log(greeting + ', ' + this.name + punctuation);
}
const person = { name: 'John' };
greet.call(person, 'Hello', '!'); // 输出: "Hello, John!"
2. apply 方法
- 语法:
func.apply(thisArg, [argsArray]) - 特点:
- 立即执行函数
- 参数以数组形式传递
- 适合处理参数数量不确定的情况
示例:
function greet(greeting, punctuation) {
console.log(greeting + ', ' + this.name + punctuation);
}
const person = { name: 'John' };
greet.apply(person, ['Hi', '!!']); // 输出: "Hi, John!!"
// 数组参数处理示例
function sum() {
return Array.from(arguments).reduce((a, b) => a + b);
}
const numbers = [1, 2, 3, 4];
console.log(sum.apply(null, numbers)); // 输出: 10
3. bind 方法
- 语法:
func.bind(thisArg[, arg1[, arg2[, ...]]]) - 特点:
- 创建一个新函数,不立即执行
- 可以预设参数(部分应用)
- 返回的函数可以稍后调用
示例:
function greet(greeting, punctuation) {
console.log(greeting + ', ' + this.name + punctuation);
}
const person = { name: 'John' };
const greetJohn = greet.bind(person, 'Hello');
greetJohn('!'); // 输出: "Hello, John!"
// 事件处理中的典型应用
const button = document.querySelector('button');
button.addEventListener('click', function() {
console.log(this); // 默认 this 指向 button
}.bind(person)); // 强制 this 指向 person
应用场景对比
| 方法 | 典型应用场景 |
|---|---|
| call | 明确知道参数个数,需要立即执行函数时使用 |
| apply | 参数数量不确定或已有数组形式参数时使用 |
| bind | 需要延迟执行或作为回调函数时使用,特别是需要固定 this 指向的情况 |
性能考虑
bind会创建一个新函数,相比call和apply有额外的性能开销- 在不需要预设参数的情况下,使用
call或apply更高效 - 在循环或高频调用的场景中,应避免频繁使用
bind
27.JavaScript 闭包是什么,有什么特征,对页面有什么影响?简要介绍你理解的闭包?
什么是闭包
闭包是指能够访问其他函数作用域中变量的函数,或者说函数和其周围状态(词法环境)的引用捆绑在一起形成的组合。
在JavaScript中,每创建一个函数,就会同时创建一个闭包。当内部函数引用了外部函数的变量时,就形成了典型的闭包使用场景。
闭包的主要特征
- 函数嵌套:闭包通常出现在函数嵌套结构中
- 变量保留:即使外部函数已经执行完毕,内部函数仍能访问外部函数的变量
- 局部变量持久化:闭包可以使局部变量的生命周期延长
- 私有性:通过闭包可以创建私有变量和方法
常见应用场景
// 1. 计数器功能
function createCounter() {
let count = 0; // 私有变量
return function() {
return ++count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
// 2. 模块模式
const calculator = (function() {
let result = 0;
return {
add: function(x) { result += x; },
subtract: function(x) { result -= x; },
getResult: function() { return result; }
};
})();
对页面的影响
-
内存占用:
- 闭包会保留其作用域链,可能导致内存无法被垃圾回收
- 不当使用可能导致内存泄漏,特别是DOM元素引用
-
性能考虑:
- 闭包访问外部变量比访问局部变量稍慢
- 但是现代JavaScript引擎已对此做了大量优化
-
实际应用:
- 事件处理程序中常用闭包来保持上下文
- 模块化和封装代码的有效手段
- 实现函数柯里化(currying)和偏函数应用
最佳实践
- 避免不必要的闭包:只在需要时使用
- 及时释放引用:特别是对DOM元素的引用
- 性能关键代码:减少闭包嵌套层级
- 模块化开发:合理利用闭包实现封装
闭包是JavaScript强大功能的核心概念之一,理解并正确使用闭包可以写出更优雅、更高效的代码。
28.JavaScript 添加 删除 替换 插入到某个接口的方法?
添加元素到数组
1. push() - 在数组末尾添加元素
let fruits = ['apple', 'banana'];
fruits.push('orange'); // 添加单个元素
// fruits现在是 ['apple', 'banana', 'orange']
fruits.push('mango', 'pear'); // 添加多个元素
// fruits现在是 ['apple', 'banana', 'orange', 'mango', 'pear']
2. unshift() - 在数组开头添加元素
let numbers = [2, 3, 4];
numbers.unshift(1); // 在开头添加
// numbers现在是 [1, 2, 3, 4]
3. concat() - 合并数组
let arr1 = [1, 2];
let arr2 = [3, 4];
let combined = arr1.concat(arr2);
// combined是 [1, 2, 3, 4]
删除数组元素
1. pop() - 删除最后一个元素
let colors = ['red', 'green', 'blue'];
let lastColor = colors.pop();
// lastColor是 'blue',colors现在是 ['red', 'green']
2. shift() - 删除第一个元素
let letters = ['a', 'b', 'c'];
let firstLetter = letters.shift();
// firstLetter是 'a',letters现在是 ['b', 'c']
3. splice() - 删除指定位置元素
let numbers = [1, 2, 3, 4, 5];
numbers.splice(2, 1); // 从索引2开始删除1个元素
// numbers现在是 [1, 2, 4, 5]
替换数组元素
1. splice() - 替换元素
let items = ['pen', 'book', 'laptop'];
items.splice(1, 1, 'notebook'); // 替换索引1的元素
// items现在是 ['pen', 'notebook', 'laptop']
2. 直接通过索引赋值
let pets = ['dog', 'cat', 'fish'];
pets[1] = 'bird'; // 直接替换
// pets现在是 ['dog', 'bird', 'fish']
在指定位置插入元素
1. splice() - 插入元素
let days = ['Mon', 'Wed', 'Fri'];
days.splice(1, 0, 'Tue'); // 在索引1处插入,不删除任何元素
// days现在是 ['Mon', 'Tue', 'Wed', 'Fri']
days.splice(3, 0, 'Thu', 'Sat'); // 插入多个元素
// days现在是 ['Mon', 'Tue', 'Wed', 'Thu', 'Sat', 'Fri']
实际应用场景示例
购物车操作
let cart = ['apple', 'banana'];
// 添加商品
cart.push('orange');
cart.unshift('bread');
// 删除第一个商品(可能是不想要的)
cart.shift();
// 替换第二个商品
cart.splice(1, 1, 'pear');
// 在特定位置插入优惠商品
cart.splice(2, 0, 'discounted_milk');
待办事项列表管理
let todos = ['Buy groceries', 'Call mom'];
// 添加新任务
todos.push('Pay bills');
// 完成第一个任务(删除)
todos.shift();
// 修改任务
todos[0] = 'Call parents';
// 插入高优先级任务
todos.unshift('Urgent: Submit report');
这些方法提供了灵活的方式来操作数组内容,可以根据具体需求选择合适的操作方式。
29.阐述JavaScript 的同源策略?
定义与基本概念
同源策略(Same-Origin Policy)是浏览器实施的一种重要的安全机制,用于限制不同源之间的交互行为。它的核心目的是防止恶意网站窃取或篡改来自其他网站的数据。
同源的定义
两个URL在以下三个方面完全相同时才被认为是同源的:
- 协议:如http、https、ftp等
- 域名:如www.example.com、sub.example.com
- 端口号:如80、443等(默认端口可省略)
例如:
https://example.com/page1和https://example.com/page2是同源的http://example.com和https://example.com不同源(协议不同)https://example.com和https://sub.example.com不同源(子域名不同)https://example.com:80和https://example.com:443不同源(端口不同)
受同源策略限制的常见操作
- AJAX请求:XMLHttpRequest和Fetch API默认只能向同源URL发送请求
- DOM访问:JavaScript不能访问不同源的iframe内容
- Cookie/Storage访问:无法读取不同源的Cookie、LocalStorage等存储数据
- Web Workers:某些操作也受同源策略限制
跨域资源共享(CORS)
为了解决合理跨域需求,W3C提出了CORS(Cross-Origin Resource Sharing)标准。CORS通过在HTTP头中添加特殊字段来实现受控的跨域访问:
- 简单请求:满足特定条件的GET/POST/HEAD请求,浏览器自动添加
Origin头,服务器响应需包含Access-Control-Allow-Origin头 - 预检请求:对于复杂请求(如PUT/DELETE等),浏览器先发送OPTIONS预检请求,检查服务器是否允许跨域
例如,服务器端可以设置:
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Content-Type
其他跨域解决方案
- JSONP:利用
<script>标签不受同源策略限制的特性 - 代理服务器:通过同源服务器转发请求
- postMessage API:实现跨文档通信
- WebSocket:不受同源策略限制
- document.domain:适用于相同主域下的子域间通信(仅限某些场景)
实际应用场景
- 单点登录(SSO):主域和子域间的认证信息共享
- 微前端架构:多个前端应用间的通信
- API网关:统一处理跨域请求
- 第三方组件集成:如嵌入地图、支付等第三方服务
安全考量
虽然跨域技术提供了便利,但也需注意:
- CORS配置不当可能导致安全漏洞
- 应避免使用过于宽松的
Access-Control-Allow-Origin: * - 敏感操作应结合CSRF Token等额外防护措施
同源策略是Web安全的基础机制,理解其原理和变通方法对现代Web开发至关重要。
30.JavaScript 阐述this 对象的理解?
在 JavaScript 中,this 是一个特殊的关键字,它指向当前执行上下文中的对象。理解 this 的行为对于掌握 JavaScript 至关重要。
this 的指向规则
-
默认绑定:在独立函数调用时,
this指向全局对象(浏览器中为window,Node.js 中为global),严格模式下为undefined -
隐式绑定:当函数作为对象的方法调用时,
this指向调用该方法的对象 -
显式绑定:通过
call()、apply()或bind()方法明确指定this的指向 -
new 绑定:使用
new关键字调用构造函数时,this指向新创建的对象实例 -
箭头函数:箭头函数没有自己的
this,它继承自外层函数作用域
常见应用场景
- 在对象方法中访问对象属性
- 在事件处理函数中引用触发事件的元素
- 在构造函数中初始化对象实例
- 通过
bind创建具有固定this值的函数
掌握 this 的指向规则有助于编写更清晰、更可维护的 JavaScript 代码。
31.dom 怎样添加 移除 移动 复制 创建 和查找节点?
添加节点
appendChild()
将节点添加到指定父节点的子节点列表末尾:
const parent = document.getElementById('parent');
const newChild = document.createElement('div');
parent.appendChild(newChild);
insertBefore()
在指定节点前插入新节点:
const parent = document.getElementById('parent');
const newChild = document.createElement('div');
const referenceNode = document.getElementById('child2');
parent.insertBefore(newChild, referenceNode);
insertAdjacentHTML()
在相对于元素指定位置插入HTML字符串:
const element = document.getElementById('target');
element.insertAdjacentHTML('beforebegin', '<div>内容</div>'); // 四种位置可选:beforebegin, afterbegin, beforeend, afterend
移除节点
removeChild()
从DOM中移除子节点:
const parent = document.getElementById('parent');
const child = document.getElementById('child-to-remove');
parent.removeChild(child);
remove()
节点自身移除方法:
const element = document.getElementById('element-to-remove');
element.remove();
移动节点
移动节点本质上是先移除再添加到新位置:
const newParent = document.getElementById('new-parent');
const element = document.getElementById('element-to-move');
newParent.appendChild(element);
复制节点
cloneNode()
创建节点的副本:
const original = document.getElementById('original');
const clone = original.cloneNode(true); // true表示深度克隆,包括所有子节点
document.body.appendChild(clone);
创建节点
createElement()
创建元素节点:
const newDiv = document.createElement('div');
newDiv.textContent = '新创建的div';
document.body.appendChild(newDiv);
createTextNode()
创建文本节点:
const textNode = document.createTextNode('这是一段文本');
document.body.appendChild(textNode);
createDocumentFragment()
创建文档片段,批量操作DOM时提高性能:
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const div = document.createElement('div');
div.textContent = `项目 ${i}`;
fragment.appendChild(div);
}
document.body.appendChild(fragment);
查找节点
常用查找方法
getElementById(): 通过ID查找getElementsByClassName(): 通过类名查找getElementsByTagName(): 通过标签名查找querySelector(): 使用CSS选择器查找单个元素querySelectorAll(): 使用CSS选择器查找所有匹配元素
// 示例
const byId = document.getElementById('myId');
const byClass = document.getElementsByClassName('myClass');
const byTag = document.getElementsByTagName('div');
const bySelector = document.querySelector('#container .item');
const allBySelector = document.querySelectorAll('.items li');
节点关系查找
parentNode: 获取父节点childNodes: 获取所有子节点firstChild/lastChild: 获取第一个/最后一个子节点previousSibling/nextSibling: 获取前一个/后一个兄弟节点
const parent = document.getElementById('child').parentNode;
const children = document.getElementById('parent').childNodes;
const first = document.getElementById('parent').firstChild;
const last = document.getElementById('parent').lastChild;
const prev = document.getElementById('item2').previousSibling;
const next = document.getElementById('item2').nextSibling;
32.JavaScript 中 null 和 unidefind 的区别?
在 JavaScript 中,null 和 undefined 的区别主要体现在以下几个方面:
-
定义差异
null表示一个空值引用,通常由开发者显式赋值undefined表示变量未定义或未初始化
-
类型检测
typeof null返回"object"(历史遗留问题)typeof undefined返回"undefined"
-
使用场景
null常用于主动清空对象引用undefined通常表示变量声明但未赋值,或访问对象不存在的属性
-
相等性比较
null == undefined返回true(抽象相等)null === undefined返回false(严格相等)
-
数值转换
Number(null)结果为0Number(undefined)结果为NaN
理解这些区别有助于编写更严谨的 JavaScript 代码。
33.JavaScript 中new 操作符具体作用?
在 JavaScript 中,new 操作符用于创建一个用户定义的对象类型的实例或具有构造函数的内置对象类型的实例。它的具体作用可以分为以下几个步骤:
-
创建新对象:
new操作符首先会创建一个空的普通 JavaScript 对象(即{})。 -
设置原型链:将这个新创建的空对象的
__proto__属性(即原型)指向构造函数的prototype属性,从而继承构造函数原型上的属性和方法。 -
绑定 this 上下文:将构造函数的作用域赋给新对象(因此
this就指向了这个新对象)。 -
执行构造函数:执行构造函数中的代码(为这个新对象添加属性)。构造函数通常会通过
this来对新对象进行初始化。 -
返回新对象:
- 如果构造函数没有显式返回一个对象,则返回新创建的对象(通常情况)。
- 如果构造函数返回了一个非原始值(如对象、数组、函数等),则
new表达式会返回这个对象而不是新创建的对象。 - 如果构造函数返回了一个原始值(如数字、字符串、布尔值等),则忽略返回值并返回新创建的对象。
示例代码:
function Person(name, age) {
this.name = name;
this.age = age;
this.greet = function() {
console.log(`Hello, my name is ${this.name}`);
};
}
// 使用 new 创建实例
const person1 = new Person('Alice', 25);
person1.greet(); // 输出: "Hello, my name is Alice"
// 检查原型链
console.log(person1 instanceof Person); // true
特殊情况示例:
function Car(model) {
this.model = model;
return { custom: 'object' }; // 返回一个对象
}
const car = new Car('Tesla');
console.log(car); // { custom: 'object' } 而不是 Car 实例
function Bike() {
return 42; // 返回原始值
}
const bike = new Bike();
console.log(bike); // Bike 实例,42 被忽略
应用场景:
- 创建自定义对象类型
- 实现基于原型的继承
- 框架和库中创建实例(如 React 组件)
- 封装具有特定行为和状态的复杂对象
注意事项:
- 忘记使用
new操作符会导致this指向全局对象(非严格模式)或 undefined(严格模式)。 - ES6 的
class语法糖内部也是基于new操作符的工作原理。 - 可以使用
new.target属性来检测函数是否通过new调用。
34.简述一下src 和href 的区别?
在 HTML 和前端开发中,src 和 href 是两个常用的属性,它们的主要区别如下:
1. 定义和用途
-
src(Source)
用于嵌入外部资源到当前文档中,例如图像、脚本、iframe 等。浏览器会加载并执行或显示这些资源。
示例:<script src="script.js"></script> <img src="image.jpg" alt="Example Image"> <iframe src="external-page.html"></iframe> -
href(Hypertext Reference)
用于建立当前文档与其他资源的链接关系,例如 CSS 文件、超链接、锚点等。浏览器不会直接加载这些资源,而是根据用户操作(如点击)或需要(如加载 CSS)去获取。
示例:<a href="https://example.com">Visit Example</a> <link href="styles.css" rel="stylesheet">
2. 加载行为
-
src
浏览器会立即加载并处理资源(如执行脚本、显示图像)。如果资源加载失败,可能会影响页面功能或显示。
场景: 脚本文件、图片、嵌入式内容(如视频或 iframe)。 -
href
资源通常是延迟加载的,例如 CSS 文件在页面渲染时加载,超链接资源仅在用户点击时加载。
场景: 样式表、超链接、favicon 等。
3. 适用标签
src常用标签:
<script>,<img>,<iframe>,<audio>,<video>,<input type="image">等。href常用标签:
<a>,<link>,<area>,<base>等。
4. 关键区别总结
| 属性 | 作用 | 加载时机 | 典型用途 |
|---|---|---|---|
src | 嵌入资源到文档 | 立即加载 | 脚本、图片、多媒体 |
href | 链接到外部资源 | 按需或延迟加载 | 超链接、样式表、锚点 |
5. 注意事项
- 错误使用可能导致功能异常,例如用
href引入脚本(<script href="...">)是无效的。 - 现代开发中,某些框架(如 React)可能通过自定义属性(如
srcSet)扩展了src的功能。
通过理解两者的差异,可以更准确地选择属性以避免页面加载或功能问题。
36.JavaScript 垃圾回收方法?
JavaScript 采用自动内存管理机制,主要通过以下两种方式回收不再使用的内存:
-
标记清除(Mark-and-Sweep)
- 这是主流 JavaScript 引擎最常用的算法
- 分为标记阶段和清除阶段
- 从根对象(全局对象)开始遍历所有可达对象并标记
- 清除未被标记的对象,回收其内存
-
引用计数(Reference Counting)
- 记录每个对象被引用的次数
- 当引用次数降为0时立即回收
- 但无法处理循环引用的情况,现代引擎已很少使用
现代 JavaScript 引擎(如 V8)还采用了分代回收、增量标记等优化策略来提高垃圾回收效率。
37.JavaScript 继承方法及其优缺点?
1. 原型链继承
实现方式
通过将子类的原型指向父类的实例来实现继承:
function Parent() {
this.name = 'parent';
}
function Child() {
this.type = 'child';
}
Child.prototype = new Parent();
优点
- 简单易实现
- 可以访问父类原型上的属性和方法
缺点
- 无法向父类构造函数传参
- 所有子类实例共享父类的引用类型属性
- 无法实现多继承
示例问题
const child1 = new Child();
const child2 = new Child();
child1.name = 'modified';
console.log(child2.name); // 也会被修改
2. 构造函数继承
实现方式
在子类构造函数中调用父类构造函数:
function Parent(name) {
this.name = name;
}
function Child(name) {
Parent.call(this, name);
this.type = 'child';
}
优点
- 可以向父类传递参数
- 避免了引用类型属性共享的问题
- 可以实现多继承(调用多个父类构造函数)
缺点
- 无法继承父类原型上的方法
- 方法都在构造函数中定义,无法复用
- 不是真正的继承,只是复制了父类的实例属性
3. 组合继承
实现方式
结合原型链继承和构造函数继承:
function Parent(name) {
this.name = name;
}
function Child(name) {
Parent.call(this, name); // 第二次调用
this.type = 'child';
}
Child.prototype = new Parent(); // 第一次调用
Child.prototype.constructor = Child;
优点
- 可以继承父类实例属性和原型方法
- 可以传递参数
- 避免了引用类型属性共享问题
- instanceof和isPrototypeOf识别正常
缺点
- 会调用两次父类构造函数
- 子类原型上会有不必要的父类实例属性
4. 原型式继承
实现方式
基于已有对象创建新对象:
function createObj(o) {
function F() {}
F.prototype = o;
return new F();
}
优点
- 不需要创建自定义类型
- 适合不需要构造函数但需要对象间共享信息的场景
缺点
- 与原型链继承有相同的问题
- 无法复用,属性共享问题
5. 寄生式继承
实现方式
创建一个仅用于封装继承过程的函数:
function createAnother(original) {
const clone = Object.create(original);
clone.sayHi = function() {
console.log('hi');
};
return clone;
}
优点
- 可以在不修改原对象的情况下增强对象
缺点
- 方法无法复用
- 与构造函数模式类似
6. 寄生组合式继承(最佳实践)
实现方式
通过借用构造函数继承属性,通过原型链继承方法:
function inheritPrototype(child, parent) {
const prototype = Object.create(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
}
function Parent(name) {
this.name = name;
}
function Child(name) {
Parent.call(this, name);
}
inheritPrototype(Child, Parent);
优点
- 只调用一次父类构造函数
- 避免了在子类原型上创建不必要的属性
- 原型链保持不变
- 是ES6 class继承的实现原理
缺点
- 实现相对复杂
7. ES6 Class继承
实现方式
使用class和extends语法:
class Parent {
constructor(name) {
this.name = name;
}
}
class Child extends Parent {
constructor(name) {
super(name);
this.type = 'child';
}
}
优点
- 语法简洁
- 内置支持super调用
- 是寄生组合式继承的语法糖
- 支持静态方法继承
缺点
- 兼容性问题(需要Babel转译)
- 不能继承普通构造函数(非class定义)
总结对比
| 继承方式 | 优点 | 缺点 |
|---|---|---|
| 原型链继承 | 简单 | 引用属性共享,无法传参 |
| 构造函数继承 | 可传参,避免共享 | 无法继承原型方法 |
| 组合继承 | 综合优点 | 调用两次父类构造函数 |
| 原型式继承 | 无需构造函数 | 与原型链相同问题 |
| 寄生式继承 | 增强对象 | 方法无法复用 |
| 寄生组合式继承 | 最佳实践 | 实现复杂 |
| ES6 Class继承 | 语法简洁 | 需要转译 |
实际开发中,推荐使用ES6 class继承或寄生组合式继承,它们提供了最完善的继承方案。
38.JavaScript 对象的几种创建方式?
1. 对象字面量
const person = {
name: '张三',
age: 25,
greet() {
console.log(`你好,我是${this.name}`);
}
};
2. new Object()
const person = new Object();
person.name = '张三';
person.age = 25;
person.greet = function() {
console.log(`你好,我是${this.name}`);
};
3. 构造函数模式
function Person(name, age) {
this.name = name;
this.age = age;
this.greet = function() {
console.log(`你好,我是${this.name}`);
};
}
const person = new Person('张三', 25);
4. Object.create()
const personProto = {
greet: function() {
console.log(`你好,我是${this.name}`);
}
};
const person = Object.create(personProto);
person.name = '张三';
person.age = 25;
5. ES6 类语法
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
console.log(`你好,我是${this.name}`);
}
}
const person = new Person('张三', 25);
40.JavaScript 原型 和原型链?
原型(Prototype)
每个 JavaScript 对象都有一个内置属性 [[Prototype]],它指向另一个对象,这个对象就是该对象的原型。原型包含可以被继承的属性和方法。
访问原型的方式
- 通过
__proto__属性(非标准,但被广泛支持) - 使用
Object.getPrototypeOf()方法(推荐) - 构造函数通过
prototype属性访问
原型链(Prototype Chain)
当访问一个对象的属性时,如果在当前对象上找不到,JavaScript 会沿着原型链向上查找,直到找到该属性或到达原型链的末端(null)。这种链式查找机制就是原型链。
特点
- 原型链构成了 JavaScript 的继承基础
- 所有对象的最终原型都是
Object.prototype Object.prototype.__proto__为null,表示原型链的终点
41.JavaScript 数据对象有哪些属性值?
JavaScript 数据对象的属性值详解
JavaScript 中的数据对象可以拥有多种类型的属性值,这些属性值决定了对象的行为和特征。以下是主要的属性值类型:
1. 数据属性值
数据属性包含一个数据值的位置,可以有以下特性:
-
value:属性的值,可以是任意 JavaScript 类型(undefined、null、boolean、number、string、symbol、bigint、object)
const obj = { name: "John" // "John" 就是 value }; -
writable:布尔值,表示是否可修改属性值(默认 false)
Object.defineProperty(obj, 'age', { value: 30, writable: false // 设置为不可写 }); -
enumerable:布尔值,表示是否可枚举(默认 false)
Object.defineProperty(obj, 'id', { value: 123, enumerable: false // 不会出现在 for...in 循环中 }); -
configurable:布尔值,表示是否可删除或修改特性(默认 false)
Object.defineProperty(obj, 'secret', { value: "confidential", configurable: false // 不能删除或修改属性特性 });
2. 访问器属性值
访问器属性不包含数据值,而是包含一对 getter 和 setter 函数:
- get:获取属性值时调用的函数(默认 undefined)
- set:设置属性值时调用的函数(默认 undefined)
const user = {
firstName: "John",
lastName: "Doe",
get fullName() {
return `${this.firstName} ${this.lastName}`;
},
set fullName(value) {
[this.firstName, this.lastName] = value.split(" ");
}
};
3. 内置属性值
JavaScript 对象还有一些内置属性:
- prototype:对象的原型链引用
- length:对于数组对象,表示元素数量
- constructor:创建该对象的构造函数引用
4. 特殊属性值
-
Symbol 属性:ES6 引入的不可枚举的唯一值属性
const id = Symbol('id'); obj[id] = "symbol value"; -
动态计算属性名:ES6 允许在对象字面量中使用表达式作为属性名
const propKey = 'name'; const obj = { [propKey]: 'John' // 动态属性名 };
5. 继承的属性值
对象可以从其原型链继承属性值:
function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
console.log(`Hello, ${this.name}!`);
};
const p = new Person("Alice");
p.greet(); // greet 是从原型继承的方法
6. 对象属性描述符
可以通过 Object.getOwnPropertyDescriptor() 获取属性描述符:
const desc = Object.getOwnPropertyDescriptor(obj, 'name');
// 返回 { value: "John", writable: true, enumerable: true, configurable: true }
可以通过 Object.defineProperty() 或 Object.defineProperties() 定义或修改属性特性:
Object.defineProperty(obj, 'readOnlyProp', {
value: "can't change me",
writable: false,
enumerable: true,
configurable: false
});
43.JavaScript 请举出一个匿名函数的典型用例?
JavaScript 匿名函数的典型用例
回调函数
匿名函数最常见的用途之一是作为回调函数传递给其他函数。例如:
// 定时器中的匿名函数
setTimeout(function() {
console.log('这段代码将在1秒后执行');
}, 1000);
// 数组方法中的回调
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(function(num) {
return num * 2;
});
console.log(doubled); // 输出: [2, 4, 6, 8, 10]
立即执行函数(IIFE)
匿名函数可以立即执行,用于创建私有作用域:
(function() {
const privateVar = '这个变量对外部不可见';
console.log(privateVar); // 可以访问
})();
// console.log(privateVar); // 会报错,privateVar未定义
事件处理
匿名函数常用于事件处理程序:
document.getElementById('myButton').addEventListener('click', function() {
alert('按钮被点击了!');
});
闭包
匿名函数可以用于创建闭包:
function createCounter() {
let count = 0;
return function() { // 返回匿名函数
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
对象方法
当方法只需要在特定位置使用时,可以使用匿名函数:
const calculator = {
add: function(a, b) {
return a + b;
}
};
console.log(calculator.add(2, 3)); // 5
Promise链
匿名函数在Promise链中也很常见:
fetch('https://api.example.com/data')
.then(function(response) {
return response.json();
})
.then(function(data) {
console.log(data);
})
.catch(function(error) {
console.error('Error:', error);
});
44.请指出JavaScript 宿主对象和原生对象的区别?
- 来源不同:
- 原生对象由 JavaScript 语言规范定义
- 宿主对象由运行环境(如浏览器)提供
- 实现方式:
- 原生对象是 ECMAScript 标准的一部分
- 宿主对象是环境特定的扩展
- 典型示例:
- 原生对象:Object、Array、Date
- 宿主对象:window、document、XMLHttpRequest
- 可移植性:
- 原生对象在所有兼容环境中表现一致
- 宿主对象在不同环境中可能有差异
- 扩展性:
- 原生对象的行为受语言规范约束
- 宿主对象可以根据环境需求进行扩展
45.JavaScript 请解释变量声明提升?
在 JavaScript 中,变量声明提升(Hoisting)是一个重要的概念,它指的是在代码执行前,JavaScript 引擎会将变量和函数的声明提升到当前作用域的顶部。
变量声明提升的表现
-
var 声明的变量:
console.log(a); // 输出:undefined var a = 5;等价于:
var a; console.log(a); // 输出:undefined a = 5; -
let 和 const 声明的变量:
console.log(b); // 报错:ReferenceError: Cannot access 'b' before initialization let b = 10;虽然 let 和 const 也存在提升,但会进入"暂时性死区"(Temporal Dead Zone),直到声明语句被执行。
-
函数声明提升:
foo(); // 输出:"Hello" function foo() { console.log("Hello"); }函数声明会被完全提升,包括函数体。
实际应用中的注意事项
-
变量命名冲突:在函数内部,同名变量的声明会覆盖全局变量
-
var x = 1; function test() { console.log(x); // 输出:undefined var x = 2; } test(); -
最佳实践:
- 始终在作用域顶部声明变量
- 优先使用 let 和 const 代替 var
- 在使用前确保变量已经被声明和初始化
底层原理
JavaScript 引擎在编译阶段会创建执行上下文(Execution Context),其中包含变量环境(Variable Environment)。所有变量声明都会在这个阶段被处理,但赋值操作要等到执行阶段才会进行。
理解变量提升有助于:
- 避免意外的 undefined 值
- 编写更可预测的代码
- 更好地调试作用域相关的问题
47.请指出 doucument.onload 和 document.ready 两个事件的区别?
-
触发时机
document.onload在整个页面(包括图片等资源)完全加载后触发document.ready(通常指 jQuery 的$(document).ready())在 DOM 树构建完成后立即触发,无需等待资源加载
-
执行顺序
document.ready总是先于document.onload执行 -
使用场景
- 需要操作 DOM 元素时使用
document.ready - 需要确保所有资源(如图片)加载完成时使用
document.onload
- 需要操作 DOM 元素时使用
-
重复绑定
document.onload会覆盖之前绑定的事件document.ready允许多个事件处理程序按顺序执行
-
原生支持
document.onload是原生 JavaScript 事件document.ready是 jQuery 提供的功能,原生 JS 可通过DOMContentLoaded事件实现类似功能
48.JavaScript 中 == 和=== 区别?
==(宽松相等):
- 会进行类型转换后再比较
- 比较时不考虑数据类型
- 例如:1 == "1" 返回 true
===(严格相等):
- 不会进行类型转换
- 要求值和类型都相同
- 例如:1 === "1" 返回 false
建议: 在大多数情况下,推荐使用 === 以避免意外的类型转换问题
49.如何从浏览器的url 中获取查询字符串参数?
- 原生JavaScript方法
// 获取当前URL的查询字符串
const queryString = window.location.search;
// 使用URLSearchParams API解析
const urlParams = new URLSearchParams(queryString);
const paramValue = urlParams.get('参数名');
// 示例:获取URL中id参数的值
// URL: http://example.com?name=John&id=123
const userId = urlParams.get('id'); // 返回"123"
- 传统字符串处理方法
function getQueryParam(paramName) {
const query = window.location.search.substring(1);
const vars = query.split('&');
for (let i=0; i<vars.length; i++) {
const pair = vars[i].split('=');
if (decodeURIComponent(pair[0]) === paramName) {
return decodeURIComponent(pair[1]);
}
}
return null;
}
// 使用示例
const userName = getQueryParam('name');
- 使用常用的第三方库
- jQuery插件方法:
// 使用jQuery URL插件
const value = $.url().param('参数名');
- Vue.js中的获取方式:
// 在Vue路由中
this.$route.query.参数名
- React Router中的获取方式:
// 使用useSearchParams hook
import { useSearchParams } from 'react-router-dom';
function MyComponent() {
const [searchParams] = useSearchParams();
const paramValue = searchParams.get('参数名');
// ...
}
注意事项:
- 参数值需要进行URI解码(decodeURIComponent)
- 处理多个同名参数时需要考虑
- 哈希值(#)后的内容不属于查询字符串
- 在单页应用(SPA)中,可能需要使用框架特定的路由API
应用场景:
- 统计跟踪(如UTM参数)
- 页面间数据传递
- 搜索结果过滤
- 用户认证(如OAuth回调)
50.JavaScript 什么是三元表达式? 三元 表示什么意思?
什么是三元表达式?
三元表达式是 JavaScript 中的一种条件运算符,也称为条件运算符(conditional operator)。它是 JavaScript 中唯一一个需要三个操作数的运算符,语法格式如下:
条件 ? 表达式1 : 表达式2
这个运算符的工作原理是:首先计算条件的值,如果条件为真(truthy),则计算并返回表达式1的值;如果条件为假(falsy),则计算并返回表达式2的值。
为什么叫"三元"?
"三元"这个名称来源于数学和逻辑学中的概念:
-
三元表示这个运算符由三部分组成:
- 一个条件判断
- 一个条件为真时的返回值
- 一个条件为假时的返回值
-
这是 JavaScript 中唯一一个需要三个操作数的运算符,其他运算符通常是一元(如
!,typeof)或二元(如+,-,*,/等)。
实际应用示例
基本用法
const age = 20;
const canDrink = age >= 18 ? '可以饮酒' : '禁止饮酒';
console.log(canDrink); // 输出: "可以饮酒"
替代简单的 if-else 语句
// 使用 if-else
let price;
if (isMember) {
price = 80;
} else {
price = 100;
}
// 使用三元表达式
const price = isMember ? 80 : 100;
嵌套三元表达式(不推荐过度使用)
const score = 85;
const grade = score >= 90 ? 'A' :
score >= 80 ? 'B' :
score >= 70 ? 'C' : 'D';
console.log(grade); // 输出: "B"
在模板字符串中使用
const user = { name: '张三', isAdmin: true };
const message = `欢迎${user.isAdmin ? '管理员' : '用户'} ${user.name}`;
console.log(message); // 输出: "欢迎管理员 张三"
注意事项
- 三元表达式虽然简洁,但过度使用(特别是嵌套)会降低代码可读性
- 对于复杂的逻辑判断,还是推荐使用 if-else 语句
- 表达式1和表达式2的类型可以不同,但为了代码可维护性,最好保持一致
- 三元表达式会返回一个值,可以用于赋值操作或作为函数参数
三元表达式是 JavaScript 中非常有用的语法糖,合理使用可以使代码更加简洁明了。
51.JavaScript里函数参数 arguments是数组吗?
JavaScript 中的 arguments 对象并不是真正的数组。虽然它看起来像数组(可以通过索引访问元素并具有 length 属性),但它实际上是类数组对象,缺少数组原型上的方法(如 push、pop 等)。如果需要对 arguments 进行操作,可以将其转换为真正的数组,例如使用 Array.from(arguments) 或展开运算符 [...arguments]。
52.JavaScript 什么是use strict ? 使用他的好处和坏处分别是什么?
什么是 "use strict"?
"use strict" 是 JavaScript 的一种严格模式声明,通过在脚本或函数顶部添加这一指令来启用。它会对 JavaScript 代码执行更严格的解析和错误处理。
使用严格模式的好处
- 减少错误:帮助开发者避免常见的编码错误
- 提高安全性:防止意外创建全局变量
- 禁用不推荐的功能:如 with 语句和 arguments.callee
- 优化性能:使 JavaScript 引擎更容易进行优化
- 为未来版本做准备:限制可能在未来版本中被移除的语法
使用严格模式的潜在缺点
- 兼容性问题:旧浏览器可能不完全支持
- 调试困难:某些在普通模式下不会报错的行为会抛出异常
- 代码迁移成本:将现有代码转换为严格模式可能需要大量修改
如何使用
在脚本或函数开头添加:
"use strict";
// 你的代码
或者在模块中自动启用(ES6模块默认启用严格模式)
53.阐述JavaScript 事件委托是什么?
JavaScript 事件委托
事件委托是一种利用事件冒泡机制的优化技术。其核心思想是:将事件监听器绑定在父元素上,而非直接绑定在多个子元素上。
工作原理
- 事件从触发元素开始向上冒泡
- 父元素捕获到冒泡事件
- 通过判断事件源(event.target)来执行相应逻辑
优势
- 性能优化:减少内存消耗,避免为大量子元素单独绑定事件
- 动态适配:自动适用于后续添加的新元素
- 简化代码:减少重复的事件绑定操作
适用场景
- 列表项(如ul-li)的事件处理
- 大量相似元素的事件监听
- 动态生成的元素需要事件绑定
实现示例
document.getElementById('parent').addEventListener('click', function(e) {
if(e.target && e.target.tagName == 'LI') {
console.log('列表项被点击:', e.target.textContent);
}
});
这种模式特别适合处理包含大量子元素的容器,能显著提升页面性能。
55.简述在JavaScript 中什么是伪数组?如何伪数组转换成标准数组?
在 JavaScript 中,伪数组是指具有类似数组特性(如 length 属性和数字索引)但并非真正数组的对象。这类对象通常无法直接使用数组方法。
要将伪数组转换为标准数组,可以使用以下方法:
- Array.from() 方法
- 扩展运算符(...)
- 借用数组的 slice 方法(如 Array.prototype.slice.call())
这些转换方式都能让伪数组获得标准数组的所有功能和方法。
56.简述对webpack 的理解?
Webpack 是一个现代 JavaScript 应用程序的静态模块打包工具。它的核心功能包括:
-
模块化支持:能够处理各种模块依赖关系,支持 CommonJS、AMD、ES6 等模块规范
-
代码转换:通过 loader 机制转换不同类型的文件(如 TypeScript、Sass 等)
-
代码优化:具备代码分割(Code Splitting)、Tree Shaking 等优化能力
-
插件系统:丰富的插件生态可以扩展功能
-
开发支持:提供开发服务器、热更新等开发辅助功能
Webpack 通过构建依赖图来管理项目资源,最终生成优化后的静态文件,是前端工程化的重要工具。
57. 简述你如何给一个事件处理函数命名空间,为什么要这样做?
如何给事件处理函数命名空间
-
使用前缀命名法:
function myApp_handleClick() { // 函数逻辑 } -
使用对象封装:
const myApp = { handlers: { click: function() { // 点击事件处理 }, scroll: function() { // 滚动事件处理 } } }; -
模块模式:
(function() { function privateHandleClick() { // 私有函数 } window.myApp = { publicHandleClick: function() { // 公开函数 } }; })(); -
ES6类:
class EventHandlers { static formSubmit() { // 表单提交处理 } static buttonClick() { // 按钮点击处理 } }
为什么要使用命名空间
-
避免全局污染:
- 防止与其他库或脚本的同名函数冲突
- 减少全局作用域中的变量数量
-
提高代码组织性:
- 将相关功能分组,便于维护
- 形成清晰的代码结构,如
app.ui.handleClick
-
增强可读性:
- 通过命名空间可以直观了解函数所属模块
- 例如
userAuth.validate()比单纯的validate()更清晰
-
便于团队协作:
- 不同开发者可以安全地添加功能而不会相互干扰
- 减少代码合并时的冲突可能性
-
模块化开发:
- 支持按需加载特定功能模块
- 有利于代码拆分和懒加载实现
-
调试便利性:
- 错误堆栈中能快速定位问题来源
- 控制台中可以方便地通过命名空间查找相关函数
实际应用示例:在大型单页应用中,使用类似 app.modules.user.loginHandlers.submit() 的结构可以清晰地表达函数层级关系,同时避免与第三方库可能存在的命名冲突。
58.JavaScript 中的split slice splice函数的区别?
-
split
字符串方法,用于将字符串按指定分隔符拆分成数组'a-b-c'.split('-') // ['a', 'b', 'c'] -
slice
数组/字符串方法,用于截取指定区间的元素(不改变原数组)[1,2,3].slice(1) // [2,3] 'abc'.slice(1) // 'bc' -
splice
数组专用方法,可在指定位置删除/添加元素(会修改原数组)const arr = [1,2,3] arr.splice(1,1) // 返回[2],arr变为[1,3]
59.全面阐述 JavaScript es6 的理解?
- 变量声明优化
- let/const 取代 var:解决变量提升问题,提供块级作用域
- const 定义常量,确保引用不变性
- 箭头函数革新
- 简化函数语法:() => {}
- 自动绑定 this 上下文
- 适用于回调函数等简洁场景
- 解构赋值
- 数组/对象快速解构提取值
- 支持嵌套结构和默认值
- 简化数据访问逻辑
- 模板字符串
- 支持多行文本定义
- 内置表达式插值功能
- 增强字符串处理能力
- 扩展运算符
- ... 语法实现数组/对象展开
- 简化合并/复制操作
- 支持函数参数展开
- 模块化系统
- import/export 标准语法
- 取代传统脚本加载方式
- 促进代码组织和复用
- 面向对象增强
- class 语法糖
- 继承机制优化
- 静态方法支持
- 异步处理改进
- Promise 标准化
- async/await 语法糖
- 完善错误处理机制
- 新数据结构
- Set/Map 集合类型
- WeakSet/WeakMap 弱引用
- 扩展数据处理能力
- 其他特性
- 默认参数值
- 剩余参数
- 属性简写
- 尾调用优化
这些特性共同提升了 JavaScript 的开发效率、代码质量和可维护性,使其成为现代 Web 开发的核心语言。
60.JavaScript 中的hoisting 是什么?
什么是 Hoisting
Hoisting(提升)是 JavaScript 引擎在代码执行前将变量和函数声明移动到它们所在作用域顶部的行为。这意味着在代码中,你可以在声明之前使用变量或调用函数。
变量提升的表现形式
1. 变量声明提升
console.log(a); // 输出:undefined
var a = 5;
console.log(a); // 输出:5
在这个例子中,虽然a在console.log之后才声明和赋值,但由于提升,第一次console.log不会报错,而是输出undefined。
2. 函数声明提升
sayHello(); // 输出:"Hello!"
function sayHello() {
console.log("Hello!");
}
函数声明会被完全提升,包括函数体,因此可以在声明前调用。
不同类型声明的提升差异
1. var 声明
- 只提升声明,不提升赋值
- 初始值为
undefined - 函数作用域
2. let 和 const 声明
- 提升声明但不初始化(暂时性死区)
- 在声明前访问会报错
- 块级作用域
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 10;
3. 函数表达式
- 行为取决于使用
var还是let/const - 只有变量声明被提升,函数体不提升
greet(); // TypeError: greet is not a function
var greet = function() {
console.log("Hi there!");
}
实际开发中的注意事项
-
避免在声明前使用变量:虽然提升允许这样做,但会降低代码可读性。
-
优先使用
let和const:可以避免变量提升带来的意外行为。 -
函数声明 vs 函数表达式:如果需要在定义前调用函数,使用函数声明而不是函数表达式。
-
类声明不会被提升:与函数声明不同,类声明不会提升。
const p = new Person(); // ReferenceError: Cannot access 'Person' before initialization
class Person {
constructor() {}
}
底层原理
JavaScript 引擎在编译阶段会扫描代码,收集所有变量和函数声明,并在内存中创建它们。这就是为什么可以在声明前访问这些标识符。但实际赋值操作仍然在代码执行到相应位置时才会发生。
61.简述JavaScript isNaN()函数?
isNaN() 是 JavaScript 中的一个全局函数,用于检测一个值是否为"非数字"(Not-a-Number)。以下是关于该函数的详细说明:
基本功能
isNaN() 函数会尝试将传入的值转换为数字类型,然后判断转换结果是否为 NaN(非数字值)。
isNaN(123) // false
isNaN('123') // false
isNaN('abc') // true
isNaN(NaN) // true
特殊行为
-
空字符串和空格:
isNaN('') // false (被转换为0) isNaN(' ') // false (被转换为0) -
布尔值:
isNaN(true) // false (被转换为1) isNaN(false) // false (被转换为0) -
日期对象:
isNaN(new Date()) // false (返回时间戳数字) -
对象和数组:
isNaN({}) // true isNaN([1,2]) // true isNaN([1]) // false (数组包含单个数字元素时)
与 Number.isNaN() 的区别
ES6 引入了 Number.isNaN(),它的行为更严谨,不会进行类型转换:
isNaN('abc') // true
Number.isNaN('abc') // false (因为'abc'本身不是NaN)
isNaN(undefined) // true
Number.isNaN(undefined) // false
实际应用场景
-
表单验证:
function validateInput(input) { if (isNaN(input)) { alert('请输入有效的数字'); } } -
数据处理:
const data = [1, '2', 'three', 4, NaN]; const cleanData = data.filter(item => !isNaN(item)); // 结果: [1, '2', 4]
注意事项
isNaN()会首先尝试将参数转换为数字,这可能导致意外的结果- 对于严格的数字检查,推荐使用
Number.isNaN()或typeof value === 'number' && isNaN(value) - 在 ES6+ 环境中,也可以考虑使用
Object.is(value, NaN)进行判断
62.JavaScript 中的负无穷大是什么?
在 JavaScript 中,负无穷大(-Infinity)是一个特殊的数值,表示比任何有限数字都小的概念。它通常出现在以下场景:
- 数学运算结果超出最小可表示范围时
- 将负数除以零时(如:-1/0)
- 显式调用
Number.NEGATIVE_INFINITY属性
其主要特点包括:
- 任何数与负无穷大相加都等于负无穷大
- 是全局对象 Number 的静态属性
- 在比较运算中,小于所有有限数值(包括 Number.MIN_VALUE)
- 使用
isFinite()检测时会返回 false
注意:负无穷大与正无穷大(Infinity)是不同的概念,两者在数值比较和数学运算中表现不同。
63.JavaScript 什么是未声明变量?未定义的变量怎么样?
JavaScript 中未声明变量和未定义变量的区别:
- 未声明变量:
- 指从未使用 var/let/const 声明过的变量
- 直接访问会抛出 ReferenceError 异常
- 严格模式下会直接报错
- 未定义变量:
- 指已声明但未赋值的变量
- 值为 undefined
- 不会报错但可能引发逻辑错误
示例:
console.log(a); // 未声明变量 - 报错
let b; // 已声明未赋值
console.log(b); // 输出 undefined
最佳实践:始终在使用前声明变量并初始化。
64.JavaScript 隐式类型强制有什么作用?举个例子?
JavaScript 隐式类型强制的作用与示例
隐式类型强制是 JavaScript 在运算或比较时自动将值从一种类型转换为另一种类型的机制。这种特性既有积极作用也有潜在风险。
主要作用
- 简化开发:让开发者不必显式处理类型转换,减少代码量
- 提高灵活性:允许不同类型的值直接运算
- 增强容错性:在类型不匹配时尝试自动转换而非直接报错
典型示例
1. 字符串连接运算
let num = 42;
let str = "The answer is: " + num; // 数字自动转为字符串
console.log(str); // 输出 "The answer is: 42"
2. 数学运算中的类型转换
let result = "10" - 5; // 字符串转为数字
console.log(result); // 5 (数字类型)
3. 比较运算中的类型转换
console.log(1 == "1"); // true (字符串转为数字)
console.log(0 == false); // true (布尔值转为数字)
4. 逻辑运算中的类型转换
if ("hello") { // 非空字符串转为true
console.log("This will execute");
}
if (0) { // 0转为false
console.log("This won't execute");
}
5. 对象到原始值的转换
let obj = {
value: 10,
toString() {
return "15";
},
valueOf() {
return this.value;
}
};
console.log(obj + 5); // 15 (调用valueOf())
console.log(String(obj)); // "15" (调用toString())
注意事项
- 隐式转换可能导致意外结果,建议使用严格相等
===避免问题 - 复杂的隐式转换规则可能降低代码可读性
- 重要场景建议显式使用
Number(),String()等转换函数
65.JavaScript 是静态类型语言 还是动态类型语言?这是什么意思?
JavaScript 属于动态类型语言。这意味着:
- 变量类型在运行时确定,而非编译时
- 同一个变量可以在不同时刻存储不同类型的数据
- 不需要显式声明变量类型
这种特性与静态类型语言(如 Java、C++)形成对比,后者需要在编译时明确指定变量类型且类型不可变。
67.解释JavaScript 中的展开运算符是什么?
基本概念
展开运算符(Spread Operator)是ES6引入的一种新语法,使用三个连续的点(...)表示。它主要用于将一个可迭代对象(如数组、字符串或类数组对象)"展开"为多个元素。
主要用途
1. 数组操作
合并数组:
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]
复制数组:
const original = [1, 2, 3];
const copy = [...original]; // 创建新数组,而非引用
在特定位置插入元素:
const numbers = [1, 2, 5, 6];
const newNumbers = [...numbers.slice(0, 2), 3, 4, ...numbers.slice(2)];
// [1, 2, 3, 4, 5, 6]
2. 函数调用
替代apply方法:
function sum(a, b, c) {
return a + b + c;
}
const nums = [1, 2, 3];
// 传统方式
sum.apply(null, nums); // 6
// 使用展开运算符
sum(...nums); // 6
3. 对象操作
复制和合并对象:
const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };
const merged = { ...obj1, ...obj2 }; // { a: 1, b: 2, c: 3, d: 4 }
覆盖属性:
const defaultSettings = { theme: 'light', fontSize: 16 };
const userSettings = { theme: 'dark' };
const finalSettings = { ...defaultSettings, ...userSettings };
// { theme: 'dark', fontSize: 16 }
注意事项
- 展开运算符执行的是浅拷贝,对于嵌套对象或数组,只复制第一层的引用
- 在对象展开时,后展开的属性会覆盖前面的同名属性
- 展开运算符只能用于可迭代对象(数组、字符串、Map、Set等)和对象字面量
实际应用场景
- React中的props传递:
function Parent() {
const props = { value: 1, onChange: handleChange };
return <Child {...props} />;
}
- 移除数组中的某个元素:
const removeItem = (arr, index) => {
return [...arr.slice(0, index), ...arr.slice(index + 1)];
}
- 处理函数的不定参数:
function logArgs(...args) {
console.log(args);
}
展开运算符极大地简化了JavaScript中对数组和对象的操作,使代码更加简洁和易读。
68.JavaScript 中如何处理异常?
基础异常处理
JavaScript 提供了 try...catch...finally 语句来处理异常:
try {
// 可能会抛出异常的代码
const result = riskyOperation();
console.log('操作成功:', result);
} catch (error) {
// 异常处理逻辑
console.error('操作失败:', error.message);
} finally {
// 无论是否发生异常都会执行的代码
console.log('操作完成');
}
关键点说明
- try 块:包含可能抛出异常的代码
- catch 块:捕获并处理异常,接收一个 Error 对象
- finally 块:用于清理资源,无论是否发生异常都会执行
Error 对象类型
JavaScript 提供了多种内置错误类型:
Error- 基础错误类型SyntaxError- 语法错误TypeError- 类型错误(如访问未定义变量)ReferenceError- 引用错误RangeError- 数值超出范围URIError- URI 处理错误EvalError- eval() 函数相关错误
try {
null.function(); // 会抛出 TypeError
} catch (error) {
if (error instanceof TypeError) {
console.log('捕获到类型错误');
} else {
console.log('捕获到其他错误');
}
}
手动抛出异常
使用 throw 语句可以主动抛出异常:
function divide(a, b) {
if (b === 0) {
throw new Error('除数不能为零');
}
return a / b;
}
try {
divide(10, 0);
} catch (error) {
console.error(error.message); // 输出: "除数不能为零"
}
Promise 异常处理
对于异步代码,可以使用 Promise 的 .catch() 方法:
fetch('https://api.example.com/data')
.then(response => {
if (!response.ok) {
throw new Error('网络响应不正常');
}
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error('请求失败:', error));
async/await 异常处理
使用 try...catch 处理 async 函数中的异常:
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
} catch (error) {
console.error('获取数据失败:', error);
}
}
全局错误处理
- window.onerror - 全局错误处理
window.onerror = function(message, source, lineno, colno, error) {
console.error('全局错误:', message, '位于', source, lineno, '行', colno, '列');
return true; // 阻止默认错误处理
};
- unhandledrejection 事件 - 处理未捕获的 Promise 拒绝
window.addEventListener('unhandledrejection', event => {
console.error('未处理的 Promise 拒绝:', event.reason);
event.preventDefault(); // 阻止默认处理
});
最佳实践
- 明确错误类型:针对不同错误类型进行特定处理
- 提供有意义的错误信息:帮助调试和问题定位
- 不要静默吞掉错误:至少记录错误信息
- 区分预期错误和程序错误:预期错误应被捕获处理,程序错误可能需要终止执行
- 清理资源:使用 finally 块或 try...finally 确保资源释放
function processFile(filename) {
let fileHandle;
try {
fileHandle = openFile(filename);
const content = readFile(fileHandle);
return processContent(content);
} catch (error) {
if (error instanceof FileNotFoundError) {
console.error('文件未找到:', filename);
return null;
} else if (error instanceof PermissionError) {
console.error('权限不足,无法读取文件:', filename);
throw error; // 重新抛出无法处理的错误
} else {
console.error('处理文件时发生未知错误:', error);
throw error;
}
} finally {
if (fileHandle) {
closeFile(fileHandle);
}
}
}
69.解释JavaScript 中的作用域是什么意思?
什么是作用域
作用域(Scope)在JavaScript中指的是变量、函数和对象的可访问范围,即代码中定义变量的区域,决定了变量在程序中的可见性和生命周期。作用域控制了变量和函数的可见性,以及变量的生命周期。
JavaScript中的作用域类型
1. 全局作用域(Global Scope)
- 定义在函数外部的变量拥有全局作用域
- 全局变量可以在程序的任何地方被访问和修改
- 在浏览器环境中,全局作用域是window对象
- 例子:
var globalVar = "I'm global"; function checkScope() { console.log(globalVar); // 可以访问 }
2. 函数作用域(Function Scope/Local Scope)
- 定义在函数内部的变量只能在函数内部访问
- 每次函数调用都会创建一个新的作用域
- 例子:
function myFunction() { var localVar = "I'm local"; console.log(localVar); // 可以访问 } console.log(localVar); // 报错:localVar未定义
3. 块级作用域(Block Scope)
- 由ES6引入,使用let和const声明的变量具有块级作用域
- 块级作用域由一对花括号{}界定
- 例子:
if (true) { let blockVar = "I'm block scoped"; const constVar = "I'm also block scoped"; console.log(blockVar); // 可以访问 } console.log(blockVar); // 报错:blockVar未定义
作用域链(Scope Chain)
JavaScript使用作用域链来查找变量:
- 当需要访问一个变量时,JavaScript引擎会首先在当前作用域查找
- 如果找不到,会向上一级作用域查找,直到全局作用域
- 如果全局作用域也找不到,则报错
例子:
var globalVar = "global";
function outer() {
var outerVar = "outer";
function inner() {
var innerVar = "inner";
console.log(innerVar); // "inner" - 当前作用域
console.log(outerVar); // "outer" - 上级作用域
console.log(globalVar); // "global" - 全局作用域
}
inner();
}
特殊作用域情况
1. 变量提升(Hoisting)
- var声明的变量会被提升到当前作用域的顶部
- 函数声明也会被提升
- 例子:
console.log(hoistedVar); // undefined(不会报错) var hoistedVar = "I was hoisted"; hoistedFunc(); // "I was hoisted too" function hoistedFunc() { console.log("I was hoisted too"); }
2. 闭包(Closures)
- 内部函数可以访问外部函数的作用域,即使外部函数已经执行完毕
- 常用于数据封装和私有变量
- 例子:
function createCounter() { let count = 0; return function() { count++; return count; }; } const counter = createCounter(); console.log(counter()); // 1 console.log(counter()); // 2
最佳实践
- 尽量使用let和const代替var,避免变量提升带来的意外行为
- 避免污染全局作用域,可以使用IIFE或模块模式
- 合理使用闭包,但要注意内存泄漏问题
- 使用严格模式("use strict")可以帮助发现作用域相关的问题
理解JavaScript作用域是掌握这门语言的关键基础之一,它直接影响代码的执行结果、变量的生命周期以及内存管理。
70.简述JavaScript 中的高阶函数?
什么是高阶函数
高阶函数(Higher-order function)是指能够接收其他函数作为参数,或者将函数作为返回值输出的函数。在JavaScript中,函数是一等公民(First-class citizen),这意味着函数可以像其他数据类型一样被传递和使用,这使得高阶函数成为JavaScript的重要特性。
常见的高阶函数示例
1. 接收函数作为参数的函数
数组方法:
Array.prototype.map()- 对数组每个元素执行回调函数
const numbers = [1, 2, 3];
const doubled = numbers.map(x => x * 2); // [2, 4, 6]
Array.prototype.filter()- 根据回调函数条件过滤数组元素
const words = ['spray', 'limit', 'elite', 'exuberant'];
const longWords = words.filter(word => word.length > 5); // ['exuberant']
Array.prototype.reduce()- 将数组元素累积为单个值
const sum = [1, 2, 3].reduce((acc, val) => acc + val, 0); // 6
2. 返回函数的函数
函数工厂:
function createMultiplier(multiplier) {
return function(x) {
return x * multiplier;
};
}
const double = createMultiplier(2);
console.log(double(5)); // 10
柯里化函数:
function add(a) {
return function(b) {
return a + b;
};
}
const add5 = add(5);
console.log(add5(3)); // 8
高阶函数的优势
- 代码复用性:通过将通用逻辑抽象为高阶函数,可以减少重复代码
- 可读性:高阶函数可以使代码更声明式和易于理解
- 灵活性:通过传递不同的函数参数,可以轻松改变高阶函数的行为
- 函数组合:便于创建更复杂的函数组合和管道
实际应用场景
- 事件处理:在DOM事件监听器中传递回调函数
- 异步操作:Promise的
.then()和.catch()方法接收函数参数 - 中间件模式:如Express.js中的中间件函数
- 函数装饰器:增强现有函数的功能而不修改其源代码
JavaScript的函数式编程特性很大程度上依赖于高阶函数的使用,这使得代码更加模块化、可维护和可测试。
71.简述JavaScript 中什么是柯里化?
JavaScript 中的柯里化(Currying)是一种将多参数函数转换为一系列单参数函数的技术。通过柯里化,一个接收多个参数的函数会被分解为多个嵌套的函数,每个函数只接收一个参数,并返回接收下一个参数的函数,直到所有参数都被处理完毕,最终返回结果。
基本概念
柯里化的核心思想是函数的分步调用,它允许开发者将函数的调用过程拆解为多个步骤,每个步骤只需传入一个参数。例如,一个接收三个参数的函数 f(a, b, c) 经过柯里化后会变为 f(a)(b)(c) 的形式。
示例
原始函数
function add(a, b, c) {
return a + b + c;
}
console.log(add(1, 2, 3)); // 输出 6
柯里化后的函数
function curriedAdd(a) {
return function(b) {
return function(c) {
return a + b + c;
};
};
}
console.log(curriedAdd(1)(2)(3)); // 输出 6
柯里化的优势
-
参数复用:柯里化后的函数可以固定部分参数,生成新的函数,减少重复代码。例如:
const addTwo = curriedAdd(1)(2); // 固定 a=1 和 b=2 console.log(addTwo(3)); // 输出 6 console.log(addTwo(4)); // 输出 7 -
延迟执行:柯里化可以将函数调用拆分为多个步骤,适合需要逐步传入参数的场景。
-
函数组合:柯里化后的函数更容易与其他高阶函数结合,实现更复杂的逻辑。
通用柯里化实现
以下是一个通用的柯里化工具函数,可以将任意函数柯里化:
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...nextArgs) {
return curried.apply(this, args.concat(nextArgs));
};
}
};
}
// 使用示例
const curriedSum = curry(add);
console.log(curriedSum(1)(2)(3)); // 输出 6
console.log(curriedSum(1, 2)(3)); // 输出 6
应用场景
柯里化常用于函数式编程中,例如:
- 事件处理:固定部分参数(如事件类型),生成特定的事件监听函数。
- 配置函数:通过柯里化生成具有默认配置的函数。
- 数据处理:在数据转换或流水线操作中逐步处理参数。
柯里化是 JavaScript 函数式编程的重要技术之一,能够提升代码的灵活性和复用性。
72.解释什么是JavaScript 中的承诺(promise)?
JavaScript 中的承诺(Promise)
基本概念
JavaScript 中的 Promise(承诺) 是一种用于处理异步操作的对象,它代表了一个异步操作的最终完成(或失败)及其结果值。Promise 是现代 JavaScript 异步编程的核心机制之一,用于替代传统的回调函数模式,解决了"回调地狱"问题。
Promise 的状态
一个 Promise 对象有三种可能的状态:
- pending(待定):初始状态,既不是成功,也不是失败状态
- fulfilled(已兑现):意味着操作成功完成
- rejected(已拒绝):意味着操作失败
状态一旦确定(fulfilled 或 rejected)就不可再改变,这称为"已定型"(settled)。
创建 Promise
const myPromise = new Promise((resolve, reject) => {
// 异步操作
if (/* 操作成功 */) {
resolve(value); // 将Promise状态改为fulfilled
} else {
reject(error); // 将Promise状态改为rejected
}
});
Promise 的方法
then()
then() 方法用于为 Promise 实例添加状态改变时的回调函数:
promise.then(
function(value) { /* 成功时的处理 */ },
function(error) { /* 失败时的处理 */ }
);
catch()
catch() 方法用于指定发生错误时的回调函数:
promise.catch(function(error) {
// 处理错误
});
finally()
finally() 方法用于指定不管 Promise 对象最后状态如何,都会执行的操作:
promise.finally(function() {
// 无论成功或失败都会执行
});
Promise 的链式调用
Promise 的一个主要优势是支持链式调用:
doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => {
console.log('最终结果:', finalResult);
})
.catch(failureCallback);
静态方法
Promise.all()
等待所有 Promise 完成,或者任何一个 Promise 失败:
Promise.all([promise1, promise2, promise3])
.then(values => {
console.log(values); // [result1, result2, result3]
})
.catch(error => {
console.error(error);
});
Promise.race()
返回第一个 settled 的 Promise(无论成功或失败):
Promise.race([promise1, promise2])
.then(value => {
console.log(value); // 第一个完成的Promise的结果
});
Promise.allSettled()
等到所有 Promise 都已 settled(每个 Promise 都已完成或拒绝):
Promise.allSettled([promise1, promise2])
.then(results => {
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log(result.value);
} else {
console.log(result.reason);
}
});
});
Promise.any()
返回第一个 fulfilled 的 Promise:
Promise.any([promise1, promise2])
.then(value => {
console.log(value); // 第一个成功的Promise的结果
})
.catch(error => {
console.log(error.errors); // 所有Promise都失败时的错误数组
});
实际应用示例
异步获取数据
function fetchData(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = () => resolve(xhr.responseText);
xhr.onerror = () => reject(xhr.statusText);
xhr.send();
});
}
fetchData('https://api.example.com/data')
.then(data => {
console.log('数据获取成功:', data);
return JSON.parse(data);
})
.then(parsedData => {
// 处理解析后的数据
})
.catch(error => {
console.error('请求失败:', error);
});
多个异步操作顺序执行
function asyncOperation1() {
return new Promise(resolve => setTimeout(() => resolve('结果1'), 1000));
}
function asyncOperation2(data) {
return new Promise(resolve => setTimeout(() => resolve(data + ' 结果2'), 1000));
}
asyncOperation1()
.then(result1 => {
console.log(result1);
return asyncOperation2(result1);
})
.then(result2 => {
console.log(result2);
});
Promise 的优势
- 更好的错误处理:通过
.catch()可以集中处理错误 - 避免回调地狱:链式调用使代码更易读
- 组合多个异步操作:使用
Promise.all()等静态方法可以方便地组合多个异步操作 - 可预测性:Promise 的状态一旦确定就不会改变
Promise 是现代 JavaScript 异步编程的基础,也是 async/await 语法的基础。理解 Promise 对于编写高质量的 JavaScript 代码至关重要。
73.解释JavaScript 中的回调函数?
什么是回调函数
回调函数(Callback Function)是指作为参数传递给另一个函数的函数,这个函数会在特定事件发生或特定条件满足时被调用执行。在JavaScript中,回调函数是实现异步编程的基础模式之一。
基本概念
回调函数的核心思想是"稍后执行"(deferred execution)。当我们将一个函数A作为参数传递给另一个函数B时,函数A就成为了回调函数,函数B可以在适当的时候调用它。
回调函数的常见用途
1. 异步操作处理
JavaScript作为单线程语言,使用回调函数处理异步操作非常重要:
// 读取文件的异步操作
fs.readFile('example.txt', 'utf8', function(err, data) {
if (err) {
console.error('读取文件出错:', err);
return;
}
console.log('文件内容:', data);
});
2. 事件处理
DOM事件监听器就是典型的回调函数应用:
document.getElementById('myButton').addEventListener('click', function() {
console.log('按钮被点击了!');
});
3. 定时器
定时器函数setTimeout和setInterval也使用回调:
setTimeout(function() {
console.log('3秒后执行');
}, 3000);
4. 数组方法
许多数组方法接受回调函数作为参数:
const numbers = [1, 2, 3, 4];
const doubled = numbers.map(function(num) {
return num * 2;
});
回调函数的特点
1. 执行时机
回调函数不是立即执行的,而是由接收它的函数决定何时调用。例如:
function doSomething(callback) {
console.log('开始执行');
// 模拟耗时操作
setTimeout(function() {
console.log('操作完成');
callback(); // 在此处调用回调函数
}, 1000);
}
doSomething(function() {
console.log('回调函数被调用');
});
2. 参数传递
回调函数可以接收参数:
function calculate(a, b, operation) {
return operation(a, b);
}
const result = calculate(5, 3, function(x, y) {
return x * y;
});
3. 上下文(this)问题
在回调函数中,this的指向可能会改变:
const obj = {
name: 'My Object',
logName: function() {
console.log(this.name);
}
};
// 正确调用
obj.logName(); // 输出: My Object
// 作为回调函数时this可能丢失
setTimeout(obj.logName, 100); // 输出可能是undefined
可以使用bind方法解决:
setTimeout(obj.logName.bind(obj), 100); // 正确输出: My Object
回调地狱问题
当多个异步操作需要顺序执行时,会出现嵌套回调,形成"金字塔"代码:
fs.readFile('file1.txt', function(err, data1) {
if (err) throw err;
fs.readFile('file2.txt', function(err, data2) {
if (err) throw err;
fs.writeFile('output.txt', data1 + data2, function(err) {
if (err) throw err;
console.log('操作完成');
});
});
});
解决方案
- 命名函数:将回调函数定义为命名函数,减少嵌套
- Promise:使用Promise链式调用
- async/await:使用ES7的async/await语法
回调函数的优缺点
优点
- 简单直观,易于理解
- 是JavaScript异步编程的基础
- 广泛应用于各种库和框架中
缺点
- 回调嵌套会导致代码难以维护(回调地狱)
- 错误处理不便(需要在每个回调中处理错误)
- 控制流不够直观
最佳实践
- 尽早处理错误
- 保持回调函数简洁
- 考虑使用Promise或async/await替代复杂回调
- 避免过深的嵌套层级
- 合理使用命名函数提高代码可读性
回调函数是JavaScript异步编程的基石,虽然现代JavaScript引入了Promise和async/await等更优雅的解决方案,但理解回调函数的工作原理对于掌握JavaScript仍然至关重要。
74.简述为什么在JavaScript中使用回调?
在JavaScript中使用回调函数的主要原因包括:
- 异步编程的需求: JavaScript是单线程语言,为了处理耗时操作(如网络请求、文件读写等)而不阻塞主线程,需要使用回调函数。当异步操作完成时,通过回调函数通知主线程继续执行相关逻辑。例如:
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => {
// 这里是回调函数
console.log(data);
});
- 事件驱动编程: JavaScript广泛应用于浏览器环境,需要响应用户交互事件(如点击、滚动等)。回调函数是处理这些事件的天然方式。例如:
document.getElementById('myButton').addEventListener('click', function() {
// 点击事件回调
alert('Button clicked!');
});
- 高阶函数的支持: JavaScript中函数是一等公民,可以像变量一样传递和使用。这使得回调模式成为实现高阶函数的自然选择。例如Array的map方法:
const numbers = [1, 2, 3];
const doubled = numbers.map(function(num) {
// 回调函数处理每个元素
return num * 2;
});
-
历史原因: 在Promise和async/await出现之前,回调是JavaScript处理异步操作的主要方式。虽然现在有更现代的替代方案,但很多遗留代码库仍大量使用回调。
-
模块化和代码复用: 回调允许将特定逻辑封装为函数,便于复用和模块化。例如Node.js中的文件读取:
const fs = require('fs');
fs.readFile('example.txt', 'utf8', function(err, data) {
// 文件读取完成后的回调处理
if (err) throw err;
console.log(data);
});
需要注意的是,过度使用回调会导致"回调地狱"(Callback Hell)问题,即多层嵌套的回调使代码难以维护。现代JavaScript开发中,通常会使用Promise、async/await等更优雅的异步处理方式。
75.简述什么是JavaScript 的严格模式?
JavaScript 的严格模式是一种限制性更强的 JavaScript 执行环境,它通过更严格的语法检查来消除代码中的潜在问题。严格模式的主要特点包括:禁止使用未声明的变量、限制对只读属性的修改、禁止删除不可删除的属性、要求函数参数名称唯一等。开发者可以通过在脚本或函数开头添加 "use strict"; 指令来启用严格模式。这种模式有助于编写更安全、更规范的代码,并能提前发现潜在错误。
76.解释什么是JavaScript 立即调用函数?
JavaScript 立即调用函数(IIFE)
立即调用函数表达式(Immediately Invoked Function Expression,简称IIFE)是JavaScript中一种特殊的函数定义和执行方式。它的主要特点是在定义函数的同时立即执行该函数。
基本语法
IIFE有两种常见的语法形式:
// 第一种形式:将整个函数体用括号包裹
(function() {
// 函数体
})();
// 第二种形式:将函数定义部分用括号包裹
(function() {
// 函数体
}());
这两种形式在功能上是等价的,都是有效的IIFE写法。
主要特点
- 立即执行:函数定义后会立即执行,不需要显式调用
- 私有作用域:创建一个独立的作用域,避免变量污染全局命名空间
- 一次性使用:通常只执行一次,不会在其他地方被调用
实际应用场景
1. 创建私有变量
(function() {
var privateVar = "这个变量外部无法访问";
console.log(privateVar); // 可以访问
})();
console.log(privateVar); // 报错:privateVar is not defined
2. 模块化开发
var myModule = (function() {
var privateCounter = 0;
function increment() {
privateCounter++;
}
function getCount() {
return privateCounter;
}
return {
increment: increment,
getCount: getCount
};
})();
myModule.increment();
console.log(myModule.getCount()); // 输出:1
3. 解决循环中的变量作用域问题
for(var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function() {
console.log(j);
}, 1000);
})(i);
}
// 输出:0 1 2 3 4(间隔1秒)
如果不使用IIFE,所有setTimeout回调都会共享同一个i变量,最终输出5个5。
4. 避免命名冲突
(function($) {
// 在这个作用域内,$被锁定为jQuery
$('body').css('background', 'blue');
})(jQuery);
带参数的IIFE
IIFE也可以接收参数:
(function(name) {
console.log("Hello, " + name);
})("World"); // 输出:Hello, World
现代JavaScript中的替代方案
随着ES6的普及,IIFE的一些用途可以被以下特性替代:
- 块级作用域:使用
let和const替代var - 模块系统:使用ES6的
import/export
但IIFE仍然在很多场景下是有用的,特别是在需要立即执行一段代码并保持变量私有化的场合。
77.JavaScript 如何删除属性及其值?
1. delete 操作符
最常见的删除属性方法是使用 delete 操作符:
const person = {
name: 'John',
age: 30,
occupation: 'Developer'
};
// 删除 occupation 属性
delete person.occupation;
console.log(person);
// 输出: {name: "John", age: 30}
注意事项:
delete操作符会完全移除属性及其值- 删除成功时返回
true,否则返回false - 无法删除继承的属性或使用
Object.defineProperty()设置为不可配置的属性
2. 使用对象解构赋值
ES6 引入的解构赋值也可以用来"删除"属性:
const { occupation, ...rest } = person;
console.log(rest);
// 输出: {name: "John", age: 30}
这种方法实际上创建了一个新对象,不包含要删除的属性,原对象保持不变。
3. Reflect.deleteProperty()
ES6 新增的 Reflect API 提供了更规范的删除属性方法:
Reflect.deleteProperty(person, 'age');
console.log(person);
// 输出: {name: "John"}
4. 设置属性为 undefined/null
虽然不是真正的删除,但有时会把属性值设为 undefined 或 null:
person.age = undefined;
console.log(person);
// 输出: {name: "John", age: undefined}
与 delete 的区别:
- 属性仍然存在于对象中
- 在遍历时仍然会被枚举到
- 可能影响某些依赖属性存在性检查的代码
应用场景比较
| 方法 | 适用场景 | 性能 | 是否改变原对象 |
|---|---|---|---|
| delete | 需要彻底移除属性 | 中等 | 是 |
| 解构赋值 | 需要保留原对象 | 较低 | 否 |
| Reflect.deleteProperty | 需要更规范的API | 中等 | 是 |
| 设为undefined | 需要保留属性键 | 高 | 是 |
在大多数情况下,delete 操作符是最简单直接的选择,特别是在需要真正移除属性的场景中。
78.如何检查JavaScript 中 变量的类型?
检查 JavaScript 变量类型的几种方法:
-
typeof 运算符
适用于基本类型检测,返回类型字符串
例如:typeof "hello" // "string" typeof 42 // "number" -
instanceof 运算符
用于检测对象是否为特定类的实例
例如:[] instanceof Array // true new Date() instanceof Date // true -
Object.prototype.toString
最准确的类型检测方法,适用于所有类型
例如:Object.prototype.toString.call([]) // "[object Array]" Object.prototype.toString.call(null) // "[object Null]" -
Array.isArray()
专门用于检测数组类型
例如:Array.isArray([]) // true
注意:
- null 的 typeof 会返回 "object"(历史遗留问题)
- 基本类型和对象类型需要采用不同的检测方式
79.JavaScript 中 preventDefault () 方法有什么作用?
preventDefault() 方法用于阻止事件的默认行为。例如,点击链接时阻止页面跳转,或提交表单时阻止页面刷新。
80.JavaScript 中 setTimeout ,setInterval 方法?
setTimeout 方法
setTimeout 是 JavaScript 中用于在指定时间后执行代码的方法。它会在等待设定的毫秒数后执行一次回调函数。
基本语法
let timeoutID = setTimeout(function[, delay, arg1, arg2, ...]);
- function: 要执行的函数
- delay (可选): 延迟的毫秒数,默认为 0
- arg1, arg2,... (可选): 传递给函数的参数
示例
// 简单示例
setTimeout(() => {
console.log('这条消息将在1秒后显示');
}, 1000);
// 带参数的示例
function greet(name) {
console.log(`Hello, ${name}!`);
}
setTimeout(greet, 2000, 'Alice'); // 2秒后显示"Hello, Alice!"
清除定时器
可以使用 clearTimeout 取消尚未执行的 setTimeout:
let timerId = setTimeout(() => console.log('这条消息不会显示'), 1000);
clearTimeout(timerId);
应用场景
- 延迟执行某些操作
- 实现简单的动画效果
- 实现防抖(debounce)功能
- 超时处理
setInterval 方法
setInterval 方法会按照指定的时间间隔(以毫秒为单位)重复调用一个函数或执行一个代码片段。
基本语法
let intervalID = setInterval(function[, delay, arg1, arg2, ...]);
示例
// 每秒显示一次当前时间
let intervalId = setInterval(() => {
console.log(new Date().toLocaleTimeString());
}, 1000);
// 5秒后停止计时器
setTimeout(() => {
clearInterval(intervalId);
console.log('计时器已停止');
}, 5000);
清除定时器
使用 clearInterval 来停止 setInterval 的执行:
let counter = 0;
let intervalId = setInterval(() => {
console.log(`计数: ${++counter}`);
if (counter >= 5) {
clearInterval(intervalId);
console.log('计数器已停止');
}
}, 1000);
应用场景
- 轮询服务器获取更新
- 实现动画效果
- 定期执行后台任务
- 实现倒计时功能
重要注意事项
-
执行时机:定时器不能保证在精确的时间执行,因为JavaScript是单线程的,如果主线程被阻塞,定时器会被推迟执行。
-
最小延迟:在浏览器中,嵌套的定时器最小延迟为4ms(连续调用5次后)。
-
this绑定:回调函数中的
this默认指向全局对象(严格模式下为undefined),可以使用箭头函数或bind来绑定正确的上下文。 -
性能考虑:频繁的短间隔定时器会影响性能,应谨慎使用。
-
替代方案:现代浏览器提供了
requestAnimationFrame用于动画,通常比setInterval更好。
高级用法示例
实现倒计时
function countdown(seconds) {
let timer = setInterval(() => {
console.log(seconds--);
if (seconds < 0) {
clearInterval(timer);
console.log('倒计时结束!');
}
}, 1000);
}
countdown(10);
实现防抖(debounce)函数
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// 使用示例
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', debounce(function() {
console.log('搜索:', this.value);
}, 300));
setTimeout 和 setInterval 是 JavaScript 异步编程的基本工具,合理使用它们可以创建各种定时任务和周期性操作。
81.简述什么是json stringify ?
JSON.stringify 是 JavaScript 中的一个方法,用于将 JavaScript 对象或值转换为 JSON 格式的字符串。它常用于数据序列化,以便存储或传输数据。例如,在向服务器发送数据前,可以先用该方法将对象转为字符串。
82.javascipt 如何将json 字符串转换为json 对象?
在 JavaScript 中,可以通过 JSON.parse() 方法将 JSON 字符串转换为 JSON 对象:
const jsonString = '{"name":"John", "age":30, "city":"New York"}';
const jsonObj = JSON.parse(jsonString);
console.log(jsonObj); // 输出: {name: "John", age: 30, city: "New York"}
83.JavaScript 中如何为变量分配默认值?
JavaScript 中为变量分配默认值的方法
在 JavaScript 中,有几种常见的方式可以为变量分配默认值,以下是详细说明:
1. 使用逻辑或运算符 (||)
这是 ES5 及之前版本常用的方法:
function greet(name) {
name = name || 'Guest';
console.log('Hello, ' + name);
}
特点:
- 当
name为 falsy 值(如null、undefined、''、0、false等)时,会使用 'Guest' - 适用于大多数情况,但不适用于需要区分 falsy 值的情况
2. 使用 ES6 默认参数
ES6 引入了更直观的函数参数默认值:
function greet(name = 'Guest') {
console.log('Hello, ' + name);
}
特点:
- 只有当
name严格等于undefined时才会使用默认值 - 更清晰易读,是推荐的做法
3. 使用解构赋值默认值
对于对象和数组的解构赋值也可以设置默认值:
// 对象解构
const { name = 'Guest', age = 18 } = user;
// 数组解构
const [firstName = 'John', lastName = 'Doe'] = names;
4. 使用空值合并运算符 (??)
ES2020 引入的空值合并运算符专门处理 null 和 undefined:
const userName = inputName ?? 'Anonymous';
特点:
- 只在左侧为
null或undefined时使用右侧默认值 - 不会将其他 falsy 值(如
0或'')视为无效
5. 实际应用场景
-
函数参数默认值:
function createUser(name = 'New User', isAdmin = false) { // 函数实现 } -
配置对象默认值:
function setup(options = {}) { const defaults = { color: 'red', size: 'medium', enabled: true }; const settings = { ...defaults, ...options }; } -
API 响应处理:
const { data = [], status = 'loading' } = await fetchData();
选择哪种方法取决于你的具体需求,但 ES6 的默认参数语法通常是现代 JavaScript 开发中的首选方案。
84.简述JavaScript 标签中 defer 和 async 属性的区别?
基本概念
在 HTML 中引入 JavaScript 时,<script> 标签有两个重要的异步加载属性:defer 和 async。它们都用于优化页面加载性能,但工作方式有所不同。
主要区别
1. 执行时机
- defer:脚本会延迟到整个 HTML 文档解析完成(DOMContentLoaded 事件之前)才执行,且保持脚本的相对顺序
- async:脚本一旦下载完成就立即执行,执行时机不确定,可能会中断 HTML 解析
2. 执行顺序
- defer:多个 defer 脚本会按照它们在文档中出现的顺序依次执行
- async:多个 async 脚本的执行顺序无法保证,先下载完成的先执行
3. DOM 依赖关系
- defer:适合有 DOM 依赖的脚本,因为保证在 DOM 完全解析后执行
- async:适合独立脚本,如统计分析代码,不需要等待 DOM 完成
使用场景示例
defer 适用场景
<script defer src="library.js"></script>
<script defer src="app.js"></script> <!-- 确保 library.js 先执行 -->
适用于:
- 需要操作 DOM 的脚本
- 多个有依赖关系的脚本
- 希望不阻塞页面渲染的情况
async 适用场景
<script async src="analytics.js"></script>
<script async src="advertisement.js"></script> <!-- 执行顺序不重要 -->
适用于:
- 独立的第三方脚本
- 不操作 DOM 的自包含脚本
- 对执行顺序没有要求的脚本
注意事项
- 如果同时使用 defer 和 async,现代浏览器会优先采用 async 行为
- 内联脚本(没有 src 属性)的 defer 和 async 会被忽略
- defer 属性在 IE9 及以下版本有特殊行为,可能不会完全按照标准执行
可视化对比
HTML 解析开始
|
├── 普通 script: 立即下载并执行,阻塞 HTML 解析
|
├── defer script: 异步下载,HTML 解析完成后按顺序执行
|
└── async script: 异步下载,下载完成后立即执行(可能在任何时候)
理解这些差异可以帮助开发者根据具体需求选择最适合的脚本加载策略,优化页面性能。
87.列举几种类型的dom 节点?
以下是几种常见的 DOM 节点类型及其详细说明:
-
元素节点(Element Node)
- 代表 HTML 文档中的标签元素
- 例如:
<div>,<p>,<span>等 - 是最常用的节点类型,可以通过
document.createElement()创建
-
文本节点(Text Node)
- 包含元素中的文本内容
- 例如:
<p>这是一个段落</p>中的 "这是一个段落" - 可以通过
document.createTextNode()创建
-
属性节点(Attribute Node)
- 代表元素的属性
- 例如:
<img src="image.jpg">中的 "src" 属性 - 可以通过
element.getAttributeNode()获取
-
注释节点(Comment Node)
- 代表 HTML 文档中的注释内容
- 例如:
<!-- 这是一个注释 --> - 可以通过
document.createComment()创建
-
文档节点(Document Node)
- 代表整个文档对象
- 是 DOM 树的根节点
- 通过
document对象访问
-
文档类型节点(DocumentType Node)
- 代表文档类型声明
- 例如:
<!DOCTYPE html> - 可以通过
document.doctype访问
-
文档片段节点(DocumentFragment Node)
- 轻量级的文档容器
- 可以临时存储多个节点
- 通过
document.createDocumentFragment()创建
-
CDATA 节点(CDATA Section Node)
- 用于 XML 文档中的特殊文本块
- 语法:
<![CDATA[内容]]> - 在 HTML5 中已不再使用
每种节点类型都有对应的 nodeType 常量值,可以通过检查该属性来识别节点类型。例如:
88.如何在不支持JavaScript 的旧浏览器中隐藏JavaScript 代码?
在不支持 JavaScript 的老旧浏览器中隐藏 JavaScript 代码
以下是几种有效的方法:
- 注释标签法 在脚本前后添加 HTML 注释标记:
<script type="text/javascript">
<!--
// JavaScript 代码
//-->
</script>
- 外部脚本引用 将代码放在单独的 .js 文件中引用:
<script type="text/javascript" src="script.js"></script>
- 条件注释法(仅限 IE)
<!--[if IE]>
<script type="text/javascript">
// IE 专用代码
</script>
<![endif]-->
- noscript 标签 为不支持 JavaScript 的浏览器提供替代内容:
<noscript>
<p>您的浏览器不支持 JavaScript</p>
</noscript>
- 现代方法 使用 type="module" 特性,不支持该特性的浏览器会自动忽略:
<script type="module" src="modern.js"></script>
注意事项:
- 这些方法主要是为了兼容性,现代浏览器已不再需要
- 确保网站核心功能不依赖 JavaScript
- 考虑渐进增强的设计理念
90.JavaScript 如何实现异步编程?
1. 回调函数 (Callback)
回调函数是最基础的异步处理方式,将函数作为参数传递给另一个函数,在异步操作完成后执行。
function fetchData(callback) {
setTimeout(() => {
callback('数据加载完成');
}, 1000);
}
fetchData((result) => {
console.log(result); // 1秒后输出"数据加载完成"
});
常见应用场景
- 文件读写操作
- 定时器(setTimeout/setInterval)
- AJAX请求
- Node.js中的I/O操作
2. Promise 对象
Promise 是 ES6 引入的异步解决方案,解决了回调地狱问题。
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('数据加载成功');
// 或者 reject('数据加载失败');
}, 1000);
});
}
fetchData()
.then(result => {
console.log(result);
return '处理后的数据';
})
.then(processedData => {
console.log(processedData);
})
.catch(error => {
console.error(error);
});
Promise 方法
Promise.all(): 等待所有promise完成Promise.race(): 取最先完成的promisePromise.allSettled(): 等待所有promise完成或拒绝
3. Async/Await
ES2017 引入的语法糖,基于Promise,使异步代码看起来像同步代码。
async function getData() {
try {
const result = await fetchData();
console.log(result);
const processedData = await processData(result);
console.log(processedData);
} catch (error) {
console.error(error);
}
}
getData();
特点
- 代码更易读和维护
- 错误处理可以使用try-catch
- 必须配合async函数使用
4. 事件监听 (EventEmitter)
通过事件驱动的方式实现异步,常见于浏览器事件和Node.js环境。
// 浏览器事件示例
document.getElementById('myButton').addEventListener('click', () => {
console.log('按钮被点击');
});
// Node.js EventEmitter示例
const EventEmitter = require('events');
const emitter = new EventEmitter();
emitter.on('dataReady', (data) => {
console.log('收到数据:', data);
});
emitter.emit('dataReady', '一些数据');
5. Generator 函数
ES6 引入的生成器函数,通过 yield 暂停函数执行,适合复杂异步流程控制。
function* fetchDataGenerator() {
const data1 = yield fetch('/api/data1');
const data2 = yield fetch('/api/data2');
return [data1, data2];
}
function runGenerator(gen) {
const iterator = gen();
function iterate(iteration) {
if (iteration.done) return iteration.value;
return Promise.resolve(iteration.value)
.then(x => iterate(iterator.next(x)))
.catch(e => iterate(iterator.throw(e)));
}
return iterate(iterator.next());
}
runGenerator(fetchDataGenerator);
6. Web Workers
用于在后台线程执行耗时操作,不影响主线程。
// 主线程
const worker = new Worker('worker.js');
worker.postMessage('开始计算');
worker.onmessage = (e) => {
console.log('收到结果:', e.data);
};
// worker.js
self.onmessage = (e) => {
const result = heavyComputation(e.data);
self.postMessage(result);
};
选择建议
- 现代开发优先使用 Async/Await
- 需要兼容旧环境时使用 Promise
- 复杂流程控制可考虑 Generator
- 计算密集型任务考虑 Web Workers
- 事件驱动场景使用 EventEmitter
每种方式各有优缺点,根据具体场景选择最合适的方案。
91.简述JavaScript中如何通过new 构建对象?
在 JavaScript 中,通过 new 运算符构建对象的过程如下:
- 首先创建一个空对象
- 将这个新对象的原型指向构造函数的
prototype属性 - 将构造函数内部的
this绑定到这个新对象上 - 执行构造函数内部的代码
- 如果构造函数没有返回其他对象,则自动返回这个新创建的对象
示例:
function Person(name) {
this.name = name;
}
const person = new Person('John');
92.简述JavaScript中构造函数的特点?
JavaScript中构造函数的特点详解
JavaScript中的构造函数是用来创建特定类型对象的特殊函数,它具有以下重要特点:
1. 命名规范
构造函数通常采用大驼峰命名法(Pascal Case),以示与普通函数的区别。例如:
function Person(name, age) {
this.name = name;
this.age = age;
}
2. 使用new关键字调用
构造函数必须通过new操作符调用,这会:
- 创建一个新的空对象
- 将构造函数的
this绑定到这个新对象 - 自动返回这个新对象(除非显式返回另一个对象)
错误示例:
const p = Person('Alice', 25); // 错误!此时this指向window/global
3. 实例属性和方法
在构造函数内部,可以通过this关键字添加实例属性和方法:
function Car(make, model) {
this.make = make; // 实例属性
this.model = model; // 实例属性
this.drive = function() { // 实例方法(不推荐这样定义)
console.log('Driving...');
};
}
4. 原型继承
更高效的方法定义方式是通过构造函数的prototype属性:
Car.prototype.startEngine = function() {
console.log('Engine started');
};
这样所有实例共享同一个方法,节省内存。
5. 隐式返回值
构造函数默认返回新创建的对象实例。如果显式返回:
- 返回原始值会被忽略
- 返回对象则会替代本该返回的新对象
6. 检测实例类型
可以使用instanceof操作符检测对象是否由特定构造函数创建:
const myCar = new Car('Toyota', 'Camry');
console.log(myCar instanceof Car); // true
7. ES6类语法糖
ES6引入的class语法本质上是构造函数的语法糖:
class Person {
constructor(name) {
this.name = name;
}
}
8. 内置构造函数
JavaScript提供了一些内置构造函数,如:
Object()Array()Date()RegExp()Function()
9. 构造函数的重用性
同一个构造函数可以创建多个具有相同属性和方法的对象实例,实现代码复用。
10. 上下文绑定
箭头函数不能用作构造函数,因为它们没有自己的this绑定。
93.面向对象的特性有哪些?
面向对象的特性主要包括以下四个方面:
-
封装(Encapsulation)
- 将数据(属性)和操作数据的方法(行为)捆绑在一起,形成一个独立的单元(类)
- 通过访问修饰符(如public、private、protected)控制对内部实现的访问
- 示例:汽车类封装了发动机状态,外部只能通过公共接口(如start()方法)操作
-
继承(Inheritance)
- 允许新建的类(子类)从现有类(父类)获得属性和方法
- 实现代码重用,建立类之间的层次关系
- 示例:动物类派生出猫类和狗类,子类自动获得父类的eat()方法
-
多态(Polymorphism)
- 同一操作作用于不同对象时产生不同行为
- 主要通过方法重写(override)和接口实现来体现
- 示例:Shape类有draw()方法,Circle和Square子类各自实现不同的绘制逻辑
-
抽象(Abstraction)
- 提取关键特征而忽略非必要细节
- 通过抽象类和接口实现
- 示例:员工管理系统只关心员工的职位和薪资,不关心具体吃饭的细节
扩展说明:
- 这些特性共同作用,使得程序更模块化、更易维护
- 实际开发中常结合使用,如通过继承实现代码复用,同时利用多态保持扩展性
- 现代编程语言(如Java、C#、Python)都支持这些特性,但实现方式可能略有不同
4633

被折叠的 条评论
为什么被折叠?



