JavaScript 基础
1. 原型链和原型链的继承,前端的继承方式有哪些?
原型链和原型链的继承
在JavaScript中,原型链是实现继承的一种机制。每一个对象都有一个原型对象,从原型对象继承方法和属性。这些对象可以作为其他对象的原型,形成一个“原型链”。
原型链的继承是通过将一个类型的实例设置为另一个构造函数的原型来实现的。
前端继承方式
- 原型链继承: 如上所述。
- 构造继承: 通过在子类构造函数中调用父类构造函数,可以继承父类的属性,但不继承父类原型上的属性。
- 组合继承: 结合原型链继承和构造函数继承,既继承属性也继承方法。
- 寄生组合继承: 最理想的继承方式,通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。
2. 如何获得对象非原型链上的属性?
可以使用Object.hasOwnProperty()
方法检查对象本身是否具有某个属性,而不是继承自其原型链。示例代码如下:
const obj = {
ownProp: "this is an own property"
};
console.log(obj.hasOwnProperty("ownProp")); // 输出 true
console.log(obj.hasOwnProperty("toString")); // 输出 false,因为toString是继承自Object.prototype
3. ||
和 &&
操作符的返回值?
逻辑或 ||
,返回第一个真值。如果两者都是假值,则返回最后一个值。
true || false
返回true
false || "hello"
返回"hello"
逻辑与 &&
,返回第一个假值,如果两者都是真值,则返回最后一个值。
true && false
返回false
false || "hello"
返回false
4. 判断数据类型的方法都有哪些?
- typeof 操作符:返回一个字符串,用于基本数据类型(如
string
,number
,boolean
,undefined
,symbol
,bigint
)以及函数类型,typeof
是非常直接且简单的方法。 - instanceof 操作符:测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。适用于自定义对象或者复杂的继承结构中,只适用于对象,不适用于基本类型。
- Array.isArray():专门用来检查一个变量是否为数组,只适用于数组的检测。
- Object.prototype.toString.call()方法:获取对象的类内部属性,从而准确判断对象的类型,包括数组,这是更准确的类型检测方法。
// typeof
console.log(typeof 42); // "number"
console.log(typeof 'str'); // "string"
console.log(typeof {}); // "object"
console.log(typeof null); // "object"
console.log(typeof function(){}); // "function"
// instanceof
console.log([] instanceof Array); // true
console.log({} instanceof Object); // true
// Array.isArray()
console.log(Array.isArray([1, 2, 3])); // true
console.log(Array.isArray({})); // false
// Object.prototype.toString.call()
console.log(Object.prototype.toString.call([1, 2, 3])); // "[object Array]"
console.log(Object.prototype.toString.call(new Date())); // "[object Date]"
5. typeof
函数会返回什么?
typeof
操作符返回一个数据类型的字符串。常见返回值有:“undefined”、“boolean”、“number”、“string”、“symbol”、“function” 和 “object”。
typeof
对于所有类型的对象(包括数组和null),返回的都是 "object"
,它无法区分数组、普通对象、null等。
typeof
对于null
返回"object"
,这是一个历史遗留问题。
6. instanceof
可以判断基本类型吗?
不可以。instanceof
操作符用于检测构造函数的 prototype
属性是否出现在某个实例对象的原型链上。
console.log([1, 2, 3] instanceof Array); // 输出:true
console.log({} instanceof Array); // 输出:false
它不适用于基本类型(如number、string 或 boolean)。
7. 如何判断数组类型?
可以使用Array.isArray()
方法来判断一个变量是否为数组。这是一个简单且准确的方式来检测数组类型。
也可以使用 instanceof
和 Object.prototype.toString.call()
。
8. 其他值到数字的转换规则是什么?
JavaScript在将非数字值转换为数字时遵循以下规则:
- Boolean:
true
转换为1
;false
转换为0
- Null:
null
转换为0
- Undefined:
undefined
转换为NaN
(非数字值) - 字符串 (String) :
- 有效的数字文本(如
"123"
),则转换为对应的数字(如123
)。 - 如果字符串为空(
""
),则转换为0
。 - 如果字符串不是有效的数字(如
"hello"
),则转换为NaN
。
- 有效的数字文本(如
- 对象 (Object) :
- 首先会尝试调用对象的
valueOf()
方法。如果valueOf()
返回一个原始值,则按照这个值的规则转换。 - 如果
valueOf()
方法不存在或者不返回一个原始值,那么调用toString()
方法,然后按照上面的字符串规则转换得到的字符串结果。 - 如果这些方法都不返回可转换为数字的值,则结果为
NaN
。
- 首先会尝试调用对象的
- 数组 (Array) :
- 首先尝试将数组转换为其字符串表示形式,然后再将该字符串转换为数字。例如,单元素数组
[123]
会首先转换为"123"
,然后转换为数字123
。 - 空数组
[]
转换为0
。 - 包含多个元素或非数字元素的数组(如
[1,2]
或["hello"]
)会转换为NaN
。
- 首先尝试将数组转换为其字符串表示形式,然后再将该字符串转换为数字。例如,单元素数组
转换操作通常使用Number()
函数,或在需要自动类型转换的算术运算中发生,比如使用一元加运算符(+
)。
Number("123"); // 123
Number("123abc"); // NaN
Number(true); // 1
Number(null); // 0
Number(undefined); // NaN
Number([123]); // 123
9. SessionStorage、localStorage 和 cookie
- SessionStorage:存储在浏览器的标签页中,当标签页关闭时数据也被清除。可以存储5MB左右的数据。
- localStorage:存储在浏览器中,但数据可以长期存储,直到手动清除。可以存储5MB左右的数据。
- Cookie:存储在用户的计算机上,并且每次向同一服务器发送请求时,都会带上Cookie数据。存储大小很小,只有4kb。
10. for in / for of 的区别
- for in:循环遍历对象的所有可枚举属性(包括继承的属性)。
- for of:提供遍历所有数据结构(如Array,Map,Set等)的值的方式,不包括对象,除非对象定义了迭代器。
注意:这两种循环可以通过
break
或return
在循环体内部直接终止。
11. forEach 和 map 的区别,二者是否可以终止循环?
forEach
用于遍历数组,对原数组进行遍历处理。map
类似于forEach
,也会对原数据遍历处理,但它会返回一个新的数组。
注意:通常
forEach
和map
不能在中间通过break
退出,但可以通过抛出异常等方式间接实现。
12. 扩展运算符 … 进行对象的拷贝是浅拷贝还是深拷贝?
扩展运算符...
提供的是浅拷贝。当使用扩展运算符复制对象时,对象内部的属性只复制其引用值,所以如果属性值是复杂的数据结构,修改其中一个会影响另一个:
const obj1 = { a: { b: 1 } };
const obj2 = { ...obj1 };
obj2.a.b = 2;
console.log(obj1.a.b); // 输出 2
13. JavaScript中箭头函数与普通函数的区别
箭头函数是ES6中新增的一种函数声明方式,与传统的函数表达式相比,它们有几个主要的不同点:
- 语法更简洁。
- 没有自己的
this
:箭头函数不绑定自己的this
,this
值继承自外围最近一层非箭头函数的上下文。 - 没有
arguments
对象:箭头函数内部没有arguments
对象,如果要访问函数的参数列表,需要使用剩余参数(...args
)。 - 不能用作构造函数:箭头函数不能用作构造函数,使用
new
关键字调用会抛出错误。 - 没有
prototype
属性:箭头函数本身没有prototype
属性。
14. this指向的代码示例和分析
在JavaScript中,this
关键字的指向取决于函数的调用方式:
function show() {
console.log(this);
}
const obj = {
show: show
};
show(); // 全局对象(在浏览器中是`window`)
obj.show(); // 对象`obj`
const newShow = obj.show;
newShow(); // 全局对象或undefined(在严格模式下)
在不同的调用环境下,this
可以指向全局对象、当前对象、新创建的对象或严格模式下的undefined
。
15. 浏览器事件机制及e.target
与e.currentTarget
的区别
- 事件机制:包括事件捕获、目标处理和事件冒泡三个阶段,为了实现事件的全面控制和处理。
e.target
:指向触发事件的元素,即事件实际发生的元素。e.currentTarget
:指向绑定事件监听器的元素,即当前通过事件传播处理事件的元素。
16. 捕获和冒泡事件的顺序
- 捕获阶段:事件从文档根节点向下传递到目标元素。
- 目标阶段:事件到达目标元素。
- 冒泡阶段:事件从目标元素向上返回到文档的根节点。 在一般情况下,事件监听器是在冒泡阶段注册的,因为这样可以防止事件在到达目标之前被捕获阶段的祖先元素取消。
<div id="outer">
<div id="inner">
<button id="button">Click me</button>
</div>
</div>
// 在捕获阶段绑定的事件处理程序
document.getElementById('outer').addEventListener('click', function() {
console.log('Outer capture');
}, true);
// 在冒泡阶段绑定的事件处理程序
document.getElementById('inner').addEventListener('click', function() {
console.log('Inner bubble');
});
// 在冒泡阶段绑定的事件处理程序
document.getElementById('button').addEventListener('click', function() {
console.log('Button bubble');
});
// Outer capture
// Inner bubble
// Button bubble
17. stopPropagation、preventDefault区别
stopPropagation 用于阻止事件进一步传播。在整个事件传播过程中(捕获阶段和冒泡阶段)。调用 stopPropagation()
会阻止事件冒泡到更高层次的 DOM 节点,但不会阻止事件处理程序的执行。也就是说,同一个事件类型绑定的其他事件处理程序仍然会按照添加的顺序执行,只是不会传递到更高层次的节点。
preventDefault 用于阻止事件的默认行为。仅在事件的目标阶段(即事件实际发生的阶段)。调用 preventDefault()
会阻止事件触发其默认行为,例如,在 submit
事件中,它会阻止表单的提交。如果事件没有默认行为,调用 preventDefault()
不会有任何效果。
18. addEventListener以及它的参数
addEventListener
方法可以用来某个元素添加事件监听器,
element.addEventListener(event, function, useCapture);
element
:要添加事件监听器的元素。event
:要监听的事件类型,例如 ‘click’, ‘mousedown’, ‘keypress’ 等。function
:事件触发时要执行的回调函数。useCapture
(可选):一个布尔值,用于指示事件是否在捕获阶段触发。默认值为false
。
要移除已经添加的事件监听器,可以使用 removeEventListener
方法。
19. new
操作符的过程
new
操作符用于创建一个定义的对象类型的实例或具有构造函数的内置对象类型的实例。当你使用new
操作符创建一个对象时,会发生以下几个步骤:
- 创建一个新的空对象。
- 将新对象的原型指向构造函数的原型对象。
- 构造函数的
this
会指向新对象,此时会把属性和方法也添加到对象上。 - 如果构造函数返回一个对象,则返回该对象;否则返回刚创建的新对象。
function Car(name) {
this.name = name;
}
// 使用 new 操作符创建 Car 的一个实例
var myCar = new Car('xiaoming');
console.log(myCar.name); // 输出:xiaoming
20. 面向对象如何实现?需要复用的变量怎么处理?
JavaScript的面向对象实现方式主要有两种:使用构造函数与原型,以及使用ES6的类。
构造函数用于创建具有特定初始属性和方法的对象实例,实例通常通过构造函数或类的new
关键字来创建。而 ES6 引入了类语法,使得面向对象编程更为直观和易于实现。
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name}`);
};
const person1 = new Person("Alice");
person1.sayHello();
// 使用ES6类
class Person {
constructor(name) {
this.name = name;
}
sayHello() {
console.log(`Hello, my name is ${this.name}`);
}
}
const person2 = new Person("Bob");
person2.sayHello();
在面向对象编程中,通常需要复用的变量可以作为类的属性(或称为成员变量)。这些变量存储在每个对象的实例中,并可以通过该类的方法进行访问和修改。例如:
class Person {
constructor(name) {
this.name = name;
this.age = 0;
}
make(age) {
this.age = age;
}
}
const person = new Person("xiaoming");
person.make(18);
21. JSONP的缺点
- 安全风险:由于JSONP是通过
<script>
标签引入数据,可能会遇到跨站脚本(XSS)的安全问题。 - 仅限GET请求:JSONP仅支持GET请求,不支持POST、PUT、DELETE等HTTP方法。
- 错误处理不便:JSONP不支持错误处理,很难准确知道请求失败的情况。
22. 闭包及其问题
闭包允许一个函数访问和操作函数外部的变量。但是闭包的也存在问题:
- 内存泄漏:闭包可以维持对外部变量的引用,这可能会导致内存不能被释放。
- 性能问题:由于闭包需要维持更多的上下文信息,可能会导致相对较慢的执行速度和更多的内存使用。
function createCounter() {
let count = 0;
return function() {
count += 1;
return count;
};
}
const counter = createCounter(); // `counter` 是一个闭包
console.log(counter()); // 输出 1
console.log(counter()); // 输出 2
console.log(counter()); // 输出 3
23. arguments介绍一下
arguments
对象是一个类数组对象,它包含了函数调用时传递的所有参数。这个对象不是一个真正的数组,而是一个特殊的对象,它提供了访问函数参数的能力。arguments
对象只存在于函数的作用域内,并且它的长度始终等于传入函数的参数数量。
可以使用数组索引获取参数arguments[1]
,也可以使用length
、for
循环等遍历 arguments
中的参数。
虽然arguments
对象的行为与数组类似,但它不是Array的实例,因此不支持数组的方法,如map
、filter
等。但是我们可以通过Array.from
,将arguments
转成一个真正的数组。
24. JavaScript为什么要进行变量提升,它导致了什么问题?
变量提升是 JavaScript 解析器的行为,它在代码执行前将变量和函数声明提升到当前作用域的顶部。这意味着写代码时,可以在声明之前使用它们。
变量提升可能导致理解上的困难和运行时错误,因为可能会误以为变量的作用范围是从它实际的代码位置开始的。
最佳的使用方式,推荐使用let
和const
关键字来声明变量。这些关键字引入了块级作用域的概念,它们所声明的变量只在声明它们的块级作用域内有效,而不会被提升到块级作用域之外。
使用let
和const
可以避免由于变量提升导致的许多常见错误,使代码的可预测性更强。
25. JavaScript中的深拷贝与浅拷贝
1.浅拷贝
浅拷贝会创建一个新的对象,但这个对象中的属性或值还是引用原始对象中的值,如果这些值是对象或者数组,那么这些对象或数组不会被复制,而是被新的和原始对象共享。
let obj = { a: 1, b: { c: 2 } };
let shallowCopy = shallowCopy = Object.assign({}, obj);
// 修改原始对象的属性
obj.a = 3;
obj.b.c = 4;
console.log(shallowCopy.a); // 输出 3,因为是浅拷贝
console.log(shallowCopy.b.c); // 输出 4,因为是浅拷贝
2.深拷贝
深拷贝会创建一个新的对象,同时新的对象中的属性或值也会复制原始对象中的值,如果这些值是对象或数组,那么这些对象或数组也会被递归地复制。
let obj = { a: 1, b: { c: 2 } };
// 方法1: JSON.parse(JSON.stringify())
let deepCopy1 = JSON.parse(JSON.stringify(obj));
// 方法2: 递归复制
function deepCopy(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (obj instanceof Date) {
return new Date(obj.getTime());
}
if (obj instanceof Array) {
return obj.reduce((arr, item, i) => {
arr[i] = deepCopy(item);
return arr;
}, []);
}
if (obj instanceof Object) {
return Object.keys(obj).reduce((newObj, key) => {
newObj[key] = deepCopy(obj[key]);
return newObj;
}, {});
}
}
let deepCopy2 = deepCopy(obj);
// 修改原始对象的属性
obj.a = 3;
obj.b.c = 4;
console.log(deepCopy1.a); // 输出 1,因为是深拷贝
console.log(deepCopy1.b.c); // 输出 2,因为是深拷贝
console.log(deepCopy2.a); // 输出 1,因为是深拷贝
console.log(deepCopy2.b.c); // 输出 2,因为是深拷贝
注意:JSON.parse(JSON.stringify()) 方法可能不适用于所有情况,例如当对象中包含函数、undefined或循环引用时,这种方法可能会导致问题。因此,在实际应用中,通常推荐使用递归方式进行深拷贝。
26. JavaScript的模块化:CommonJS与ES6模块的区别
CommonJS 主要用于服务器端,如Node.js,通过require
来加载模块,通过module.exports
来导出模块。加载是同步的。
ES6 模块用于浏览器和服务器端,使用import
和export
语句来导入和导出模块。支持静态和动态导入,并且导入是异步的。
ES6 新特性
1. ES6 新特性
ES6引入了许多新特性,包括但不限于:
-
let和const关键字:提供块级作用域的变量声明方式。
-
箭头函数:提供了一种更简洁的函数写法,并且不绑定自己的
this
,arguments
。非常适合用作那些不需要自己作用域的函数,如回调函数。 -
模板字符串:模板字符串是增强版的字符串字面量,可以使用反引号(
`
)标识。它们支持字符串插值和多行字符串。let a = 10; let multiLineString = `This is a string ${a} that spans multiple lines.`; console.log(multiLineString); // 输出: // This is a string 10 // that spans multiple // lines.
-
解构赋值:允许从数组或对象中提取数据,并赋值给新的变量,使得从复杂数据结构中提取数据更直观。
{...obj, ...obj1}
、[...arr1, ...arr2]
-
类和继承:JavaScript 类是基于原型的继承的语法糖。它提供了一种使用
class
和extends
关键字的声明方式来实现对象的继承和复用。 -
Promises:Promises 提供了一种处理异步操作的新方式。
-
模块化导入/导出:ES6 引入了原生的模块化支持,允许使用
import
和export
语句来导入和导出模块中的函数、对象或原始值。 -
Symbol:一种新的原始数据类型,唯一且不可修改。Symbols 主要用作对象属性的键,每个 Symbol 值都是唯一的,不会与其他属性名发生冲突。
-
Set和Map:
Set
: 是一种新的数据结构,它类似于数组,但是成员的值都是唯一的,没有重复的值。Map
: 也是一种新的数据结构,它类似于对象,但键的范围不限于字符串,可以使用各种类型的值(数字、字符串、对象、函数都行)。
2. const 定义的空数组是否能进行 push 操作
是的,使用const
定义的空数组是可以进行push
操作的,因为const
保证的是变量引用的不变性,而非值的不变性。
const
关键字用于声明变量时,确保变量的引用不会改变(一旦被赋予一个值或对象,就不能再被赋予另一个新的值或对象)。然而,const
并不意味着该变量所持有的对象内容是不可变的。
const array = [];
array.push(1); // 这是允许的
console.log(array); // 输出 [1]
array = 123; // 不允许,报错
3. ES6 Map 与 WeakMap 的区别
Map
和 WeakMap
这两种新的数据结构,它们都用于存储键值对。
Map
Map
对象可以使用任何类型的值作为键,包括对象、数组、字符串等。Map
强引用其键和值,这意味着只要Map
存在,其键和值就不会被垃圾回收机制回收。Map
是可迭代的,可以直接使用for...of
循环或其他迭代方法(如forEach
)进行迭代。- 提供了
.size
属性以及.set()
,.get()
,.has()
,.delete()
,.clear()
等方法,可以方便地操作存储的数据。
WeakMap
WeakMap
只接受对象作为键。尝试使用非对象作为键将会抛出错误。WeakMap
弱引用其键。这意味着如果没有其他引用指向键对象,这些键对象可以被垃圾回收。这种特性有助于防止内存泄漏。WeakMap
不可迭代,这意味着不能直接使用迭代方法遍历其中的元素。- 提供
.get()
,.set()
,.has()
,.delete()
方法,但没有.clear()
方法,因为清理工作应由垃圾回收器处理,也没有提供.size
属性,因为其键可能随时被清除。
4. ES6 Map 和 JSON 存储键值对的区别
区别主要涉及数据结构的灵活性、数据操作的功能以及使用场景。
-
键的类型:ES6 Map 可以使用任何类型的值作为键,包括对象、函数、基本类型等。JSON 的键必须是字符串。如果在对象中使用非字符串键,这些键会被自动转换为字符串。
-
有序性:ES6 Map 保持了键的插入顺序。当对Map进行迭代时,元素将按照插入的顺序返回。JSON 在JavaScript中,对象(可以用来表示JSON)的键被视为无序的,尽管在现代JavaScript引擎中,对象的键通常会按照创建顺序进行存储和迭代,但这并不是规范的要求。
-
数据操作方法:ES6 Map 提供了多种实用的方法来操作数据,如
set()
,get()
,delete()
,has()
, 和clear()
等。JSON 自身没有提供方法来直接操作。通常需要先将其转换为 JavaScript 对象,然后再进行修改。 -
数据的序列化和传输:ES6 Map 不支持直接序列化。如果需要网络传输,需要将Map转换为数组或其他可序列化的格式。JSON 本身就是一种数据交换格式,非常适合于数据序列化和网络传输。
-
性能:ES6 Map 在频繁添加和删除键值对的场景中,通常提供更优的性能,因为它是为了高效的键值插入和查找而设计的。JSON 在解析和序列化大量数据时可能较慢,特别是数据结构复杂或数据量大时。
ES6 Map 提供了更为灵活和功能丰富的方式来管理键值对数据,适合在应用程序中作为数据结构使用,而 JSON 则更适合数据存储、配置文件或网络传输的场合
5. ES6 中 Symbol 的使用及应用场景
Symbol
是ES6引入的一种新的原始数据类型,表示独一无二的值。最主要的用途是用来作为对象属性的键:
let sym = Symbol("key");
let obj = {
[sym]: "value"
};
console.log(obj[sym]); // 输出"value"
应用场景:
Symbol
值作为键不会被常规方法遍历到,所以可以用于创建对象的私有属性。- 如果使用第三方库,使用
Symbol
作为属性键可以防止与库内部可能存在的属性键冲突。
6. ES6 Proxy 和 Reflect 的使用场景
Proxy:Proxy
用于修改某些操作的默认行为,等同于在语言层面做出修改,可以理解为在目标对象之前架设一层“拦截”。使用场景包括跟踪属性访问、属性验证、观察者模式等。
let handler = {
get: function(target, name) {
return name in target ? target[name] : 42;
}
};
let p = new Proxy({}, handler);
p.a = 1;
console.log(p.a, p.b); // 输出1, 42
Reflect:Reflect
是一个内置的对象,它提供拦截JavaScript操作的方法。这些方法与Proxy
handlers的方法相同。使用场景主要是与Proxy
一起使用,以简化自定义操作的实现。
function Greet() {
console.log(`Hello, ${this.name}`);
}
let obj = { name: 'Xavier' };
Reflect.apply(Greet, obj, []); // "Hello, Xavier"
7. Promise 的 resolve 和 reject
Promise
是异步编程的一种解决方案。当你创建一个Promise
时,需要提供一个执行器函数,这个函数接受两个参数,resolve
和reject
:
resolve:当异步操作成功时,我们调用resolve
函数,这会将Promise
状态从pending
变为fulfilled
。
reject:当异步操作失败时,我们调用reject
函数,这会将Promise
状态从pending
变为rejected
。
const delay = (ms) => {
return new Promise(resolve => setTimeout(resolve, ms));
};
8. Promise 的用法?了解 allSettled 方法么,怎么实现?
- Promise是异步编程的一种解决方案,代表一个可能在未来某个点完成的操作和它的结果。
Promise.allSettled()
方法会等待所有的 Promise 都执行完毕,无论它们最终状态是 fulfilled 还是 rejected,都会返回一个数组,数组中的每个元素对应传入的每个 Promise。// 创建三个 promise 实例 const p1 = new Promise((resolve, reject) => setTimeout(() => resolve('p1_value'), 2000)); const p2 = new Promise((resolve, reject) => setTimeout(() => resolve('p1_value'), 1000)); const p3 = new Promise((resolve, reject) => setTimeout(() => reject('p3_value'), 3000)); // 使用 Promise.allSettled 对它们进行处理 Promise.allSettled([p1, p2, p3]).then((results) => { // results ['p1_value', 'p1_value', 'p3_value'] });
Promise.all()
只有当所有 Promise 都成功 resolve 时,才会调用成功的回调。如果任何一个 Promise 被拒绝,所有的回调都不会被调用。Promise.race()
不关心输入 Promise 的顺序,只要其中一个完成,无论是 fulfilled 还是 rejected,它就会立即返回。- Promise.resolve() 返回一个成功解决的 Promise。通常用于创建一个立即解决的 Promise。
const resolvedPromise = Promise.resolve('Success'); resolvedPromise.then(value => console.log(value)); // 输出 'Success'
- Promise.reject() 返回一个立即拒绝的 Promise。通常用于创建一个立即失败的 Promise。
此外,Promise.prototype 也有自身的属性方法:
then()
如果 Promise 被解决,则回调函数被调用。const promise = new Promise((resolve, reject) => { setTimeout(() => resolve('Success'), 1000); }); promise.then(value => console.log(value)); // 1秒后输出 'Success'
catch()
如果 Promise 被拒绝,则回调函数被调用。finally()
无论 Promise 被解决,还是被拒绝,回调函数被调用。timeout()
用于设置 Promise 的超时时间。如果在指定的时间内 Promise 没有被解决或拒绝,则会自动被拒绝。const promise = new Promise((resolve, reject) => { setTimeout(() => resolve('Success'), 2000); }); const timedOutPromise = promise.timeout(1000); timedOutPromise.then(value => console.log(value)) .catch(error => console.log(error)); // 输出 'Timeout',因为 Promise 在 2 秒后被解决,但设置了 1 秒的超时
9. async 和 await 的使用以及错误捕获
async
和await
是写异步代码的新方式,相比于之前的回调和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 fetching data:', error);
}
}
在使用async
和await
时,错误可以通过try...catch
语句来捕获。await
表达式可能会抛出异常,如果不捕获这些异常,会导致程序中断。
10. Generator 函数的应用场景
Generator
函数是一个状态机,封装了多个内部状态。执行Generator
函数会返回一个遍历器对象,可以依次遍历Generator
函数内部的每一个状态。
应用场景:
- 异步操作的同步化表达:在异步操作中使用
yield
,使得异步代码看起来更像是同步代码。 - 控制流管理:可以按部就班地执行代码,或者在特定的步骤中返回并稍后再从相同点继续执行。
- 部分计算:生成器提供了一个强大的接口来控制函数的执行,可以按需执行计算。
// 定义一个 Generator 函数
function* countdown(n) {
while (n >= 0) {
yield n; // 使用 yield 来暂停执行
n--;
}
}
// 使用 Generator 函数
const generator = countdown(5);
// 开始执行 Generator 函数
console.log(generator.next()); // 输出 {value: 5, done:true}
console.log(generator.next()); // 输出 {value: 4, done:true}
console.log(generator.next()); // 输出 {value: 3, done:true}
console.log(generator.next()); // 输出 {value: 2, done:true}
console.log(generator.next()); // 输出 {value: 1, done:true}
console.log(generator.next()); // 输出 {value: 0, done:true}
// 当 generator.next() 被调用时,函数会从上次 yield 的地方继续执行
// 每次调用 next(),函数都会返回一个对象,包括当前的 yield 值和一个 done 属性
// 当 done 属性为 true 时,表示 Generator 函数已经执行完毕
11. async/await的实现原理
async
/await
可以看作是Generator
函数的语法糖。它允许开发者以一种更接近同步代码的方式来编写异步代码。
执行步骤:
- 当一个函数被标记为
async
时,它在内部会隐式地返回一个 Promise。 await
表达式会暂停当前函数的执行,直到背后的 Promise 被解决或拒绝。拿到值再继续执行。
底层原理:
- 当函数被标记为
async
时,JavaScript 引擎会将这个函数转换成一个异步函数对象。这个对象在内部维护一个状态机,用于跟踪函数的执行状态。 - 当调用异步函数时,JavaScript 引擎会创建一个新的执行上下文。这个上下文会包含一个
this
值和一个包含await
表达式的调用栈。 - 每当遇到
await
表达式时,引擎会将当前的执行上下文推送到调用栈,并继续执行下一个任务。一旦背后的 Promise 被解决或拒绝,引擎会从调用栈中弹出之前的执行上下文,并继续执行。 - 当
await
表达式背后的 Promise 被解决或拒绝时,异步函数会根据 Promise 的结果更新其状态,并继续执行后续代码。
12. async和await的错误捕获细节
使用async
和await
时,错误捕获通常通过try...catch
块来实现。这与使用传统的Promise
链中的.catch()
方法类似,但语法更接近同步代码的错误处理方式。例如:
async function fetchData(url) {
try {
const response = await fetch(url);
const data = await respons参考json();
return data;
} catch (error) {
console.error('An error occurred:', error);
throw error; // 可以重新抛出错误,如果你想让调用者也能处理这个错误
}
}
13. 如何使用ES6 Proxy进行状态管理,手写
Proxy
适用于做前端状态管理,比如在一个框架中跟踪状态的变化来自动更新UI。这可以通过拦截设置属性的操作,并在属性值变化时通知视图层来实现。
function updateComponent() {
console.log('Component updated!');
}
const state = new Proxy({ name: 'Alice' }, {
set(target, property, value) {
target[property] = value;
updateComponent(); // 更新UI的函数
return true;
}
});
state.name = 'Bob'; // 设置属性时自动触发组件更新
JavaScript 其他技术
1. Service Worker怎么理解?
Service Worker 是一种运行在Web浏览器后台的JavaScript工作者线程,它独立于主页面运行,能够拦截和处理网络请求,管理缓存。这使得它在没有网络连接的情况下,仍然可以提供页面或应用的功能。
Service Worker在独立于主网页的后台线程中运行,不会阻塞页面或影响页面性能。它通过监听和处理事件(如fetch, push, sync等)来实现功能。
应用场景:
- 离线应用支持:通过缓存关键资源来提供基本的离线浏览体验。
- 网络请求拦截和管理:自定义响应网络请求的行为,如优先返回缓存内容,实现快速加载。
// 注册一个 Service Worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(function(registration) {
console.log('Service Worker registered with scope: ', registration.scope);
})
.catch(function(error) {
console.log('Service Worker registration failed: ', error);
});
}
// sw.js
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open('v1').then(function(cache) {
return cache.addAll([
'/index.html',
'/styles.css',
'/script.js'
]);
})
);
});
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request).then(function(response) {
return response || fetch(event.request);
})
);
});
2. Web Worker 是做什么用的?
用于执行那些计算密集或耗时的任务,而不阻塞主线程,保证用户界面的流畅。
// main.js
if (window.Worker) {
const myWorker = new Worker('worker.js');
myWorker.postMessage([10, 24]); // 发送数据到 Web Worker
myWorker.onmessage = function(e) {
console.log('Message received from worker: ', e.data);
};
}
// worker.js
onmessage = function(e) {
console.log('Message received from main script');
const result = e.data[0] * e.data[1]; // 简单的乘法操作
postMessage(result);
}
3. Service Worker 与 Web Worker区别
Web Worker 允许Web应用程序进行多线程处理,执行后台任务而不干扰用户界面;
Service Worker 主要用于管理离线缓存和网络请求,增强Web应用的性能和离线使用体验。
4. Websocket 的建立连接和优缺点
WebSocket 允许建立一个持久的连接,通过该连接,客户端和服务器可以进行双向通信。
WebSocket 不仅允许服务器主动向客户端发送消息,更适合需要频繁和实时交互的应用。但是在某些严格的网络环境(如代理和防火墙)下,可能无法顺利使用WebSocket。
// 创建一个新的WebSocket连接
var socket = new WebSocket('ws://example.com/socket');
// 连接成功建立时触发
socket.onopen = function(event) {
// 发送数据到服务器
socket.send('Hello, Server!');
socket.close(1000, 'Close'); // 关闭连接。参数:状态码、描述,可不传
};
// 从服务器接收到消息时触发
socket.onmessage = function(event) {
console.log('Message from server ', event.data);
};
// 连接关闭时触发,事件对象提供 `code` 和 `reason` 属性,表示关闭的状态码和原因。
socket.onclose = function(event) {
console.log('Closed:', event.code, event.reason);
};
// 连接发生错误时触发
socket.onerror = function(error) {
console.error('WebSocket Error ', error);
};
5. PWA使用过吗?serviceWorker的使用原理是啥?
PWA是一种通过使用现代 Web 技术来提供类似原生应用体验的 Web 应用。它的关键技术包括Service Worker、Manifest文件和响应式设计。
PWA
支持多端的响应式设计、离线工作、推动通知。。。
Service Worker
主要用于提供离线本地服务运行。
// 注册Service Worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').then(function(registration) {
console.log('Service Worker registered with scope:', registration.scope);
}).catch(function(error) {
console.log('Service Worker registration failed:', error);
});
}
TypeScript
1. ts 中 type 和 interface 的区别
- Interface(接口) :主要用于定义对象的形状,支持扩展(extends)和实现(implements)。接口是开放的,可以在程序中多次声明同一个接口,TypeScript会将它们视为单个接口。
- Type(类型别名) :可以用于对象类型,也可以用于其他类型(如基本类型、联合类型、交叉类型等)。类型别名不是开放的,不能像接口那样被扩展或实现。
2. ts 中的枚举使用
TypeScript的枚举(Enum)是一种定义一组命名常量的方式。以下是一个基本的枚举使用示例:
enum Color {
Red,
Green,
Blue
}
let c: Color = Color.Green;
3. ts 泛型做什么的,infer关键字的作用?
- 泛型:允许在定义函数、接口或类时不具体指定数据类型,使用时再指定类型。泛型提供了更大的灵活性和代码复用性。
- infer 关键字:用在条件类型中,用于在条件类型的内部推断类型变量。
JavaScript 应用场景
1. 观察者模式的实现
观察者模式是一种设计模式,允许多个对象监听某个对象的状态,以便在状态发生变化时得到通知。以下是一个简单的观察者模式实现示例:
class Subject {
constructor() {
this.observers = [];
}
subscribe(observer) {
this.observers.push(observer);
}
unsubscribe(observer) {
this.observers = this.observers.filter(obs => obs !== observer);
}
notify(data) {
this.observers.forEach(observer => observer.update(data));
}
}
class Observer {
update(data) {
console.log("Observer received data:", data);
}
}
// 使用示例
const subject = new Subject();
const observer1 = new Observer();
const observer2 = new Observer();
subject.subscribe(observer1);
subject.subscribe(observer2);
subject.notify("Hello Observers!");
此模式在JavaScript中的应用非常广泛,特别是在实现MVC或MVVM架构的框架中。
2. 观察者模式和发布-订阅模式
- 观察者模式:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
- 发布-订阅模式:通过一个中介者(通常是事件通道或事件总线)来管理事件和订阅者之间的关系,发布者发布事件到事件通道,而不是直接通知订阅者。
3. JavaScript中的装饰器模式
装饰器(Decorator)模式是一种结构型设计模式,它允许通过将对象放入包含行为的特殊封装对象中来动态地添加新功能。在JavaScript中,装饰器通常实现为高阶函数,返回一个包装了原始函数的新函数,能够执行额外的功能。
function readOnly(target, key, descriptor) {
descriptor.writable = false;
return descriptor;
}
class Job {
@readOnly
title() {
return 'Developer';
}
}
注意:装饰器目前是一个实验性功能,需要在Babel等编译器中启用相应插件。
4. 前端路由怎样实现
- 前端路由:使用JavaScript来管理路由,不需要每次都从服务器加载新页面,常见的实现方式是利用
hash
(哈希)或history API
(HTML5 历史API)。
5. 前端路由直接刷新404怎样处理
- 刷新404问题:可以通过服务器配置始终返回入口HTML文件(例如,使用单页应用时),或者使用
hash
模式,因为hash
变化不会导致浏览器向服务器发起新的请求。
6. 移动端300ms延迟的原因及处理方法
移动端浏览器通常会有300ms的点击延迟,这是因为浏览器需要判断用户操作是单击还是双击。为了移除这个延迟,可以使用如FastClick
库,或者通过设置CSS的touch-action
属性为manipulation
来禁用双击缩放,从而消除延迟。
7. Base64编码的应用场景与优缺点
-
Base64编码图片:在无法直接引用外部资源的场景,可以使用Base64编码将图片直接嵌入到内容中。在某些静态资源合并或懒加载的场景,将图片转为Base64编码并内嵌到CSS或HTML中,可以减少服务端的HTTP请求次数。
- 优点:减少HTTP请求次数,提高页面加载速度。
- 缺点:编码后的字符串比原始二进制数据大约增加33%,这可能会导致HTML、CSS文件体积变大,影响页面加载速度。
-
外链图片:通过外链,用户可以轻松地在不同页面、应用或设备之间分享和引用图片。利用CDN(内容分发网络)加速,外部链接的图片可以由分布在全球的节点提供服务,减少延迟,提高加载速度。
- 优点:便于管理,通过CDN缓存,不仅提高访问速度,还减轻服务器负担。
- 缺点:外链图片的显示依赖于网络连接,如果没有网络或者服务提供商的服务出现问题时,图片可能无法显示。
总的来说,Base64编码适合于不需要频繁更新、体积较小、对加载速度要求不高的静态页面或较小规模的应用。而外链适合于需要快速加载、易于共享、经常需要更新或维护的大型网站和应用。
8. JavaScript的内存管理
JavaScript的内存管理主要是通过垃圾回收机制自动完成的。主要的垃圾回收算法包括标记清除(Mark-and-Sweep)和引用计数。
标记清除是最常用的垃圾回收方法,工作原理是标记活动对象然后清除那些未被标记的对象。引用计数跟踪每个值被引用的次数,当引用次数降到零时,释放内存。
9. JavaScript中执行上下文和作用域链有什么不同?
执行上下文是当前JavaScript代码被评估和执行时的环境。每次函数被调用时,就会创建一个新的执行上下文,包括绑定其所需的变量、函数声明、this
的值等。
作用域链是由当前执行上下文中可访问的作用域组成的列表,它确保了对执行上下文有效的变量和函数的有序访问。当代码需要访问一个变量时,JavaScript引擎会首先查找当前执行上下文的变量对象,如果没有找到,就会沿着作用域链向上查找。
10. JavaScript异步编程的演进
JavaScript的异步编程已经从简单的回调函数发展到更加复杂的Promise和异步函数(async/await)。这个演变提高了代码的可读性和错误处理能力。
- 回调函数:最初的异步编程模式,容易导致回调地狱。
- Promise:提供了更好的错误处理机制和链式调用方式。
- Async/Await:使异步代码看起来更像同步代码,提高了代码的可读性。
11. Web性能优化策略
优化 Web 应用的性能,这是一个持续的过程:
- 代码拆分:使用如Webpack这样的模块打包器来实现代码拆分,按需加载。
- 资源压缩和合并:减少HTTP请求的数量和大小。
- 使用缓存策略:利用浏览器缓存,减少资源重复加载。
- 延迟加载和预加载:优化资源加载策略,改善用户体验。
- 服务端渲染(SSR) :提高首次加载性能和SEO。
12. 浏览器渲染过程
浏览器的渲染过程大致如下:
- 解析HTML:构建DOM树。
- 解析CSS:构建CSSOM树。
- 合并DOM树和CSSOM树:生成渲染树。
- 布局(Layout/Reflow) :计算出渲染树中每个节点的位置和大小。
- 绘制(Paint) :将渲染树的节点转换成屏幕上的实际像素。
- 合成(Composite) :将多层的图层合成为一层,以优化绘制效率。
13. 从输入URL到页面展示的过程
当你在浏览器地址栏输入URL并按下回车后,会经历以下步骤:
- DNS解析:将域名解析成IP地址。
- TCP连接:与服务器建立TCP连接。
- 发送HTTP请求:浏览器构建HTTP请求并发送到服务器。
- 服务器处理请求并返回HTTP响应。
- 浏览器解析响应内容:解析HTML、CSS、JavaScript等。
- 渲染页面:按照上述浏览器渲染过程进行。
- 显示内容:在用户屏幕上展示最终内容。