一、JavaScript基础
前端工程师吃饭的家伙,深度、广度一样都不能差。
变量和类型
- 1.
JavaScript
规定了几种语言类型
答案
null undefined number boolean string object symbol(es6) bigInt(es10)
- 2.
JavaScript
对象的底层数据结构是什么
答案
map 键值对
- 3.
Symbol
类型在实际开发中的应用、可手动实现一个简单的Symbol
答案
var sym = Symbol('foo');
typeof sym; // 'symbol'
Symbol('foo') === Symbol('foo'); // false
var obj = {[sym]: 1};
obj[sym]; // 1
- 4.
JavaScript
中的变量在内存中的具体存储形式
答案
栈(内存空间大小固定,变量值不可变)
基本数据类型的值直接存于栈内存,而引用数据类型在栈内存中存储引用地址,实际数据存于堆内存。
- 5.基本类型对应的内置对象,以及他们之间的装箱拆箱操作
答案
String Number Boolean
var s = 'Hello World';
console.log(s + 'Aaron'); // 先装箱,再拆箱
- 6.理解值类型和引用类型
答案
都是值,一个是字面量值,一个是内存地址
- 7.
null
和undefined
的区别
答案
- null 代表对象为空,undefined 代表变量未赋值
- typeof null === 'object', typeof undefined === 'undefined'
- 8.至少可以说出三种判断
JavaScript
数据类型的方式,以及他们的优缺点,如何准确的判断数组类型
答案
- typeof 判断基本类型和函数对象很方便,但无法区分 null 和 object(包括数组)。
- instanceof 运算符用于检测构造函数的
prototype
属性是否出现在某个实例对象的原型链上,只能检测对象的类型。 - Object.prototype.toString 默认返回当前对象的 [[Class]]
var number = 1; // [object Number]
var string = '123'; // [object String]
var boolean = true; // [object Boolean]
var und = undefined; // [object Undefined]
var nul = null; // [object Null]
var obj = {a: 1} // [object Object]
var array = [1, 2, 3]; // [object Array]
var date = new Date(); // [object Date]
var error = new Error(); // [object Error]
var reg = /a/g; // [object RegExp]
var func = function a(){}; // [object Function]
Math //[object Math]
JSON //[object JSON]
- 9.可能发生隐式类型转换的场景以及转换原则,应如何避免或巧妙应用
答案
隐式类型转换的场景:
- if 语句和逻辑语句
- 各种数学运算
- ==
- 10.出现小数精度丢失的原因,
JavaScript
可以存储的最大数字、最大安全数字,JavaScript
处理大数字的方法、避免精度丢失的方法
答案
小数精度丢失的原因
在 JavaScript 中,数字采用 IEEE 754 双精度 64 位浮点数格式来表示。这种格式由 1 位符号位、11 位指数位和 52 位尾数位构成。
在进行小数运算时,部分十进制小数无法精准地用二进制来表示。例如,十进制的 0.1 转换为二进制是一个无限循环小数 0.0001100110011...
。由于 52 位尾数的存储限制,只能截取部分二进制位,从而造成精度丢失。
以下是一个简单的示例:
console.log(0.1 + 0.2);
上述代码输出的结果并非 0.3,而是一个近似值 0.30000000000000004
。
JavaScript 可以存储的最大数字、最大安全数字
- 最大数字:JavaScript 里能够存储的最大数字是
Number.MAX_VALUE
,其值约为1.7976931348623157e+308
。当数字超出这个范围时,会被表示为Infinity
。
console.log(Number.MAX_VALUE);
- 最大安全数字:JavaScript 中最大的安全整数是
Number.MAX_SAFE_INTEGER
,其值为9007199254740991
。安全整数意味着在这个范围内的整数可以精确地表示和进行比较。
console.log(Number.MAX_SAFE_INTEGER);
JavaScript 处理大数字的方法
- BigInt:ES2020 引入了
BigInt
类型,用于表示任意大的整数。在数字后面加上n
或者使用BigInt()
函数来创建BigInt
类型的值。
const bigNumber = 9007199254740991n + 1n;
console.log(bigNumber);
- 第三方库:像
bignumber.js
、decimal.js
这类第三方库,能处理任意精度的数字运算。以bignumber.js
为例:
const BigNumber = require('bignumber.js');
const num1 = new BigNumber('9007199254740991');
const num2 = new BigNumber('1');
const result = num1.plus(num2);
console.log(result.toString());
避免精度丢失的方法
- 使用整数进行计算:把小数转换为整数进行运算,最后再转换回小数。
function add(a, b) {
const baseNum = Math.pow(10, Math.max(getDecimalLength(a), getDecimalLength(b)));
return (a * baseNum + b * baseNum) / baseNum;
}
function getDecimalLength(num) {
const str = num.toString();
const decimalIndex = str.indexOf('.');
return decimalIndex === -1 ? 0 : str.length - decimalIndex - 1;
}
console.log(add(0.1, 0.2));
- 使用 BigInt 或第三方库:如前文所述,
BigInt
可以处理大整数,第三方库能处理任意精度的数字运算,从而避免精度丢失。
参考:不老实的number类型
原型和原型链
- 1.理解原型设计模式以及
JavaScript
中的原型规则
答案
- 2.
instanceof
的底层实现原理,手动实现一个instanceof
- 3.实现继承的几种方式以及他们的优缺点
- 4.至少说出一种开源项目(如
Node
)中应用原型继承的案例
答案
在 Node.js 的 http
模块中就应用了原型继承,下面详细介绍其具体情况。
案例背景
在 Node.js 里,http
模块是用于构建 HTTP 服务器和客户端的核心模块。当我们创建一个 HTTP 服务器时,会用到 http.createServer()
方法,该方法返回的服务器对象继承了一些基础功能,这其中就运用了原型继承。
代码示例及原理分析
const http = require('http');
// 创建一个 HTTP 服务器实例
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello, World!\n');
});
// 监听端口
server.listen(3000, '127.0.0.1', () => {
console.log('Server running at http://127.0.0.1:3000/');
});
原型继承的体现
http.createServer()
方法返回的 server
对象继承自 net.Server
。net.Server
是 Node.js 中用于创建 TCP 服务器的基础类,http.Server
通过原型继承了 net.Server
的属性和方法。具体来说,http.Server
的原型链指向 net.Server
的实例,这样 http.Server
就拥有了 net.Server
的功能,同时还可以添加自己特有的 HTTP 相关功能。
代码层面解释
在 Node.js 的源码中,可以看到类似如下的继承关系构建(简化示意):
const net = require('net');
// 模拟 http.Server 构造函数
function HttpServer(requestListener) {
// 调用父类构造函数
net.Server.call(this, { allowHalfOpen: true });
// 绑定请求事件处理函数
if (requestListener) {
this.on('request', requestListener);
}
}
// 实现原型继承
HttpServer.prototype = Object.create(net.Server.prototype);
HttpServer.prototype.constructor = HttpServer;
// 定义 http.Server 特有的方法和属性
HttpServer.prototype.listen = function() {
// 实现监听逻辑
net.Server.prototype.listen.apply(this, arguments);
};
// 模拟 http.createServer 方法
function createServer(requestListener) {
return new HttpServer(requestListener);
}
module.exports = {
createServer
};
在上述代码中:
HttpServer
构造函数通过net.Server.call(this, { allowHalfOpen: true });
调用父类构造函数,继承父类的实例属性。HttpServer.prototype = Object.create(net.Server.prototype);
让HttpServer
的原型指向net.Server
原型的一个副本,实现了原型继承,使得HttpServer
实例可以访问net.Server
原型上的方法。HttpServer.prototype.constructor = HttpServer;
修正了构造函数的指向,确保constructor
属性正确指向HttpServer
。
优点
- 代码复用:通过原型继承,
http.Server
复用了net.Server
的代码,避免了重复编写基础的服务器功能代码,提高了开发效率。 - 扩展性:
http.Server
可以在继承的基础上添加自己特有的 HTTP 相关功能,如处理 HTTP 请求和响应等,使得代码具有良好的扩展性。
- 5.可以描述
new
一个对象的详细过程,手动实现一个new
操作符
答案
- 6.理解
es6 class
构造以及继承的底层实现原理
答案
ES6 引入的 class
关键字为 JavaScript 提供了更简洁、更符合传统面向对象编程风格的类和继承语法,但本质上它还是基于原型链的语法糖。下面详细解释 class
构造以及继承的底层实现原理。
ES6 class
构造的底层原理
ES6 的 class
定义一个类,其实在底层是基于函数和原型实现的。下面通过一个示例来解释:
// ES6 class 定义
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHello() {
console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
}
}
// 等价的 ES5 实现
function PersonES5(name, age) {
this.name = name;
this.age = age;
}
PersonES5.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
};
在上述代码中,ES6 的 class
定义的 Person
类和 ES5 实现的 PersonES5
函数在功能上是等价的。具体解释如下:
- 构造函数:
class
中的constructor
方法相当于 ES5 中的构造函数,当使用new
关键字创建实例时,constructor
方法会被调用。 - 原型方法:
class
中定义的方法(如sayHello
)会被添加到类的原型上,这和 ES5 中通过prototype
属性添加方法是一样的。
ES6 class
继承的底层原理
ES6 的 class
继承使用 extends
关键字,底层同样基于原型链和构造函数的组合。以下是一个示例:
// 父类
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a noise.`);
}
}
// 子类
class Dog extends Animal {
constructor(name) {
super(name);
}
speak() {
console.log(`${this.name} barks.`);
}
}
// 等价的 ES5 实现
function AnimalES5(name) {
this.name = name;
}
AnimalES5.prototype.speak = function() {
console.log(`${this.name} makes a noise.`);
};
function DogES5(name) {
AnimalES5.call(this, name);
}
DogES5.prototype = Object.create(AnimalES5.prototype);
DogES5.prototype.constructor = DogES5;
DogES5.prototype.speak = function() {
console.log(`${this.name} barks.`);
};
在上述代码中,ES6 的 class
继承和 ES5 的实现方式在功能上是等价的。具体解释如下:
extends
关键字:extends
关键字实现了子类对父类的原型继承,在 ES5 中是通过Object.create
方法来实现的,它创建了一个新对象,该对象的原型指向父类的原型。super
关键字:super
关键字在子类的构造函数中用于调用父类的构造函数,在 ES5 中是通过call
方法来实现的,它改变了this
的指向,使得父类的构造函数在子类的上下文中执行。
总结 ES6 的 class
构造和继承是基于 ES5 的原型和构造函数实现的语法糖,它提供了更简洁、更直观的语法,使得 JavaScript 的面向对象编程更加符合传统的编程习惯。但在底层,JavaScript 仍然是基于原型的语言,class
和 extends
只是对原型链和构造函数的封装。
作用域和闭包
- 1.理解词法作用域和动态作用域
答案
词法作用域:又称静态作用域,是在函数声明时即确定的。this使用的是词法作用域 动态作用域:则与调用者有关
词法作用域和动态作用域是编程语言中两种不同的作用域规则,它们决定了变量和函数的可访问性。下面为你详细介绍这两种作用域。
词法作用域
定义
词法作用域(Lexical Scope),也被称为静态作用域,是指变量和函数的作用域是由它们在代码中定义的位置决定的,与函数的调用位置无关。也就是说,在编写代码时,变量和函数的作用域就已经确定了。
示例
function outer() {
let outerVar = 'I am from outer function';
function inner() {
console.log(outerVar);
}
return inner;
}
const innerFunc = outer();
innerFunc();
解释
- 在上述代码里,
inner
函数内部引用了outerVar
变量。依据词法作用域规则,inner
函数的作用域链包含其自身作用域和outer
函数的作用域。 - 由于
outerVar
是在outer
函数中定义的,inner
函数能够访问该变量,即便inner
函数是在outer
函数外部被调用的。 - 变量的可访问性取决于其在代码中的定义位置,而不是调用位置。
优点
- 代码可预测性强:开发者通过查看代码的定义位置,就能清晰知晓变量和函数的作用范围,便于理解和调试代码。
- 便于模块化开发:每个模块内部的变量和函数作用域清晰,减少了不同模块之间的命名冲突。
缺点
- 灵活性相对较低:在某些特定场景下,若需要根据函数调用时的上下文动态确定变量的作用域,词法作用域就难以满足需求。
动态作用域
定义
动态作用域(Dynamic Scope)是指变量和函数的作用域是由函数的调用位置决定的,而不是定义位置。在动态作用域中,当查找一个变量时,会从当前执行上下文开始,依次向上查找调用栈中的上下文,直到找到该变量。
示例(以类 JavaScript 伪代码说明)
let globalVar = 'Global variable';
function foo() {
console.log(globalVar);
}
function bar() {
let globalVar = 'Local variable in bar';
foo();
}
bar();
解释
- 在动态作用域的语言中,当
foo
函数被bar
函数调用时,foo
函数内部的globalVar
会先在bar
函数的作用域中查找,若找到则使用该值。 - 所以上述代码的输出会是
Local variable in bar
。但在 JavaScript 中采用的是词法作用域,输出结果会是Global variable
。
优点
- 灵活性高:能够根据函数的调用上下文动态地确定变量的作用域,适用于一些需要动态调整变量值的场景。
缺点
- 代码可维护性差:变量的作用域不固定,依赖于函数的调用位置,使得代码的执行结果难以预测,增加了调试和维护的难度。
JavaScript 中的作用域规则
JavaScript 采用的是词法作用域。不过,JavaScript 中的 this
关键字在一定程度上体现了动态作用域的特点,this
的值是在函数调用时动态确定的,取决于函数的调用方式。
综上所述,词法作用域注重代码的定义位置,使得代码更具可预测性和可维护性;而动态作用域则侧重于函数的调用位置,提供了更高的灵活性,但降低了代码的可维护性。
- 2.理解
JavaScript
的作用域和作用域链
答案
在 JavaScript 中,作用域和作用域链是非常重要的概念,它们对于理解变量和函数的可访问性、生命周期以及代码的执行逻辑起着关键作用。下面将详细介绍作用域和作用域链的相关知识。
作用域
作用域定义了变量和函数的可访问范围,即决定了在代码的哪个部分可以访问到特定的变量和函数。JavaScript 中有三种主要的作用域:全局作用域、函数作用域和块级作用域(ES6 引入)。
全局作用域
全局作用域是最外层的作用域,在全局作用域中声明的变量和函数可以在代码的任何地方访问。
// 全局作用域
var globalVariable = 'I am a global variable';
function globalFunction() {
console.log('I am a global function');
}
// 在任何地方都可以访问全局变量和函数
console.log(globalVariable);
globalFunction();
函数作用域
函数作用域是指在函数内部定义的变量和函数只能在该函数内部访问,外部无法直接访问。
function myFunction() {
// 函数作用域
var functionVariable = 'I am a function variable';
function innerFunction() {
console.log(functionVariable);
}
innerFunction();
}
myFunction();
// 下面这行代码会报错,因为 functionVariable 只能在 myFunction 内部访问
// console.log(functionVariable);
块级作用域
ES6 引入了 let
和 const
关键字,它们可以创建块级作用域。块级作用域是指由 {}
包裹的代码块,如 if
语句、for
循环等。在块级作用域中声明的变量只能在该块内部访问。
if (true) {
// 块级作用域
let blockVariable = 'I am a block variable';
console.log(blockVariable);
}
// 下面这行代码会报错,因为 blockVariable 只能在 if 块内部访问
// console.log(blockVariable);
作用域链
作用域链是由多个作用域组成的链表,它用于查找变量和函数的定义。当在某个作用域中访问一个变量或函数时,JavaScript 引擎会首先在当前作用域中查找,如果找不到,就会沿着作用域链向上查找,直到找到该变量或函数的定义或者到达全局作用域。
作用域链的形成
每个函数都有自己的作用域,当一个函数嵌套在另一个函数内部时,就会形成作用域链。内部函数可以访问外部函数的变量和函数,这是因为内部函数的作用域链包含了外部函数的作用域。
function outerFunction() {
var outerVariable = 'I am an outer variable';
function innerFunction() {
var innerVariable = 'I am an inner variable';
// 内部函数可以访问外部函数的变量
console.log(outerVariable);
console.log(innerVariable);
}
innerFunction();
}
outerFunction();
在上述代码中,innerFunction
的作用域链包含了它自己的作用域和 outerFunction
的作用域以及全局作用域。当 innerFunction
访问 outerVariable
时,由于在自己的作用域中找不到该变量,就会沿着作用域链向上查找,最终在 outerFunction
的作用域中找到该变量。
作用域链的意义
作用域链的存在使得 JavaScript 中的变量和函数具有层次性和可见性,保证了变量和函数的访问规则。同时,作用域链也为闭包的实现提供了基础,闭包可以访问其外部函数的变量,即使外部函数已经执行完毕。
总结
作用域定义了变量和函数的可访问范围,而作用域链则是用于查找变量和函数定义的机制。理解作用域和作用域链对于编写高质量的 JavaScript 代码、避免变量冲突和理解闭包等概念都非常重要。
- 3.理解
JavaScript
的执行上下文栈,可以应用堆栈信息快速定位问题
答案
1. 什么是执行上下文栈
在 JavaScript 中,执行上下文栈(Execution Context Stack,也被称为调用栈)是一个用于管理执行上下文的栈结构。执行上下文是 JavaScript 中的一个重要概念,它定义了变量、函数的作用域以及代码的执行环境。每一段 JavaScript 代码都在一个执行上下文中运行,而执行上下文栈则负责跟踪这些执行上下文的执行顺序。
执行上下文栈遵循后进先出(LIFO)的原则,即最后进入栈的执行上下文会最先被执行和弹出栈。当一段代码开始执行时,会创建一个新的执行上下文并将其压入栈顶;当代码执行完毕后,该执行上下文会从栈顶弹出。
2. 执行上下文的组成
每个执行上下文由三个部分组成:
- 变量对象(Variable Object):用于存储变量和函数的定义。在全局执行上下文中,变量对象就是全局对象(在浏览器中是
window
对象);在函数执行上下文中,变量对象就是活动对象(Activation Object),它包含了函数的参数、局部变量和函数定义。 - 作用域链(Scope Chain):由多个作用域组成的链表,用于查找变量和函数的定义。作用域链的第一个元素是当前执行上下文的变量对象,后续元素是外部执行上下文的变量对象。
this
指针:指向当前执行上下文的对象。this
的值在函数调用时动态确定,取决于函数的调用方式。
3. 执行上下文栈的工作流程
以下是一个简单的示例代码,用于说明执行上下文栈的工作流程:
function foo() {
function bar() {
console.log('Inside bar');
}
bar();
console.log('Inside foo');
}
foo();
console.log('Global scope');
具体步骤如下:
- 全局执行上下文入栈:当 JavaScript 代码开始执行时,首先会创建一个全局执行上下文并将其压入执行上下文栈的底部。全局执行上下文包含了全局变量和函数的定义。
foo
函数执行上下文入栈:当调用foo
函数时,会创建一个新的函数执行上下文并将其压入栈顶。该执行上下文包含了foo
函数的局部变量和函数定义。bar
函数执行上下文入栈:在foo
函数内部调用bar
函数时,会创建一个新的函数执行上下文并将其压入栈顶。该执行上下文包含了bar
函数的局部变量和函数定义。bar
函数执行上下文出栈:bar
函数执行完毕后,其执行上下文会从栈顶弹出。- 继续执行
foo
函数:bar
函数执行上下文出栈后,继续执行foo
函数中剩余的代码。 foo
函数执行上下文出栈:foo
函数执行完毕后,其执行上下文会从栈顶弹出。- 继续执行全局代码:
foo
函数执行上下文出栈后,继续执行全局代码中剩余的代码。 - 全局执行上下文出栈:当所有代码执行完毕后,全局执行上下文会从栈中弹出,执行上下文栈为空。
4. 利用堆栈信息快速定位问题
当 JavaScript 代码出现错误时,浏览器的开发者工具会提供详细的堆栈信息,通过分析这些信息可以快速定位问题所在。以下是一些常见的错误场景和利用堆栈信息定位问题的方法:
栈溢出错误(Stack Overflow Error)
栈溢出错误通常是由于递归函数没有正确的终止条件,导致执行上下文栈不断增长,最终超出了栈的最大容量。例如:
function recursiveFunction() {
recursiveFunction();
}
recursiveFunction();
当出现栈溢出错误时,浏览器的开发者工具会显示堆栈信息,指出错误发生的位置和调用栈的情况。通过查看堆栈信息,可以发现递归函数不断调用自身,没有终止条件,从而定位到问题所在。
函数调用错误
当函数调用出现错误时,堆栈信息可以帮助我们确定错误发生的具体位置和调用路径。例如:
function outer() {
function inner() {
throw new Error('Something went wrong');
}
inner();
}
outer();
当 inner
函数抛出错误时,浏览器的开发者工具会显示堆栈信息,指出错误发生在 inner
函数中,并且可以看到 inner
函数是由 outer
函数调用的。通过分析堆栈信息,可以快速定位到错误发生的位置和调用路径。
总结
执行上下文栈是 JavaScript 中用于管理执行上下文的重要机制,理解执行上下文栈的工作原理和利用堆栈信息定位问题的方法,对于编写高质量的 JavaScript 代码和调试代码非常有帮助。
- 4.
this
的原理以及几种不同使用场景的取值
答案
在 JavaScript 里,this
是一个特殊的关键字,其值会在函数调用时动态确定,具体取决于函数的调用方式。下面为你详细介绍 this
的原理以及不同使用场景下 this
的取值。
this
的原理
this
是 JavaScript 执行上下文的一部分,每个执行上下文都有一个与之关联的 this
值。当函数被调用时,会创建一个新的执行上下文,this
的值也会随之确定。this
的值不取决于函数的定义位置,而是取决于函数的调用方式。
不同使用场景下 this
的取值
1. 全局作用域中
在全局作用域中,this
指向全局对象。在浏览器环境下,全局对象是 window
;在 Node.js 环境下,全局对象是 global
。
console.log(this === window); // 在浏览器环境中输出 true
2. 函数作为普通函数调用
当函数作为普通函数调用时,this
指向全局对象(在严格模式下,this
为 undefined
)。
function foo() {
console.log(this);
}
foo(); // 在浏览器环境中输出 window 对象
在严格模式下:
'use strict';
function bar() {
console.log(this);
}
bar(); // 输出 undefined
3. 函数作为对象的方法调用
当函数作为对象的方法调用时,this
指向调用该方法的对象。
const obj = {
name: 'John',
sayName() {
console.log(this.name);
}
};
obj.sayName(); // 输出 'John'
4. 构造函数调用
当函数使用 new
关键字作为构造函数调用时,this
指向新创建的对象。
function Person(name) {
this.name = name;
console.log(this);
}
const person = new Person('Alice'); // 输出 Person { name: 'Alice' }
5. call
、apply
和 bind
方法调用
call
、apply
和 bind
是函数对象的方法,它们可以用来显式地指定 this
的值。
call
方法:第一个参数是要绑定的this
值,后续参数是传递给函数的参数。
function greet(message) {
console.log(`${message}, ${this.name}`);
}
const person1 = { name: 'Bob' };
greet.call(person1, 'Hello'); // 输出 'Hello, Bob'
apply
方法:第一个参数是要绑定的this
值,第二个参数是一个数组,数组中的元素是传递给函数的参数。
function greet(message) {
console.log(`${message}, ${this.name}`);
}
const person2 = { name: 'Charlie' };
greet.apply(person2, ['Hi']); // 输出 'Hi, Charlie'
bind
方法:创建一个新的函数,在调用时this
值会被绑定到指定的对象上。
function greet(message) {
console.log(`${message}, ${this.name}`);
}
const person3 = { name: 'David' };
const boundGreet = greet.bind(person3);
boundGreet('Hey'); // 输出 'Hey, David'
6. 箭头函数中的 this
箭头函数没有自己的 this
,它的 this
值继承自外层函数的 this
值。
const obj = {
name: 'Eve',
sayName: function() {
const arrowFunc = () => {
console.log(this.name);
};
arrowFunc();
}
};
obj.sayName(); // 输出 'Eve'
总结
this
的值在 JavaScript 中是动态的,取决于函数的调用方式。理解不同使用场景下 this
的取值规则,对于编写高质量的 JavaScript 代码和避免错误非常重要。
- 5.闭包的实现原理和作用,可以列举几个开发中闭包的实际应用
答案
闭包的实现原理
在 JavaScript 里,函数会形成闭包。闭包本质是指有权访问另一个函数作用域里变量的函数。其实现原理和 JavaScript 的作用域链密切相关。
作用域链
每个函数在创建时,都会保存一个作用域链,该作用域链是一个包含多个变量对象的列表。当函数访问一个变量时,JavaScript 会先在当前函数的变量对象里查找,如果找不到,就会顺着作用域链到上一级的变量对象中查找,一直到全局变量对象。
闭包的形成
当一个函数内部定义了另一个函数,并且内部函数引用了外部函数的变量时,即使外部函数执行完毕,其作用域内的变量也不会被销毁,因为内部函数的作用域链依然引用着这些变量。内部函数就形成了一个闭包,它可以访问并操作外部函数作用域中的变量。
以下是一个简单的闭包示例:
function outerFunction() {
let outerVariable = 'I am from outer function';
function innerFunction() {
console.log(outerVariable);
}
return innerFunction;
}
let closure = outerFunction();
closure();
在上述代码中,innerFunction
引用了 outerFunction
中的 outerVariable
变量,outerFunction
执行完毕后返回了 innerFunction
。此时,closure
变量保存了 innerFunction
的引用,由于 innerFunction
形成了闭包,它可以访问 outerFunction
作用域中的 outerVariable
变量。
闭包的作用
- 读取函数内部的变量:闭包能够让外部代码访问函数内部的变量,突破了函数作用域的限制。
- 让这些变量的值始终保持在内存中:闭包会阻止外部函数的变量被垃圾回收机制回收,使得这些变量的值可以一直保存下来,实现数据的持久化。
开发中闭包的实际应用
- 实现私有变量和方法 闭包可以用来创建私有变量和方法,外部代码无法直接访问这些私有成员,只能通过闭包提供的公共接口来操作。
function createCounter() {
let count = 0;
return {
increment: function() {
count++;
return count;
},
decrement: function() {
count--;
return count;
},
getCount: function() {
return count;
}
};
}
let counter = createCounter();
console.log(counter.getCount());
console.log(counter.increment());
console.log(counter.decrement());
在这个例子中,count
变量是私有的,外部代码无法直接访问,只能通过 increment
、decrement
和 getCount
方法来操作。
- 函数柯里化 函数柯里化是指将一个多参数函数转换为一系列单参数函数的技术。闭包可以用来实现函数柯里化。
function add(a, b) {
if (typeof b === 'undefined') {
return function(b) {
return a + b;
};
}
return a + b;
}
let addFive = add(5);
console.log(addFive(3));
在这个例子中,add
函数可以接收两个参数,也可以只接收一个参数并返回一个新的函数,新函数可以接收另一个参数并完成加法运算。
- 事件处理中的数据绑定 在事件处理函数中,闭包可以用来保存事件处理所需的数据。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<button id="btn1">Button 1</button>
<button id="btn2">Button 2</button>
<script>
function setupButtons() {
let buttons = document.querySelectorAll('button');
for (let i = 0; i < buttons.length; i++) {
(function(index) {
buttons[index].addEventListener('click', function() {
console.log(`Button ${index + 1} was clicked.`);
});
})(i);
}
}
setupButtons();
</script>
</body>
</html>
在这个例子中,使用立即执行函数创建了一个闭包,将当前的 i
值保存起来,确保每个按钮的点击事件处理函数都能正确获取到对应的索引值。
- 6.理解堆栈溢出和内存泄漏的原理,如何防止
答案
堆栈溢出和内存泄漏是在程序运行过程中可能出现的两种内存相关的问题,下面将分别介绍它们的原理及防止方法。
(1)堆栈溢出
- 原理:程序中的内存空间分为栈区和堆区。栈主要用于存储函数调用时的局部变量、参数等,它的空间是有限的。当进行大量的函数递归调用或者定义了大量的局部变量,导致栈空间被耗尽时,就会发生堆栈溢出错误。例如,一个递归函数没有正确的终止条件,就会不断地在栈中压入新的函数调用记录,最终使栈空间用完。
- 防止方法
- 检查递归函数:确保递归函数有正确的终止条件,并且在每次递归调用时都朝着终止条件靠近。可以添加适当的边界条件判断,避免无限递归。
- 优化函数调用:避免不必要的函数嵌套调用过深。如果可能,将一些函数逻辑进行合并或简化,减少栈帧的创建。
- 合理使用局部变量:避免在函数中定义过大的局部数组或对象,尽量将一些不相关的变量定义在函数外部,以减少栈空间的占用。
(2)内存泄漏
- 原理:当程序在运行过程中动态分配了内存,但在使用完毕后没有及时释放,导致这部分内存无法被再次利用,就发生了内存泄漏。随着程序的运行,不断泄漏的内存会逐渐耗尽系统资源,最终可能导致程序崩溃。常见的内存泄漏场景包括忘记释放动态分配的内存、持有不必要的对象引用等。例如,在JavaScript中使用
setTimeout
或setInterval
时,如果在回调函数中引用了外部的对象,而在不再需要这个定时器时没有正确地清除它,那么被引用的对象就无法被垃圾回收,从而导致内存泄漏。 - 防止方法
- 及时释放动态分配的内存:在使用完动态分配的内存后,一定要调用相应的释放函数来释放内存。例如,在C语言中使用
malloc
分配内存后,要使用free
函数释放;在Java中,虽然有自动的垃圾回收机制,但对于一些占用大量资源的对象,在使用完毕后可以将其引用置为null
,以便让垃圾回收器及时回收。 - 避免不必要的全局变量和静态变量:全局变量和静态变量的生命周期较长,如果在程序中大量使用,可能会导致内存泄漏。尽量将变量的作用域限制在最小范围内,避免全局污染。
- 注意事件监听和回调函数中的内存管理:在添加事件监听器或使用回调函数时,要确保在不再需要时正确地移除它们,避免因残留的引用导致内存泄漏。例如,在JavaScript中,使用
addEventListener
添加事件监听器后,要使用removeEventListener
来移除。 - 使用内存分析工具:借助一些内存分析工具来检测程序中的内存泄漏问题。例如,在浏览器中可以使用开发者工具的性能分析面板来查看内存使用情况,在Node.js中可以使用
node -prof
等工具来分析内存泄漏的原因。通过这些工具,可以找出哪些对象没有被正确释放,从而针对性地进行修复。
- 及时释放动态分配的内存:在使用完动态分配的内存后,一定要调用相应的释放函数来释放内存。例如,在C语言中使用
- 7.如何处理循环的异步操作
答案
在 JavaScript 里,处理循环的异步操作是常见需求,不过由于异步操作的特性,直接处理可能会引发问题。下面为你介绍几种常见的处理循环异步操作的方法。
1. 使用 for...of
循环结合 async/await
async/await
是 ES2017 引入的语法糖,能让异步代码以同步的方式书写,结合 for...of
循环可按顺序处理异步操作。
function asyncOperation(item) {
return new Promise((resolve) => {
setTimeout(() => {
console.log(`Processed: ${item}`);
resolve();
}, 1000);
});
}
async function processItems(items) {
for (const item of items) {
await asyncOperation(item);
}
console.log('All items processed');
}
const items = [1, 2, 3, 4, 5];
processItems(items);
解释:processItems
函数是一个异步函数,借助 for...of
循环遍历 items
数组。在每次循环中,使用 await
关键字等待 asyncOperation
这个异步操作完成,再进行下一次循环。
2. 使用 Promise.all
若循环中的异步操作相互独立,可使用 Promise.all
并行处理它们。Promise.all
会接收一个包含多个 Promise
的数组,当所有 Promise
都解决时,它会返回一个新的 Promise
。
function asyncOperation(item) {
return new Promise((resolve) => {
setTimeout(() => {
console.log(`Processed: ${item}`);
resolve();
}, 1000);
});
}
const items = [1, 2, 3, 4, 5];
const promises = items.map(item => asyncOperation(item));
Promise.all(promises)
.then(() => {
console.log('All items processed');
})
.catch(error => {
console.error('An error occurred:', error);
});
解释:先使用 map
方法将数组中的每个元素转换为一个 Promise
,接着将这些 Promise
放入 Promise.all
中。当所有 Promise
都解决时,Promise.all
返回的 Promise
也会解决,此时可以执行后续操作。
3. 使用 Promise.race
若只需处理循环中第一个完成的异步操作,可使用 Promise.race
。Promise.race
接收一个包含多个 Promise
的数组,当数组中的任意一个 Promise
解决或拒绝时,它会返回该 Promise
的结果。
function asyncOperation(item) {
return new Promise((resolve) => {
const randomTime = Math.random() * 2000;
setTimeout(() => {
console.log(`Processed: ${item}`);
resolve(item);
}, randomTime);
});
}
const items = [1, 2, 3, 4, 5];
const promises = items.map(item => asyncOperation(item));
Promise.race(promises)
.then(result => {
console.log(`First item processed: ${result}`);
})
.catch(error => {
console.error('An error occurred:', error);
});
解释:同样使用 map
方法将数组元素转换为 Promise
,然后将这些 Promise
放入 Promise.race
中。当其中一个 Promise
率先解决时,Promise.race
返回的 Promise
会立即解决,其结果就是第一个完成的异步操作的结果。
4. 使用递归处理异步循环
递归也是处理异步循环的一种方式,特别是在需要按顺序执行异步操作且循环次数不确定时。
function asyncOperation(item) {
return new Promise((resolve) => {
setTimeout(() => {
console.log(`Processed: ${item}`);
resolve();
}, 1000);
});
}
function processItemsRecursively(items, index = 0) {
if (index >= items.length) {
console.log('All items processed');
return;
}
asyncOperation(items[index])
.then(() => {
processItemsRecursively(items, index + 1);
})
.catch(error => {
console.error('An error occurred:', error);
});
}
const items = [1, 2, 3, 4, 5];
processItemsRecursively(items);
解释:processItemsRecursively
函数会递归调用自身,每次处理数组中的一个元素,直到处理完所有元素。在每次递归调用中,等待当前元素的异步操作完成后,再递归处理下一个元素。
- 8.理解模块化解决的实际问题,可列举几个模块化方案并理解其中原理
答案
在软件开发中,模块化是一种重要的编程思想和开发方式,它能够解决许多实际问题,以下是详细介绍以及常见的模块化方案和原理。
模块化解决的实际问题
1. 命名冲突
在大型项目中,如果没有模块化,所有的变量、函数和类都定义在全局作用域中,很容易出现命名冲突。例如,不同的开发者可能会定义同名的变量或函数,导致程序出现意外的行为。模块化可以将代码封装在独立的模块中,每个模块有自己的作用域,避免了全局命名冲突。
2. 代码复用
模块化使得代码可以被拆分成多个独立的模块,每个模块可以被其他模块重复使用。这样可以提高代码的复用性,减少代码的冗余。例如,一个处理日期的模块可以在多个项目中被复用。
3. 可维护性
模块化的代码结构更加清晰,每个模块只负责特定的功能,使得代码的维护和扩展更加容易。当需要修改某个功能时,只需要修改对应的模块,而不会影响到其他模块。
4. 依赖管理
在复杂的项目中,代码之间可能存在大量的依赖关系。模块化可以明确地管理这些依赖关系,使得代码的加载和执行更加有序。例如,一个模块可以声明它所依赖的其他模块,在加载该模块时,会先加载其依赖的模块。
常见的模块化方案及原理
1. CommonJS
- 应用场景:主要用于服务器端的 Node.js 环境。
- 原理:在 CommonJS 中,每个文件就是一个模块,拥有自己独立的作用域。模块通过
exports
或module.exports
来导出模块中的变量、函数或类,使用require
函数来引入其他模块。
// math.js
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;
module.exports = {
add,
subtract
};
// main.js
const math = require('./math');
console.log(math.add(2, 3));
console.log(math.subtract(5, 2));
在上述代码中,math.js
模块通过 module.exports
导出了 add
和 subtract
函数,main.js
模块使用 require
函数引入了 math.js
模块,并使用其中的函数。
2. AMD(Asynchronous Module Definition)
- 应用场景:主要用于浏览器环境,适合异步加载模块。
- 原理:AMD 采用异步加载模块的方式,通过
define
函数来定义模块,require
函数来加载模块。define
函数接收三个参数:模块名(可选)、依赖数组和模块定义函数。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>AMD Example</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js"></script>
<script>
require.config({
baseUrl: './',
paths: {
math: 'math'
}
});
require(['math'], function (math) {
console.log(math.add(2, 3));
});
</script>
</head>
<body>
</body>
</html>
// math.js
define(function () {
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;
return {
add,
subtract
};
});
在上述代码中,math.js
模块使用 define
函数定义,index.html
中使用 require
函数异步加载 math.js
模块,并使用其中的函数。
3. ES6 模块
- 应用场景:是 JavaScript 官方的模块化方案,既可以用于浏览器环境,也可以用于服务器端。
- 原理:ES6 模块使用
export
关键字来导出模块中的变量、函数或类,使用import
关键字来引入其他模块。
// math.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
// main.js
import { add, subtract } from './math.js';
console.log(add(2, 3));
console.log(subtract(5, 2));
在上述代码中,math.js
模块使用 export
关键字导出 add
和 subtract
函数,main.js
模块使用 import
关键字引入这些函数。
4. UMD(Universal Module Definition)
- 应用场景:兼容 CommonJS、AMD 和全局变量的模块化方案,使得模块可以在不同的环境中使用。
- 原理:UMD 模块通过判断当前环境是 CommonJS、AMD 还是全局环境,来选择不同的模块定义方式。
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define([], factory);
} else if (typeof module === 'object' && module.exports) {
// CommonJS
module.exports = factory();
} else {
// 全局变量
root.math = factory();
}
}(this, function () {
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;
return {
add,
subtract
};
}));
在上述代码中,通过判断 define
和 module.exports
是否存在,来确定当前环境是 AMD、CommonJS 还是全局环境,并选择相应的模块定义方式。
执行机制
- 1.为何
try
里面放return
,finally
还会执行,理解其内部机制
答案
在 JavaScript 或者 Java 等很多编程语言里,当 try
块中存在 return
语句时,finally
块依旧会执行。下面详细解释其内部机制。
基本代码示例
以 JavaScript 为例:
function test() {
try {
console.log('try block');
return 1;
} catch (error) {
console.log('catch block');
} finally {
console.log('finally block');
}
}
let result = test();
console.log(result);
上述代码里,try
块中有 return
语句,但 finally
块还是会执行。
内部机制解释
1. 执行顺序
当程序执行到 try
块中的 return
语句时,并不会马上结束函数的执行。return
语句会先计算返回值,不过在真正返回这个值之前,finally
块会被执行。这是因为 finally
块的设计目的就是确保无论 try
块中是否出现异常或者是否有 return
语句,其中的代码都一定会被执行。
2. 详细步骤
- 计算返回值:当遇到
return
语句时,程序会先计算return
后面表达式的值,并且把这个值暂存起来。比如在上面的代码中,会计算出返回值为1
,然后将其保存。 - 执行
finally
块:接着程序会跳转到finally
块,执行其中的代码。在这个过程中,即便finally
块里也有return
语句,它也会覆盖之前try
块中return
语句的返回值。 - 返回结果:当
finally
块执行完毕后,如果finally
块中没有return
语句,程序就会返回之前暂存的返回值;如果finally
块中有return
语句,就会返回finally
块中return
语句的返回值。
3. 代码示例验证
function test() {
let value = 1;
try {
console.log('try block');
return value;
} finally {
value = 2;
console.log('finally block');
}
}
let result = test();
console.log(result);
在这个例子中,try
块里 return
语句计算出的返回值是 1
并暂存。finally
块中虽然修改了 value
的值为 2
,但由于返回值已经确定,最终返回的结果还是 1
。
总结
try
块里的 return
语句会先计算返回值,然后在返回之前执行 finally
块。finally
块的执行是为了保证一些必要的清理工作(如关闭文件、释放资源等)能够被执行,无论 try
块的执行情况如何。这种机制保证了程序的健壮性和资源管理的可靠性。
- 2.
JavaScript
如何实现异步编程,可以详细描述EventLoop
机制
答案
在 JavaScript 里,异步编程是一项重要特性,它能防止阻塞主线程,让程序在等待某些操作(像网络请求、定时器等)完成时,继续执行其他任务。下面会介绍 JavaScript 实现异步编程的常见方式,并且详细阐述 Event Loop 机制。
JavaScript 实现异步编程的常见方式
1. 回调函数
回调函数是最基础的异步编程方式,把一个函数作为参数传递给另一个函数,在异步操作完成后调用这个回调函数。
function fetchData(callback) {
setTimeout(() => {
const data = { message: 'Data fetched successfully' };
callback(data);
}, 1000);
}
fetchData((result) => {
console.log(result);
});
在这个例子中,fetchData
函数模拟了一个异步操作,在 setTimeout
的回调函数里调用传入的 callback
函数,并把数据作为参数传递给它。
2. Promise
Promise 是一种更优雅的处理异步操作的方式,它有三种状态:pending
(进行中)、fulfilled
(已成功)和 rejected
(已失败)。通过 then
方法处理成功的结果,用 catch
方法处理失败的情况。
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) {
const data = { message: 'Data fetched successfully' };
resolve(data);
} else {
reject(new Error('Failed to fetch data'));
}
}, 1000);
});
}
fetchData()
.then((result) => {
console.log(result);
})
.catch((error) => {
console.error(error);
});
3. async/await
async/await
是基于 Promise 的语法糖,能让异步代码以同步的方式书写,使代码更易读和维护。
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) {
const data = { message: 'Data fetched successfully' };
resolve(data);
} else {
reject(new Error('Failed to fetch data'));
}
}, 1000);
});
}
async function main() {
try {
const result = await fetchData();
console.log(result);
} catch (error) {
console.error(error);
}
}
main();
Event Loop 机制
1. 基本概念
JavaScript 是单线程的,这意味着同一时间只能执行一个任务。为了实现异步编程,JavaScript 引入了 Event Loop 机制。Event Loop 负责协调异步任务的执行顺序,主要涉及调用栈(Call Stack)、任务队列(Task Queue)和微任务队列(Microtask Queue)。
2. 调用栈(Call Stack)
调用栈是一个后进先出(LIFO)的数据结构,用于存储正在执行的函数调用。当调用一个函数时,会将该函数的执行上下文压入调用栈;函数执行完毕后,会将其执行上下文从调用栈中弹出。
3. 任务队列(Task Queue)和微任务队列(Microtask Queue)
- 任务队列:也称为宏任务队列,用于存储异步任务的回调函数,如
setTimeout
、setInterval
、setImmediate
等。 - 微任务队列:用于存储微任务的回调函数,如 Promise 的
then
和catch
方法、MutationObserver
等。微任务的优先级高于宏任务。
4. Event Loop 的执行流程
- 执行同步代码:JavaScript 引擎首先会执行调用栈中的同步代码,将同步函数依次压入调用栈并执行,执行完毕后从调用栈中弹出。
- 处理异步任务:当遇到异步任务(如
setTimeout
、Promise 等)时,会将这些任务交给浏览器的 Web API 处理,同时继续执行后续的同步代码。 - 任务完成:当异步任务完成后,其回调函数会被放入相应的任务队列(宏任务队列或微任务队列)中。
- Event Loop 检查:当调用栈为空时,Event Loop 会开始工作。它会先检查微任务队列,如果微任务队列中有任务,会依次将微任务队列中的任务取出并放入调用栈中执行,直到微任务队列为空。
- 处理宏任务:当微任务队列为空后,Event Loop 会从宏任务队列中取出一个任务放入调用栈中执行,执行完毕后,再次检查微任务队列,重复上述步骤。
5. 示例代码及执行分析
console.log('Start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('Promise then');
});
console.log('End');
执行步骤如下:
- 执行同步代码,打印
Start
和End
。 setTimeout
是异步任务,交给 Web API 处理,其回调函数会在 0 毫秒后放入宏任务队列。Promise.resolve().then
是微任务,其回调函数会放入微任务队列。- 调用栈为空,Event Loop 开始工作,先处理微任务队列,打印
Promise then
。 - 微任务队列为空,处理宏任务队列,打印
setTimeout
。
综上所述,Event Loop 机制通过协调调用栈、任务队列和微任务队列,实现了 JavaScript 的异步编程,确保异步任务能在合适的时机执行。
- 3.宏任务和微任务分别有哪些
答案
宏任务和微任务是JavaScript中用于处理异步操作的两种任务类型。以下分别介绍常见的宏任务和微任务:
宏任务(Macrotask)
setTimeout
:设置一个定时器,在指定的延迟时间后执行回调函数。setInterval
:设置一个定时器,按照指定的时间间隔重复执行回调函数。setImmediate
:将回调函数添加到下一个事件循环迭代中执行,优先级高于setTimeout
和setInterval
。I/O
操作:例如文件读取、网络请求等,完成后会将相应的回调函数放入宏任务队列。UI 渲染
:浏览器在适当的时候会将UI渲染任务添加到宏任务队列中,以更新页面的显示。
微任务(Microtask)
Promise
的then
和catch
回调:当Promise
被解决(resolved)或被拒绝(rejected)后,then
和catch
方法中的回调函数会被添加到微任务队列中。MutationObserver
:用于监听DOM变化的API,当DOM发生变化时,相应的回调函数会在微任务队列中执行。process.nextTick
(Node.js环境):在Node.js中,process.nextTick
会将回调函数添加到微任务队列的头部,优先于其他微任务执行。
宏任务和微任务在执行顺序上有所不同。当JavaScript引擎执行完当前的同步代码后,会先检查微任务队列,将微任务队列中的所有任务执行完毕,然后再从宏任务队列中取出一个任务执行,如此循环往复。
为什么需要微任务
在JavaScript中,引入微任务主要有以下几个原因:
1、实现更精细的异步控制
- 微任务提供了一种在当前执行栈执行完毕后,下一次事件循环开始之前执行代码的机制。这使得开发者能够更精确地控制异步操作的执行顺序。例如,在处理Promise时,通过微任务可以确保
then
回调中的代码在当前同步代码和其他可能的微任务之后,下一个宏任务之前执行,从而实现对异步操作结果的及时处理,避免了结果处理的延迟。
2、避免阻塞UI渲染
- 由于微任务会在宏任务之间执行,并且在执行微任务时,浏览器不会进行UI渲染。如果有一些操作需要在不阻塞UI渲染的情况下尽快执行,就可以将其放在微任务中。例如,当通过
MutationObserver
监听DOM变化后,相关的回调函数会作为微任务执行,这样可以在不影响页面视觉更新的前提下,及时对DOM变化做出响应,进行一些数据更新或其他逻辑处理。
3、提高性能和效率
- 微任务队列的执行是在事件循环的一个阶段中完成的,相对宏任务来说,执行的时机更加紧凑。这意味着在某些情况下,可以将一些对性能要求较高、需要尽快执行的异步操作放在微任务中,以减少不必要的等待时间,提高整体的执行效率。例如,在一些动画效果的实现中,通过微任务可以在每一帧绘制之前执行一些计算操作,确保动画的流畅性。
4、支持异步操作的链式调用和组合
- 在处理复杂的异步操作时,微任务有助于实现异步操作的链式调用和组合。以Promise为例,多个
then
方法的回调函数会通过微任务机制依次执行,使得异步操作可以按照顺序进行处理,并且可以方便地对前一个操作的结果进行处理和传递,从而构建出清晰、可读的异步操作链,提高代码的可维护性和可扩展性。
- 4.可以快速分析一个复杂的异步嵌套逻辑,并掌握分析方法
答案
分析复杂的异步嵌套逻辑时,关键在于理解异步操作的执行顺序,以下为你介绍一套有效的分析方法,并结合示例进行说明。
分析方法
- 识别异步操作类型:首先要确定代码中包含哪些异步操作,常见的异步操作有
setTimeout
、setInterval
、Promise
、async/await
等。不同类型的异步操作在执行顺序和机制上有所不同。 - 明确任务队列:了解宏任务和微任务的区别,以及它们各自包含哪些异步操作。宏任务如
setTimeout
、setInterval
等,微任务如Promise
的then
和catch
方法。 - 梳理执行顺序:按照事件循环机制,同步代码先执行,当遇到异步操作时,将其放入相应的任务队列中。当调用栈为空时,先处理微任务队列中的任务,处理完后再从宏任务队列中取出一个任务执行,重复此过程。
- 绘制流程图:对于复杂的异步嵌套逻辑,绘制流程图可以帮助你更清晰地理解代码的执行流程。将每个异步操作和其回调函数用节点表示,用箭头表示执行顺序。
示例分析
console.log('1. 同步代码开始');
// 宏任务 setTimeout
setTimeout(() => {
console.log('4. setTimeout 回调开始');
// 微任务 Promise
Promise.resolve().then(() => {
console.log('6. Promise then 回调');
});
console.log('5. setTimeout 回调结束');
}, 0);
// 微任务 Promise
Promise.resolve().then(() => {
console.log('3. Promise then 回调');
});
console.log('2. 同步代码结束');
分析步骤
- 执行同步代码:首先执行同步代码,依次打印
1. 同步代码开始
和2. 同步代码结束
。 - 处理异步操作:
- 遇到
setTimeout
,将其回调函数放入宏任务队列。 - 遇到
Promise.resolve().then
,将其回调函数放入微任务队列。
- 遇到
- 处理微任务队列:当调用栈为空时,Event Loop 开始工作,先处理微任务队列中的任务,打印
3. Promise then 回调
。 - 处理宏任务队列:微任务队列处理完毕后,从宏任务队列中取出
setTimeout
的回调函数执行,打印4. setTimeout 回调开始
。 - 再次处理微任务队列:在
setTimeout
回调中又遇到Promise.resolve().then
,将其回调函数放入微任务队列。继续执行setTimeout
回调中的同步代码,打印5. setTimeout 回调结束
。此时调用栈再次为空,处理微任务队列,打印6. Promise then 回调
。
总结
通过以上步骤,我们可以清晰地分析出复杂异步嵌套逻辑的执行顺序。在实际分析中,要牢记事件循环机制,按照同步代码、微任务、宏任务的顺序依次处理,逐步梳理出代码的执行流程。
- 5.使用
Promise
实现串行
答案
在 JavaScript 里,借助 Promise
实现串行操作是一种常见需求,也就是按顺序依次执行多个异步任务。下面介绍几种实现 Promise
串行的方法。
方法一:使用 .then()
链式调用
通过 .then()
方法进行链式调用,能保证每个 Promise
按顺序执行。以下是示例代码:
function asyncTask1() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('Task 1 completed');
resolve();
}, 1000);
});
}
function asyncTask2() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('Task 2 completed');
resolve();
}, 1000);
});
}
function asyncTask3() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('Task 3 completed');
resolve();
}, 1000);
});
}
asyncTask1()
.then(asyncTask2)
.then(asyncTask3)
.then(() => {
console.log('All tasks completed');
});
在这个示例中,asyncTask1
执行完成后,才会执行 asyncTask2
,接着执行 asyncTask3
。只有当所有任务都完成后,才会执行最后的 .then()
回调。
方法二:使用 for...of
循环和 async/await
async/await
能让异步代码以同步的方式书写,结合 for...of
循环可以按顺序执行多个 Promise
。示例如下:
function asyncTask(delay, taskNumber) {
return new Promise((resolve) => {
setTimeout(() => {
console.log(`Task ${taskNumber} completed`);
resolve();
}, delay);
});
}
const tasks = [
() => asyncTask(1000, 1),
() => asyncTask(1000, 2),
() => asyncTask(1000, 3)
];
async function runTasksSequentially() {
for (const task of tasks) {
await task();
}
console.log('All tasks completed');
}
runTasksSequentially();
在这个例子中,runTasksSequentially
函数是一个异步函数,使用 for...of
循环遍历 tasks
数组,每次循环使用 await
等待当前任务完成后,再执行下一个任务。
方法三:使用递归函数
递归函数也能实现 Promise
的串行执行。示例代码如下:
function asyncTask(delay, taskNumber) {
return new Promise((resolve) => {
setTimeout(() => {
console.log(`Task ${taskNumber} completed`);
resolve();
}, delay);
});
}
const tasks = [
() => asyncTask(1000, 1),
() => asyncTask(1000, 2),
() => asyncTask(1000, 3)
];
function runTasksSequentially(index) {
if (index === tasks.length) {
console.log('All tasks completed');
return;
}
tasks[index]().then(() => {
runTasksSequentially(index + 1);
});
}
runTasksSequentially(0);
在这个示例中,runTasksSequentially
函数通过递归调用自身,按顺序执行 tasks
数组中的每个任务。当所有任务都执行完毕后,打印出 All tasks completed
。
这些方法都能实现 Promise
的串行执行,你可以根据具体需求和代码结构选择合适的方法。
- 6.
Node
与浏览器EventLoop
的差异
答案
`Node.js` 和浏览器中的 `Event Loop` 都是用于处理异步操作的机制,但由于运行环境和使用场景不同,它们存在一些差异,下面从任务队列、执行顺序等方面详细介绍。任务队列的差异
浏览器的任务队列
- 宏任务队列:包含
setTimeout
、setInterval
、setImmediate
(部分浏览器支持)、I/O
操作(如网络请求)、UI 渲染
等。 - 微任务队列:包含
Promise
的then
和catch
回调、MutationObserver
等。
Node.js 的任务队列
- 宏任务队列:
timer
阶段:执行setTimeout
和setInterval
的回调函数。I/O callbacks
阶段:处理一些系统级的I/O
回调,如网络、文件操作等。idle, prepare
阶段:仅供内部使用。poll
阶段:获取新的I/O
事件,执行I/O
相关的回调。check
阶段:执行setImmediate
的回调函数。close callbacks
阶段:执行一些关闭操作的回调,如socket.on('close')
。
- 微任务队列:包含
Promise
的then
和catch
回调、process.nextTick
(Node.js 特有的微任务)等。
执行顺序的差异
浏览器的 Event Loop 执行顺序
- 执行同步代码。
- 当调用栈为空时,检查微任务队列,依次执行微任务队列中的所有任务,直到微任务队列为空。
- 从宏任务队列中取出一个任务执行,执行完毕后,再次检查微任务队列,重复上述步骤。
Node.js 的 Event Loop 执行顺序
- 执行同步代码。
- 当调用栈为空时,进入
Event Loop
,依次经过各个阶段:timer
阶段:检查是否有setTimeout
或setInterval
的回调函数到期,如果有则执行。I/O callbacks
阶段:处理系统级的I/O
回调。idle, prepare
阶段:内部使用,一般开发者无需关注。poll
阶段:- 如果
poll
队列中有回调函数,则依次执行。 - 如果
poll
队列为空:- 如果有
setImmediate
回调,则进入check
阶段。 - 如果没有
setImmediate
回调,则等待I/O
事件的到来。
- 如果有
- 如果
check
阶段:执行setImmediate
的回调函数。close callbacks
阶段:执行关闭操作的回调。
- 在每个阶段切换之前,会检查微任务队列,执行微任务队列中的所有任务。
特殊微任务的差异
浏览器
浏览器中主要的微任务是 Promise
的 then
和 catch
回调、MutationObserver
等,没有像 process.nextTick
这样的特殊微任务。
Node.js
process.nextTick
是 Node.js 特有的微任务,它的优先级高于 Promise
的 then
和 catch
回调。在每个阶段切换之前,都会先执行 process.nextTick
队列中的所有任务。
示例代码对比
浏览器环境
console.log('Start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('Promise then');
});
console.log('End');
执行顺序:Start
-> End
-> Promise then
-> setTimeout
Node.js 环境
console.log('Start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
});
Promise.resolve().then(() => {
console.log('Promise then');
});
process.nextTick(() => {
console.log('process.nextTick');
});
console.log('End');
执行顺序可能有两种情况:
- 如果
setTimeout
先进入timer
阶段,执行顺序为:Start
->End
->process.nextTick
->Promise then
->setTimeout
->setImmediate
- 如果
setImmediate
先进入check
阶段,执行顺序为:Start
->End
->process.nextTick
->Promise then
->setImmediate
->setTimeout
综上所述,Node.js
和浏览器的 Event Loop
在任务队列、执行顺序和特殊微任务等方面存在差异,开发者在编写代码时需要根据具体的运行环境来处理异步操作。
setImmediate
setImmediate
是 Node.js 中用于异步执行代码的一个函数,同时在部分现代浏览器环境中也得到了支持。下面将从其基本概念、使用场景、与 setTimeout
和 process.nextTick
的对比等方面进行详细介绍。
1、基本概念 setImmediate
用于将一个回调函数添加到事件循环的 check
阶段执行。当事件循环进入 check
阶段时,会执行所有通过 setImmediate
注册的回调函数。
2、基本语法
setImmediate(callback[, ...args]);
callback
:必需参数,即需要异步执行的回调函数。...args
:可选参数,传递给回调函数的参数。
3、示例代码
console.log('开始');
setImmediate(() => {
console.log('setImmediate 的回调函数执行');
});
console.log('结束');
在上述代码中,setImmediate
的回调函数会在当前同步代码执行完毕后,事件循环进入 check
阶段时执行。
4、使用场景
- 异步 I/O 操作后的回调:在完成一个 I/O 操作(如文件读取、网络请求)后,如果需要执行一些后续处理逻辑,但又不想阻塞其他异步操作,可以使用
setImmediate
。例如:
const fs = require('fs');
fs.readFile('example.txt', 'utf8', (err, data) => {
if (err) {
console.error(err);
return;
}
setImmediate(() => {
// 对读取到的数据进行处理
console.log('开始处理读取到的数据');
console.log(data);
});
});
- 避免递归调用导致的栈溢出:当需要进行递归操作时,如果递归深度过大,可能会导致栈溢出错误。可以使用
setImmediate
将递归调用转换为异步操作,避免栈溢出。例如:
function recursiveFunction(count) {
if (count === 0) {
return;
}
console.log(count);
setImmediate(() => {
recursiveFunction(count - 1);
});
}
recursiveFunction(10000);
5、与 setTimeout
和 process.nextTick
的对比
- 与
setTimeout
的对比:setTimeout
用于在指定的延迟时间后执行回调函数,而setImmediate
会在当前事件循环的check
阶段执行回调函数。- 当
setTimeout
的延迟时间设置为0
时,由于setTimeout
的回调函数会在timer
阶段执行,而setImmediate
的回调函数会在check
阶段执行,它们的执行顺序可能会有所不同,取决于事件循环的状态。例如:
setTimeout(() => {
console.log('setTimeout 回调');
}, 0);
setImmediate(() => {
console.log('setImmediate 回调');
});
在不同的环境和条件下,上述代码的输出顺序可能不同。
- 与
process.nextTick
的对比:process.nextTick
是 Node.js 特有的微任务,它的优先级高于setImmediate
。在每个事件循环阶段切换之前,会先执行process.nextTick
队列中的所有回调函数。setImmediate
是宏任务,会在check
阶段执行。例如:
process.nextTick(() => {
console.log('process.nextTick 回调');
});
setImmediate(() => {
console.log('setImmediate 回调');
});
上述代码中,process.nextTick
的回调函数会先执行,然后才会执行 setImmediate
的回调函数。
- 7.如何在保证页面运行流畅的情况下处理海量数据
答案
在保证页面运行流畅的情况下处理海量数据,可以从数据获取、数据处理、页面渲染和性能优化等多个方面入手,以下是一些常见的方法:
数据获取
- 分页加载:将数据分成多个较小的部分,每次只加载当前页面所需的数据。用户滚动到页面底部或点击“加载更多”按钮时,再加载下一页的数据。这样可以避免一次性加载大量数据导致页面卡顿。
- 懒加载:对于图片、脚本等资源,采用懒加载的方式。只有当这些资源进入浏览器的可视区域时,才进行加载。这样可以减少页面初始加载时的资源请求数量,提高页面加载速度。
数据处理
- 服务器端处理:在服务器端对数据进行预处理,如筛选、排序、聚合等操作,只返回前端页面所需的数据。这样可以减轻前端的处理负担,提高数据处理效率。
- Web Workers:使用Web Workers在后台线程中处理数据,避免阻塞主线程。Web Workers可以与主线程进行通信,将处理结果返回给主线程。例如,可以将数据的解析、计算等操作放在Web Workers中进行。
页面渲染
- 虚拟列表:当需要展示大量列表数据时,使用虚拟列表技术。只渲染当前可视区域内的列表项,而不是渲染所有列表项。当用户滚动列表时,动态更新可视区域内的列表项。这样可以大大减少页面的DOM节点数量,提高渲染性能。
- 优化DOM操作:尽量减少对DOM的直接操作,避免频繁地创建、更新和删除DOM节点。可以使用文档片段(DocumentFragment)来批量操作DOM,减少重排和重绘的次数。
性能优化
- 缓存:对经常访问的数据进行缓存,如使用浏览器的本地存储(Local Storage)或内存缓存。下次访问相同数据时,可以直接从缓存中获取,避免再次请求服务器,提高数据加载速度。
- 优化图片:对图片进行压缩和优化,选择合适的图片格式(如JPEG、PNG、WebP等)。根据图片在页面中的显示大小,调整图片的分辨率,避免加载过大的图片。
- 监测和分析性能:使用浏览器的开发者工具或专业的性能监测工具,对页面的性能进行监测和分析。找出性能瓶颈,如加载时间过长的资源、耗时的脚本等,并针对性地进行优化。
语法和API
- 1.理解
ECMAScript
和JavaScript
的关系
答案
ECMAScript和JavaScript之间联系紧密又有区别,下面将从二者的概念、发展历程、相互关系几个方面详细介绍它们之间的关系。
概念
- ECMAScript:它是一种由Ecma国际(前身为欧洲计算机制造商协会)制定的脚本语言标准化规范,其英文全称为European Computer Manufacturers Association Script。该规范为脚本语言的语法、语义等方面制定了统一的标准,目的是让不同的实现者可以根据这个标准来开发兼容的脚本语言。
- JavaScript:它是由Netscape公司开发的一种广泛应用于Web开发的脚本语言,可用于实现网页的交互效果、动态内容展示等功能。
发展历程
- ECMAScript:1996 年 11 月,Netscape 公司将 JavaScript 提交给欧洲计算机制造商协会进行标准化。1997 年,Ecma 国际发布了第一版 ECMAScript 标准(ECMA - 262)。此后,ECMAScript 不断发展,陆续发布了多个版本,如 ES5、ES6(也称为 ES2015)、ES2016、ES2017 等,每个版本都增加了新的语法特性和功能。
- JavaScript:1995 年,Netscape 公司的 Brendan Eich 在网景浏览器中开发了 JavaScript 语言,最初命名为 Mocha,后改名为 LiveScript,最终定名为 JavaScript。随着 Web 技术的发展,JavaScript 不断发展壮大,应用范围也越来越广泛。
相互关系
- ECMAScript 是 JavaScript 的标准:ECMAScript 为 JavaScript 制定了语法和语义的规范,JavaScript 是 ECMAScript 的一种具体实现。也就是说,JavaScript 必须遵循 ECMAScript 标准来实现其语法和功能。例如,ECMAScript 规定了变量声明、函数定义、控制流语句等基本语法,JavaScript 按照这些规定来实现相应的功能。
- JavaScript 扩展了 ECMAScript:除了遵循 ECMAScript 标准外,JavaScript 还在浏览器环境中扩展了许多与 Web 相关的功能,如 DOM(文档对象模型)操作、BOM(浏览器对象模型)操作、事件处理等。这些功能不属于 ECMAScript 标准的范畴,但它们是 JavaScript 在 Web 开发中不可或缺的一部分。例如,通过 JavaScript 可以使用
document.getElementById()
方法来获取网页中的元素,这是基于浏览器提供的 DOM API 实现的,而不是 ECMAScript 标准规定的。 - 其他实现:除了 JavaScript 之外,还有其他语言也是 ECMAScript 标准的实现,如 JScript(微软开发的一种脚本语言)、ActionScript(用于 Adobe Flash 平台的脚本语言)等。这些语言都遵循 ECMAScript 标准,但在具体的实现和应用场景上可能会有所不同。
综上所述,ECMAScript 是 JavaScript 的标准化规范,JavaScript 是 ECMAScript 的一种具体实现并在 Web 环境中进行了扩展。随着 ECMAScript 标准的不断发展,JavaScript 也在不断引入新的特性和功能,以满足日益复杂的 Web 开发需求。
- 2.熟练运用
es5
、es6
提供的语法规范
答案
ES5(ECMAScript 5)和 ES6(ECMAScript 2015)为 JavaScript 带来了诸多实用的语法特性,下面为你详细介绍一些常用语法规范及示例。
ES5 常用语法规范
1. 严格模式
严格模式对 JavaScript 代码增加了更严格的语法和行为检查,有助于编写更规范、更安全的代码。在脚本或函数开头添加 "use strict";
即可开启。
function strictFunction() {
"use strict";
// 严格模式下不允许未声明就使用变量
// x = 10; // 这行代码会报错
var x = 10;
return x;
}
console.log(strictFunction());
2. Object.defineProperty()
用于直接在一个对象上定义一个新属性,或者修改一个现有属性的配置,并返回这个对象。
var person = {};
Object.defineProperty(person, 'name', {
value: 'John',
writable: false, // 不可修改
enumerable: true, // 可枚举
configurable: false // 不可重新配置
});
person.name = 'Jane'; // 由于 writable 为 false,这行代码不会改变 name 的值
console.log(person.name);
3. 数组方法
Array.prototype.forEach()
:对数组的每个元素执行一次提供的函数。
var numbers = [1, 2, 3];
numbers.forEach(function (number) {
console.log(number);
});
Array.prototype.map()
:创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。
var squaredNumbers = numbers.map(function (number) {
return number * number;
});
console.log(squaredNumbers);
Array.prototype.filter()
:创建一个新数组,其包含通过所提供函数实现的测试的所有元素。
var evenNumbers = numbers.filter(function (number) {
return number % 2 === 0;
});
console.log(evenNumbers);
ES6 常用语法规范
1. 块级作用域
引入 let
和 const
关键字,用于声明块级作用域的变量和常量。
if (true) {
let blockVariable = 'I am a block variable';
const blockConstant = 'I am a block constant';
console.log(blockVariable);
console.log(blockConstant);
}
// 下面这行代码会报错,因为 blockVariable 和 blockConstant 只能在 if 块内部访问
// console.log(blockVariable);
2. 箭头函数
提供了更简洁的函数定义语法,并且箭头函数没有自己的 this
,它继承自外层函数的 this
值。
var numbers = [1, 2, 3];
var squaredNumbers = numbers.map(number => number * number);
console.log(squaredNumbers);
3. 模板字符串
使用反引号(`)来定义字符串,可以在字符串中嵌入表达式。
var name = 'John';
var message = `Hello, ${name}!`;
console.log(message);
4. 解构赋值
可以从数组或对象中提取值,并赋值给变量。
// 数组解构
var [first, second] = [1, 2];
console.log(first);
console.log(second);
// 对象解构
var person = { name: 'John', age: 30 };
var { name, age } = person;
console.log(name);
console.log(age);
5. 类和继承
引入了 class
关键字来定义类,使用 extends
关键字实现类的继承。
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a noise.`);
}
}
class Dog extends Animal {
speak() {
console.log(`${this.name} barks.`);
}
}
var dog = new Dog('Buddy');
dog.speak();
6. Promise
对象
用于处理异步操作,避免回调地狱。
function asyncOperation() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Operation completed');
}, 1000);
});
}
asyncOperation()
.then(result => {
console.log(result);
})
.catch(error => {
console.error(error);
});
通过熟练运用 ES5 和 ES6 的这些语法规范,可以编写出更简洁、更高效、更具可读性的 JavaScript 代码。
- 3.熟练掌握
JavaScript
提供的全局对象(例如Date
、Math
)、全局函数(例如decodeURI
、isNaN
)、全局属性(例如Infinity
、undefined
)
答案
在 JavaScript 里,全局对象、全局函数和全局属性是非常重要的基础内容,下面将详细介绍一些常用的全局对象、全局函数和全局属性。
全局对象
Date
对象
Date
对象用于处理日期和时间。可以使用它来创建日期实例、获取和设置日期时间的各个部分,以及进行日期时间的计算。
// 创建一个表示当前时间的 Date 对象
const now = new Date();
console.log(now);
// 创建一个指定日期的 Date 对象
const specificDate = new Date('2024-10-01');
console.log(specificDate);
// 获取年份
const year = now.getFullYear();
console.log(year);
// 获取月份(注意:月份从 0 开始计数,0 表示 1 月,11 表示 12 月)
const month = now.getMonth();
console.log(month + 1);
// 获取日期
const date = now.getDate();
console.log(date);
// 获取小时
const hours = now.getHours();
console.log(hours);
// 设置日期
now.setDate(15);
console.log(now);
Math
对象
Math
对象提供了许多用于数学计算的属性和方法。
// 获取圆周率
const pi = Math.PI;
console.log(pi);
// 生成 0 到 1 之间的随机数
const random = Math.random();
console.log(random);
// 向上取整
const ceilNum = Math.ceil(3.2);
console.log(ceilNum);
// 向下取整
const floorNum = Math.floor(3.8);
console.log(floorNum);
// 四舍五入
const roundNum = Math.round(3.5);
console.log(roundNum);
// 求绝对值
const absNum = Math.abs(-5);
console.log(absNum);
// 求最大值
const maxNum = Math.max(1, 2, 3, 4, 5);
console.log(maxNum);
// 求最小值
const minNum = Math.min(1, 2, 3, 4, 5);
console.log(minNum);
全局函数
decodeURI
和 encodeURI
encodeURI
用于对 URI(统一资源标识符)进行编码,将特殊字符转换为 URI 安全的格式。decodeURI
用于对经过encodeURI
编码的 URI 进行解码。
const uri = 'https://example.com/?name=张三';
const encodedUri = encodeURI(uri);
console.log(encodedUri);
const decodedUri = decodeURI(encodedUri);
console.log(decodedUri);
isNaN
isNaN
函数用于判断一个值是否为 NaN
(非数字)。
const num1 = 10;
const num2 = 'abc';
const num3 = NaN;
console.log(isNaN(num1));
console.log(isNaN(num2));
console.log(isNaN(num3));
parseInt
和 parseFloat
parseInt
用于将一个字符串解析为整数。parseFloat
用于将一个字符串解析为浮点数。
const str1 = '123';
const str2 = '123.45';
const intNum = parseInt(str1);
console.log(intNum);
const floatNum = parseFloat(str2);
console.log(floatNum);
全局属性
Infinity
和 -Infinity
Infinity
表示正无穷大,-Infinity
表示负无穷大。
const positiveInfinity = 1 / 0;
const negativeInfinity = -1 / 0;
console.log(positiveInfinity === Infinity);
console.log(negativeInfinity === -Infinity);
NaN
NaN
表示非数字,通常在进行无效的数学运算时会得到 NaN
。
const result = 'abc' / 2;
console.log(result === NaN);
console.log(isNaN(result));
undefined
undefined
表示变量已声明但未赋值,或者函数没有返回值。
let variable;
console.log(variable === undefined);
function noReturnValue() {}
const returnValue = noReturnValue();
console.log(returnValue === undefined);
通过对这些全局对象、全局函数和全局属性的学习和使用,你能够更高效地处理日期时间、数学计算、字符串编码解码等常见的编程任务。
- 4.熟练应用
map
、reduce
、filter
等高阶函数解决问题
答案
在 JavaScript 里,map
、reduce
、filter
都是数组的高阶函数,它们可以让你以声明式的方式处理数组,编写更加简洁和高效的代码。下面会详细介绍这些函数的用法,并且给出一些实际应用的示例。
map
函数
map
函数会对数组中的每个元素执行一次提供的函数,然后返回一个新数组,新数组中的元素是原数组元素经过处理后的结果。
语法
const newArray = array.map((currentValue, index, array) => {
// 处理逻辑
return processedValue;
});
currentValue
:当前正在处理的数组元素。index
:当前元素的索引。array
:调用map
方法的数组。
示例
将数组中的每个元素都乘以 2。
const numbers = [1, 2, 3, 4, 5];
const doubledNumbers = numbers.map(num => num * 2);
console.log(doubledNumbers);
filter
函数
filter
函数会创建一个新数组,新数组中的元素是原数组中满足所提供函数测试的所有元素。
语法
const newArray = array.filter((currentValue, index, array) => {
// 测试条件
return condition;
});
示例
筛选出数组中的偶数。
const numbers = [1, 2, 3, 4, 5];
const evenNumbers = numbers.filter(num => num % 2 === 0);
console.log(evenNumbers);
reduce
函数
reduce
函数对数组中的每个元素执行一个由你提供的 reducer
函数,将其结果汇总为单个返回值。
语法
const result = array.reduce((accumulator, currentValue, index, array) => {
// 处理逻辑
return accumulator + processedValue;
}, initialValue);
accumulator
:累加器,用于存储上一次调用reducer
函数的返回值。currentValue
:当前正在处理的数组元素。index
:当前元素的索引。array
:调用reduce
方法的数组。initialValue
:可选参数,累加器的初始值。如果没有提供,数组的第一个元素会作为初始值,并且从第二个元素开始执行reducer
函数。
示例
计算数组中所有元素的总和。
const numbers = [1, 2, 3, 4, 5];
const sum = numbers.reduce((acc, num) => acc + num, 0);
console.log(sum);
综合应用示例
假设有一个包含对象的数组,每个对象代表一个人,包含 name
和 age
属性。现在要完成以下任务:
- 筛选出年龄大于 18 岁的人。
- 提取这些人的名字。
- 把这些名字用逗号连接成一个字符串。
const people = [
{ name: 'Alice', age: 20 },
{ name: 'Bob', age: 15 },
{ name: 'Charlie', age: 25 },
{ name: 'David', age: 12 }
];
const namesOfAdults = people
.filter(person => person.age > 18)
.map(person => person.name)
.reduce((str, name) => str ? `${str}, ${name}` : name, '');
console.log(namesOfAdults);
上述代码首先使用 filter
函数筛选出年龄大于 18 岁的人,接着用 map
函数提取这些人的名字,最后使用 reduce
函数把名字连接成一个字符串。通过组合使用这些高阶函数,能够以简洁的方式完成复杂的数据处理任务。
- 5.
setInterval
需要注意的点,使用settimeout
实现setInterval
答案
setInterval
需要注意的点
1. 清除定时器
setInterval
会按照指定的时间间隔重复执行函数,直到手动清除定时器。如果不清除,它会一直运行,可能会导致性能问题,甚至出现内存泄漏。可以使用 clearInterval
函数来清除定时器,该函数接受一个定时器 ID 作为参数,这个 ID 是 setInterval
函数的返回值。
// 设置定时器
const intervalId = setInterval(() => {
console.log('This will repeat every 2 seconds');
}, 2000);
// 5 秒后清除定时器
setTimeout(() => {
clearInterval(intervalId);
console.log('Interval cleared');
}, 5000);
2. 执行时机
setInterval
不会等待前一次函数执行完毕就会开始下一次计时。如果函数执行时间超过了设定的时间间隔,那么函数会连续执行,可能会导致一些意外的结果。
function longRunningTask() {
// 模拟耗时操作
const start = Date.now();
while (Date.now() - start < 3000) {}
console.log('Task completed');
}
// 设置时间间隔为 2 秒
setInterval(longRunningTask, 2000);
在上述代码中,longRunningTask
函数执行时间超过了 2 秒,setInterval
不会等待它执行完就会尝试再次调用,可能会导致多个任务同时执行。
3. 上下文问题
在定时器函数中,this
的指向可能会与预期不符。在非严格模式下,this
会指向全局对象(在浏览器中是 window
),在严格模式下是 undefined
。
const obj = {
name: 'Example',
startInterval() {
setInterval(function() {
console.log(this.name); // 这里的 this 指向全局对象或 undefined
}, 1000);
}
};
obj.startInterval();
使用 setTimeout
实现 setInterval
可以通过递归调用 setTimeout
来模拟 setInterval
的功能,这样可以避免 setInterval
不等待函数执行完毕就开始下一次计时的问题。
function mySetInterval(callback, delay) {
function interval() {
callback();
setTimeout(interval, delay);
}
setTimeout(interval, delay);
}
// 使用示例
mySetInterval(() => {
console.log('This is a custom interval');
}, 2000);
在上述代码中,mySetInterval
函数接受一个回调函数和一个时间间隔作为参数。在 interval
函数内部,先执行回调函数,然后再使用 setTimeout
递归调用 interval
函数,从而实现了类似 setInterval
的功能。这种方式会等待前一次回调函数执行完毕后才会开始下一次计时。
同时,为了能够清除这个自定义的定时器,可以对上述代码进行扩展,返回一个清除定时器的函数:
function mySetInterval(callback, delay) {
let timerId;
function interval() {
callback();
timerId = setTimeout(interval, delay);
}
timerId = setTimeout(interval, delay);
return function clearMyInterval() {
clearTimeout(timerId);
};
}
// 使用示例
const clearIntervalFunction = mySetInterval(() => {
console.log('This is a custom interval');
}, 2000);
// 5 秒后清除定时器
setTimeout(() => {
clearIntervalFunction();
console.log('Custom interval cleared');
}, 5000);
这样就可以像使用 clearInterval
一样清除自定义的定时器。
- 6.
JavaScript
提供的正则表达式API
、可以使用正则表达式(邮箱校验、URL
解析、去重等)解决常见问题
答案
JavaScript 提供的正则表达式 API
在 JavaScript 中,正则表达式是用于匹配字符串模式的强大工具,有以下常用的 API:
1. 创建正则表达式
可以使用正则表达式字面量或 RegExp
构造函数来创建正则表达式对象。
// 使用正则表达式字面量
const regex1 = /abc/;
// 使用 RegExp 构造函数
const regex2 = new RegExp('abc');
2. 正则表达式对象的方法
test()
用于测试字符串中是否存在匹配正则表达式的模式,返回布尔值。
const regex = /abc/;
const str = 'abcd';
console.log(regex.test(str)); // 输出: true
exec()
在字符串中执行查找匹配的正则表达式,返回一个数组,包含匹配的结果信息;如果没有匹配到,则返回 null
。
const regex = /abc/;
const str = 'abcd';
const result = regex.exec(str);
console.log(result);
// 输出: [ 'abc', index: 0, input: 'abcd', groups: undefined ]
3. 字符串对象的方法
match()
在字符串中查找匹配的正则表达式,返回一个数组,包含所有匹配的结果;如果没有匹配到,则返回 null
。
const regex = /abc/g; // g 标志表示全局匹配
const str = 'abcabc';
const result = str.match(regex);
console.log(result); // 输出: [ 'abc', 'abc' ]
replace()
用于替换字符串中匹配正则表达式的部分,返回替换后的新字符串。
const regex = /abc/g;
const str = 'abcabc';
const newStr = str.replace(regex, 'def');
console.log(newStr); // 输出: 'defdef'
search()
在字符串中查找匹配正则表达式的位置,返回第一个匹配项的索引;如果没有匹配到,则返回 -1。
const regex = /abc/;
const str = 'abcd';
const index = str.search(regex);
console.log(index); // 输出: 0
split()
根据正则表达式将字符串分割成数组,返回分割后的数组。
const regex = /,/;
const str = 'apple,banana,orange';
const arr = str.split(regex);
console.log(arr); // 输出: [ 'apple', 'banana', 'orange' ]
使用正则表达式解决常见问题
1. 邮箱校验
邮箱地址通常遵循一定的格式,如 username@domain.com
。可以使用正则表达式来校验邮箱地址的有效性。
function validateEmail(email) {
const regex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return regex.test(email);
}
const email1 = 'test@example.com';
const email2 = 'invalid_email';
console.log(validateEmail(email1)); // 输出: true
console.log(validateEmail(email2)); // 输出: false
2. URL 解析
可以使用正则表达式来解析 URL 的各个部分,如协议、域名、路径等。
function parseURL(url) {
const regex = /^(\w+):\/\/([^\/]+)(.*)$/;
const result = regex.exec(url);
if (result) {
return {
protocol: result[1],
domain: result[2],
path: result[3]
};
}
return null;
}
const url = 'https://www.example.com/path/to/page';
const parsed = parseURL(url);
console.log(parsed);
// 输出: { protocol: 'https', domain: 'www.example.com', path: '/path/to/page' }
3. 去重
假设要去除字符串中连续重复的字符,可以使用正则表达式和 replace()
方法。
function removeDuplicates(str) {
const regex = /(.)\1+/g;
return str.replace(regex, '$1');
}
const str = 'aaabbbccc';
const newStr = removeDuplicates(str);
console.log(newStr); // 输出: 'abc'
这些示例展示了正则表达式在 JavaScript 中的强大功能,可以帮助你高效地处理字符串匹配和处理问题。
- 7.
JavaScript
异常处理的方式,统一的异常处理方案
答案
在 JavaScript 中,异常处理是保证代码健壮性和稳定性的重要手段。以下将介绍 JavaScript 异常处理的常见方式以及一种统一的异常处理方案。
JavaScript 异常处理的常见方式
1. try...catch...finally
语句
这是 JavaScript 中最基本的异常处理机制,其基本语法如下:
try {
// 可能会抛出异常的代码块
let result = 1 / 0; // 这里会抛出异常
console.log(result);
} catch (error) {
// 当 try 块中的代码抛出异常时,会执行这里的代码
console.error('捕获到异常:', error.message);
} finally {
// 无论 try 块中的代码是否抛出异常,finally 块中的代码都会执行
console.log('finally 块执行');
}
在上述代码中,try
块里的 1 / 0
会抛出异常,catch
块会捕获这个异常并输出错误信息,finally
块则会在最后执行。
2. throw
语句
throw
语句用于手动抛出一个异常,你可以抛出一个内置的错误类型(如 Error
、SyntaxError
等),也可以抛出自定义的错误对象。
function divide(a, b) {
if (b === 0) {
// 手动抛出一个异常
throw new Error('除数不能为零');
}
return a / b;
}
try {
let result = divide(10, 0);
console.log(result);
} catch (error) {
console.error('捕获到异常:', error.message);
}
3. Promise
中的异常处理
在使用 Promise
进行异步操作时,可以使用 .catch()
方法来捕获 Promise
被拒绝时抛出的异常。
function asyncOperation() {
return new Promise((resolve, reject) => {
setTimeout(() => {
// 模拟一个错误
reject(new Error('异步操作出错'));
}, 1000);
});
}
asyncOperation()
.then(result => {
console.log(result);
})
.catch(error => {
console.error('捕获到异常:', error.message);
});
4. async/await
中的异常处理
在使用 async/await
进行异步操作时,可以使用 try...catch
语句来捕获异常。
async function main() {
try {
const result = await asyncOperation();
console.log(result);
} catch (error) {
console.error('捕获到异常:', error.message);
}
}
main();
统一的异常处理方案
为了实现统一的异常处理,可以创建一个全局的异常处理函数,并在合适的地方调用它。以下是一个简单的示例:
// 全局异常处理函数
function handleGlobalError(error) {
console.error('全局异常处理:', error.message);
// 可以在这里添加更多的处理逻辑,如发送错误日志到服务器等
}
// 封装异步操作的函数
async function wrapperAsyncFunction(asyncFunction) {
try {
const result = await asyncFunction();
return result;
} catch (error) {
handleGlobalError(error);
return null;
}
}
// 模拟一个异步操作
function asyncOperation() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('异步操作出错'));
}, 1000);
});
}
// 使用封装的函数进行异常处理
wrapperAsyncFunction(asyncOperation);
在上述代码中,handleGlobalError
是全局异常处理函数,wrapperAsyncFunction
是一个封装异步操作的函数,它会捕获异步操作中抛出的异常并调用全局异常处理函数进行处理。这样,在整个应用中,所有的异步操作都可以使用 wrapperAsyncFunction
进行封装,从而实现统一的异常处理。
对于同步代码,可以在调用函数的地方统一使用 try...catch
并调用全局异常处理函数:
function syncFunction() {
throw new Error('同步操作出错');
}
try {
syncFunction();
} catch (error) {
handleGlobalError(error);
}
通过这种方式,可以将异常处理逻辑集中管理,提高代码的可维护性。
二、HTML和CSS
HTML
- 1.从规范的角度理解
HTML
,从分类和语义的角度使用标签
答案
1、诞生背景:
- HTML 是在 20 世纪计算机技术发展、互联网兴起以及信息共享需求背景下,由蒂姆・伯纳斯 - 李为解决网络信息展示与交流问题而提出的一种超文本标记语言。
2、规范
- HyperTextMarkupLanguage
3、分类
- 结构型:
<header><body><footer><aside><main><section><article>
- 导航型:
<nav><ul><li>
- 文本型:
<p><pre><code>
4、语义化目标:可读性、可维护性
(人的可读性)便于开发者理解和维护代码
:使用语义化标签能清晰地表达页面的结构和内容,让开发者在阅读和编写代码时更容易理解每个部分的功能和作用,提高代码的可读性和可维护性。(搜索引擎的可读性)有利于搜索引擎优化
:搜索引擎能够更好地理解语义化后的网页内容,准确识别页面的主题、结构和重要信息,从而提高页面在搜索结果中的排名,增加网站的流量。(机器的可读性)提高无障碍访问性
:对于残障人士使用的辅助技术,如屏幕阅读器,语义化的 HTML 可以帮助它们更准确地解读页面内容,为残障人士提供更好的访问体验。
- 2.常用页面标签的默认样式、自带属性、不同浏览器的差异、处理浏览器兼容问题的方式
答案
以下为你介绍一些常用 HTML 页面标签的默认样式、自带属性、不同浏览器的差异以及处理浏览器兼容问题的方式。
1. <div>
标签
- 默认样式:块级元素,会独占一行,宽度默认是父元素的 100%,没有内边距、边框和外边距(在不同浏览器中,可能会有微小的默认 margin 和 padding 差异)。
- 自带属性:
id
、class
、style
、title
等通用属性,还可使用hidden
等布尔属性。 - 不同浏览器的差异:默认的 margin 和 padding 可能不同,不过差距很小。一些旧版本浏览器可能对某些 CSS3 样式的支持不完全。
- 处理兼容问题的方式:使用 CSS 重置样式表,如
* { margin: 0; padding: 0; }
来统一不同浏览器的默认样式。
2. <p>
标签
- 默认样式:块级元素,上下有一定的外边距,用于表示段落文本。
- 自带属性:通用属性如
id
、class
、style
、title
等。 - 不同浏览器的差异:默认的外边距大小可能在不同浏览器中略有不同。
- 处理兼容问题的方式:通过 CSS 自定义外边距,例如
p { margin: 1em 0; }
来统一段落的外边距。
3. <a>
标签
- 默认样式:内联元素,文本通常带有下划线,颜色为蓝色(未访问)、紫色(已访问)。
- 自带属性:
href
(指定链接地址)、target
(指定链接打开方式,如_blank
表示在新窗口打开)、title
(鼠标悬停时显示的提示信息)等。 - 不同浏览器的差异:在某些旧版本浏览器中,
target
属性的兼容性可能存在问题;链接颜色和下划线样式可能有细微差别。 - 处理兼容问题的方式:使用 CSS 统一链接的样式,如
a { color: #007bff; text-decoration: none; }
去除下划线并统一颜色。
4. <img>
标签
- 默认样式:内联元素,没有边框(在某些旧浏览器中,可能会有默认边框)。
- 自带属性:
src
(指定图片的源地址)、alt
(图片无法显示时的替代文本)、width
和height
(指定图片的宽度和高度)等。 - 不同浏览器的差异:图片的缩放算法可能不同,导致图片在不同浏览器中的显示效果略有差异;旧版本浏览器可能不支持某些图片格式。
- 处理兼容问题的方式:使用 CSS 控制图片的缩放和布局,如
img { max-width: 100%; height: auto; }
确保图片在不同设备上自适应显示。同时,可以提供多种图片格式供不同浏览器选择。
5. <ul>
和 <ol>
标签
- 默认样式:块级元素,
<ul>
列表项前有实心圆点,<ol>
列表项前有数字序号,都有一定的内边距。 - 自带属性:通用属性如
id
、class
、style
等,<ol>
还可以使用start
属性指定序号的起始值。 - 不同浏览器的差异:列表项的缩进和符号样式可能不同。
- 处理兼容问题的方式:使用 CSS 自定义列表样式,如
ul { list-style-type: none; padding: 0; }
去除默认的列表样式和内边距。
6. <input>
标签
- 默认样式:外观因
type
属性而异,如type="text"
是文本输入框,type="button"
是按钮等。 - 自带属性:
type
(指定输入框的类型)、name
(表单提交时的名称)、value
(输入框的值)、placeholder
(输入框的占位文本)等。 - 不同浏览器的差异:输入框的外观、尺寸和样式在不同浏览器中可能有较大差异;一些 HTML5 新增的
type
属性(如email
、date
等)在旧版本浏览器中可能不支持。 - 处理兼容问题的方式:使用 CSS 统一输入框的样式,如
input { border: 1px solid #ccc; padding: 5px; }
。对于不支持 HTML5 新增type
属性的浏览器,可以使用 JavaScript 进行 polyfill 处理。
7. <select>
标签
- 默认样式:下拉选择框,有特定的外观和交互方式。
- 自带属性:
name
(表单提交时的名称)、multiple
(是否允许多选)等。 - 不同浏览器的差异:下拉选择框的外观、尺寸和样式在不同浏览器中可能有较大差异。
- 处理兼容问题的方式:使用 CSS 自定义下拉选择框的样式,如改变背景颜色、边框样式等。也可以使用 JavaScript 插件来实现统一的下拉选择框效果。
- 3.元信息类标签(
head
、title
、meta
)的使用目的和配置方法
答案
head
、title
、meta
等元信息类标签在 HTML 中用于提供关于网页的元数据,帮助浏览器和搜索引擎更好地理解和处理网页。以下是它们的使用目的和配置方法:
<head>
标签
- 使用目的:
<head>
标签是 HTML 文档的头部标签,它包含了文档的元数据,如文档标题、样式表、脚本、字符编码声明等。这些信息不会直接显示在网页内容中,但对于浏览器正确渲染页面以及搜索引擎索引页面起着重要作用。 - 配置方法:在 HTML 文档的开头,紧跟在
<html>
标签之后使用<head>
标签,然后在<head>
标签内部添加各种元信息标签和其他相关元素。例如:
<head>
<title>网页标题</title>
<meta charset="UTF-8">
<link rel="stylesheet" href="styles.css">
<script src="script.js"></script>
</head>
<title>
标签
- 使用目的:
<title>
标签用于定义网页的标题,它会显示在浏览器的标题栏或标签页上,同时也是搜索引擎结果中显示的重要信息之一,帮助用户快速了解网页的主题。 - 配置方法:
<title>
标签必须放在<head>
标签内部,其内容是网页的标题文本。例如:
<head>
<title>我的精彩网页</title>
</head>
<meta>
标签
- 使用目的:
<meta>
标签用于提供各种元数据,如字符编码、页面描述、关键词、作者信息、视口设置等,以帮助搜索引擎优化(SEO)、浏览器渲染和移动设备适配等。 - 配置方法:
- 设置字符编码:使用
charset
属性指定文档的字符编码,确保浏览器正确解析页面中的字符。例如:
- 设置字符编码:使用
<meta charset="UTF-8">
- **设置页面描述**:使用 `name="description"` 和 `content` 属性来描述网页的内容,这有助于搜索引擎在搜索结果中显示更准确的摘要。例如:
<meta name="description" content="这是一个关于旅游的网站,提供各种旅游攻略和景点介绍。">
- **设置关键词**:使用 `name="keywords"` 和 `content` 属性来指定与网页内容相关的关键词,多个关键词之间用逗号分隔。不过,现在搜索引擎对关键词元标签的依赖度已经降低。例如:
<meta name="keywords" content="旅游,攻略,景点,美食">
- **设置作者信息**:使用 `name="author"` 和 `content` 属性来指定网页的作者。例如:
<meta name="author" content="张三">
- **设置视口**:用于移动端网页适配,使用 `name="viewport"` 和 `content` 属性来设置视口的宽度、初始缩放比例等。例如:
<meta name="viewport" content="width=device-width, initial-scale=1.0">
字符集的历史演化
是一个随着计算机技术发展和全球信息交流需求不断变化的过程,主要经历了以下几个阶段:
早期单字节字符集
- ASCII 字符集:美国信息交换标准代码(ASCII)是最早的字符集之一,诞生于20世纪60年代。它用7位二进制数表示128个字符,包括英文字母、数字、标点符号和一些控制字符,基本满足了当时美国英语环境下的计算机信息处理需求。后来扩展为8位,可表示256个字符。
- EBCDIC 字符集:扩展的二 - 十进制交换码(EBCDIC)主要用于IBM的大型机系统。它也是8位字符集,能表示256个字符,但编码方式与ASCII不同,主要用于特定的商业和大型计算机环境。
多字节字符集
- ISO - 8859系列:为了满足不同欧洲语言的需求,国际标准化组织(ISO)在20世纪80年代推出了ISO - 8859系列字符集。它们是单字节编码,每个字符集能表示256个字符,分别针对不同的语言区域,如ISO - 8859 - 1(Latin - 1)用于西欧语言,包含了大部分欧洲语言的字符,但无法处理亚洲等其他地区的语言。
- GB2312:1980年,中国发布了GB2312字符集,它是双字节编码,收录了6763个常用简体汉字和682个非汉字字符,主要用于简体中文环境下的信息处理,基本满足了中文文本的日常处理需求,但不包含繁体字和一些生僻字。
- GBK:1995年,GBK字符集发布,它是对GB2312的扩展,采用双字节编码,收录了21003个汉字和883个图形符号,支持繁体字和更多的生僻字,能更好地满足中文信息处理的各种需求。
- Big5:主要用于繁体中文地区,如中国台湾、香港等地。它是双字节编码,收录了约13000个繁体汉字和一些符号,在繁体中文的计算机系统和文档中广泛使用。
统一字符集
- Unicode:为了解决全球各种语言字符的统一编码问题,20世纪90年代开始开发Unicode字符集。它旨在为世界上所有的字符提供一个唯一的、统一的编码,涵盖了几乎所有的语言文字、符号、表情等。Unicode最初采用16位编码,可表示65536个字符,但随着字符数量的不断增加,后来扩展到了32位甚至更多位。
- UTF - 8:是Unicode的一种可变长度编码方式。它可以用1到4个字节来表示一个字符,对于ASCII字符,UTF - 8编码与ASCII编码相同,使用1个字节表示;对于其他字符,根据字符的Unicode值大小,使用不同数量的字节表示。UTF - 8具有良好的兼容性和扩展性,在网络传输和存储中节省空间,因此成为了目前互联网上最常用的字符集。
随着技术的不断发展,字符集也在持续更新和完善,以适应新出现的字符和符号,如 emoji 表情等,同时也在不断优化编码效率和兼容性,以满足日益复杂的全球信息交流和处理需求。
- 4.
HTML5
离线缓存原理
答案
在现代 Web 开发中,Service Worker 逐渐取代了应用程序缓存,成为更强大、更灵活的离线缓存解决方案
概念
Service Worker 是一种在浏览器后台运行的脚本,独立于网页,为实现离线支持、消息推送、后台同步等功能提供了强大的能力。它本质上是一个可编程的网络代理,能够拦截和处理网络请求,让开发者可以精确控制页面的网络行为。
工作原理
Service Worker 的工作流程主要分为注册、安装、激活和运行几个阶段:
- 注册:在网页中通过 JavaScript 代码注册 Service Worker。通常在页面加载完成后,调用
navigator.serviceWorker.register()
方法指定 Service Worker 脚本的路径进行注册。
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/service-worker.js')
.then(function(registration) {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
})
.catch(function(err) {
console.log('ServiceWorker registration failed: ', err);
});
});
}
- 安装:注册成功后,浏览器会尝试安装 Service Worker。在安装阶段,可以监听
install
事件,利用event.waitUntil()
方法缓存所需的静态资源,如 HTML、CSS、JavaScript 文件和图片等。
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open('my-cache-v1')
.then(function(cache) {
return cache.addAll([
'/',
'/index.html',
'/styles.css',
'/script.js',
'/image.jpg'
]);
})
);
});
- 激活:安装完成后,Service Worker 进入等待状态,直到所有旧的 Service Worker 实例都关闭。当新的 Service Worker 激活时,会触发
activate
事件,通常在此事件中可以清理旧的缓存。
self.addEventListener('activate', function(event) {
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.filter(function(cacheName) {
// 返回需要删除的旧缓存名称
return cacheName.startsWith('my-') && cacheName!== 'my-cache-v1';
}).map(function(cacheName) {
return caches.delete(cacheName);
})
);
})
);
});
- 运行:激活后,Service Worker 开始控制页面,此时可以监听
fetch
事件,拦截页面的网络请求。可以根据请求的 URL 决定是从缓存中获取资源,还是向服务器发起请求。
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
if (response) {
return response;
}
return fetch(event.request);
})
);
});
特点和优势
- 离线支持:可以缓存网页资源,在离线状态下为用户提供基本的页面访问能力,提升用户体验。
- 节省流量:通过缓存常用资源,减少对服务器的请求,降低数据流量消耗。
- 消息推送:结合浏览器的消息推送 API,实现即使页面未打开也能向用户推送通知。
- 后台同步:允许在网络条件良好时,将离线时未完成的操作(如表单提交)同步到服务器。
局限性和注意事项
- https 限制:为了保证安全性,Service Worker 通常需要在 HTTPS 协议下运行,不过在本地开发环境(localhost)中可以例外。
- 浏览器兼容性:虽然现代浏览器大多支持 Service Worker,但一些旧版本浏览器可能不支持,需要进行兼容性检查。
- 调试复杂:由于其在后台运行,调试和排查问题相对困难。
- 5.可以使用
Canvas API
、SVG
等绘制高性能的动画
答案
Canvas API
和 SVG
都是在网页中绘制图形和创建动画的强大工具,它们各有特点,以下分别介绍如何使用它们来绘制高性能的动画。
使用 Canvas API 绘制高性能动画
Canvas
是 HTML5 新增的元素,通过 JavaScript 脚本来动态绘制图形和动画。Canvas
基于像素操作,适合绘制复杂的、频繁更新的动画,例如游戏、数据可视化等。
示例:简单的小球移动动画
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas Animation</title>
<style>
canvas {
border: 1px solid black;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="800" height="600"></canvas>
<script>
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
// 小球的初始位置和速度
let x = 50;
let y = 50;
let dx = 2;
let dy = 2;
const ballRadius = 10;
function drawBall() {
ctx.beginPath();
ctx.arc(x, y, ballRadius, 0, Math.PI * 2);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}
function draw() {
// 清除画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBall();
// 检查边界碰撞
if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
dx = -dx;
}
if (y + dy > canvas.height - ballRadius || y + dy < ballRadius) {
dy = -dy;
}
// 更新小球位置
x += dx;
y += dy;
// 请求下一帧动画
requestAnimationFrame(draw);
}
draw();
</script>
</body>
</html>
使用 SVG 绘制高性能动画
SVG
(可缩放矢量图形)是一种基于 XML 的图形格式,使用标签来描述图形。SVG
基于矢量图形,适合绘制简单的、需要缩放的动画,例如图标动画、广告动画等。
示例:简单的矩形移动动画
性能优化建议
Canvas 性能优化
- 使用
requestAnimationFrame
:避免使用setInterval
或setTimeout
,requestAnimationFrame
会根据浏览器的刷新频率来调用动画函数,减少不必要的重绘,提高性能。 - 批量操作:尽量减少对画布的频繁绘制和清除操作,可以将多个图形的绘制操作合并,减少重绘次数。
- 使用离屏画布:对于复杂的图形绘制,可以先在离屏画布上绘制,然后再将其复制到主画布上,减少主画布的绘制负担。
- 使用
SVG 性能优化
- 减少元素数量:尽量简化 SVG 图形的结构,减少不必要的元素和嵌套,降低浏览器的渲染负担。
- 使用 CSS 动画:对于简单的动画效果,可以使用 CSS 动画来实现,因为 CSS 动画由浏览器的渲染引擎直接处理,性能更高。
- 避免过度使用滤镜和渐变:滤镜和渐变会增加 SVG 的复杂度,影响渲染性能,尽量减少使用。
CSS
- 1.
CSS
盒模型,在不同浏览器的差异
答案
- 2.
CSS
所有选择器及其优先级、使用场景,哪些可以继承,如何运用at
规则
答案
CSS 选择器及其优先级、使用场景
1. 元素选择器
- 语法:直接使用 HTML 元素名称,如
p
、h1
、div
等。 - 优先级:较低,权重为 1。
- 使用场景:用于对页面中所有相同元素设置统一的样式,例如设置所有段落的字体颜色和行高。
p {
color: blue;
line-height: 1.5;
}
- 继承情况:部分属性可继承,如
color
、font-family
、font-size
等。
2. 类选择器
- 语法:以
.
开头,后面跟上类名,如.my-class
。 - 优先级:较高,权重为 10。
- 使用场景:当需要对页面中多个不同元素应用相同样式,或者对某个元素添加特定样式时使用。例如,为所有具有
highlight
类的元素设置背景颜色。
.highlight {
background-color: yellow;
}
- 继承情况:取决于具体属性,可继承属性会继承。
3. ID 选择器
- 语法:以
#
开头,后面跟上 ID 名,如#my-id
。 - 优先级:非常高,权重为 100。
- 使用场景:用于为页面中唯一的元素设置样式,例如为页面的导航栏设置特定样式。
#navbar {
background-color: #333;
color: white;
}
- 继承情况:取决于具体属性,可继承属性会继承。
4. 属性选择器
- 语法:根据元素的属性来选择元素,如
[attribute]
、[attribute=value]
等。 - 优先级:与类选择器相同,权重为 10。
- 使用场景:当需要根据元素的属性值来选择元素并设置样式时使用。例如,为所有
disabled
属性的输入框设置灰色背景。
input[disabled] {
background-color: #ccc;
}
- 继承情况:取决于具体属性,可继承属性会继承。
5. 伪类选择器
- 语法:以
:
开头,如:hover
、:active
、:first-child
等。 - 优先级:与类选择器相同,权重为 10。
- 使用场景:用于根据元素的特定状态或位置来设置样式。例如,为鼠标悬停在链接上时设置不同的颜色。
a:hover {
color: red;
}
- 继承情况:通常不继承。
6. 伪元素选择器
- 语法:以
::
开头,如::before
、::after
、::first-letter
等。 - 优先级:与类选择器相同,权重为 10。
- 使用场景:用于在元素的特定位置插入虚拟的内容或设置样式。例如,为段落的第一个字母设置较大的字体。
p::first-letter {
font-size: 2em;
}
- 继承情况:通常不继承。
7. 组合选择器
- 后代选择器:用空格分隔,如
div p
表示选择div
元素内的所有p
元素。 - 子选择器:用
>
分隔,如div > p
表示选择div
元素的直接子元素p
。 - 相邻兄弟选择器:用
+
分隔,如h1 + p
表示选择紧跟在h1
元素后面的第一个p
元素。 - 通用兄弟选择器:用
~
分隔,如h1 ~ p
表示选择h1
元素后面的所有p
元素。 - 优先级:根据组合的选择器的权重相加计算。
- 使用场景:用于更精确地选择元素。例如,为
nav
元素内的所有a
元素设置样式。
nav a {
text-decoration: none;
}
- 继承情况:取决于具体属性,可继承属性会继承。
CSS 选择器优先级规则
选择器的优先级由权重决定,权重越高,优先级越高。当多个选择器作用于同一个元素且样式冲突时,优先级高的选择器的样式会覆盖优先级低的选择器的样式。优先级从高到低依次为:
- 内联样式(权重为 1000)。
- ID 选择器(权重为 100)。
- 类选择器、属性选择器、伪类选择器(权重为 10)。
- 元素选择器、伪元素选择器(权重为 1)。
- 通配符选择器
*
(权重为 0)。
CSS 可继承属性
可继承的属性通常是与文本相关的属性,如:
color
:文本颜色。font-family
:字体家族。font-size
:字体大小。font-style
:字体样式(如斜体)。font-weight
:字体粗细。line-height
:行高。text-align
:文本对齐方式。text-indent
:文本缩进。
CSS @规则的运用
1. @import
- 作用:用于在 CSS 文件中导入其他 CSS 文件。
- 语法:
@import url('styles.css');
- 使用场景:当项目中有多个 CSS 文件,需要将它们合并使用时,可以使用
@import
导入。
2. @media
- 作用:用于根据不同的媒体查询条件应用不同的样式,实现响应式设计。
- 语法:
@media (max-width: 768px) {
body {
font-size: 14px;
}
}
- 使用场景:当需要根据设备的屏幕宽度、高度、分辨率等条件来调整页面样式时使用。
3. @keyframes
- 作用:用于定义 CSS 动画的关键帧。
- 语法:
@keyframes slide {
0% {
transform: translateX(0);
}
100% {
transform: translateX(100px);
}
}
.element {
animation: slide 2s infinite;
}
- 使用场景:当需要创建复杂的动画效果时使用。
4. @font-face
- 作用:用于引入自定义字体。
- 语法:
@font-face {
font-family: 'MyFont';
src: url('myfont.woff2') format('woff2'),
url('myfont.woff') format('woff');
font-weight: normal;
font-style: normal;
}
body {
font-family: 'MyFont', sans-serif;
}
- 使用场景:当需要使用非系统默认字体时使用。
5. @supports
- 作用:用于检测浏览器是否支持某个 CSS 属性或值。
- 语法:
@supports (display: flex) {
.container {
display: flex;
}
}
- 使用场景:当需要根据浏览器的特性支持情况来应用不同的样式时使用。
- 3.
CSS
伪类和伪元素有哪些,它们的区别和实际应用
答案
CSS 伪类和伪元素的介绍
常见的 CSS 伪类
- 链接相关伪类
:link
:用于选取未访问的链接。例如:
a:link {
color: blue;
}
- `:visited`:用于选取已访问的链接。例如:
a:visited {
color: purple;
}
- 用户行为伪类
:hover
:当鼠标悬停在元素上时应用样式。常用于按钮、链接等可交互元素。例如:
button:hover {
background-color: lightgray;
}
- `:active`:当元素被激活(如鼠标点击时)应用样式。例如:
a:active {
color: red;
}
- `:focus`:当元素获得焦点时应用样式,常用于表单元素。例如:
input:focus {
border: 2px solid blue;
}
- 结构伪类
:first-child
:选取父元素的第一个子元素。例如:
ul li:first-child {
font-weight: bold;
}
- `:last-child`:选取父元素的最后一个子元素。例如:
ul li:last-child {
color: gray;
}
- `:nth-child(n)`:选取父元素的第 `n` 个子元素,`n` 可以是数字、表达式或关键字。例如,选取偶数项:
ul li:nth-child(even) {
background-color: lightblue;
}
- 状态伪类
:disabled
:选取禁用的表单元素。例如:
input:disabled {
background-color: #ccc;
}
- `:checked`:选取被选中的表单元素(如复选框、单选框)。例如:
input:checked + label {
color: green;
}
常见的 CSS 伪元素
::before
:在元素内容的前面插入虚拟内容。例如,为每个段落的开头添加引号:
p::before {
content: "“";
}
::after
:在元素内容的后面插入虚拟内容。例如,为每个链接的后面添加外部链接图标:
a::after {
content: " 🔗";
}
::first-letter
:选取元素的第一个字母。常用于首字母大写等效果。例如:
p::first-letter {
font-size: 2em;
}
::first-line
:选取元素的第一行。例如:
p::first-line {
font-weight: bold;
}
::selection
:选取用户选中的文本。例如,改变选中文本的背景颜色和文字颜色:
::selection {
background-color: yellow;
color: red;
}
伪类和伪元素的区别
- 语法不同:伪类使用单冒号
:
,伪元素在 CSS3 中推荐使用双冒号::
,不过单冒号在很多情况下也能兼容。 - 本质不同:伪类用于根据元素的特定状态或位置来选择元素,它并没有创建新的元素;而伪元素用于创建虚拟的元素,这些元素在 HTML 代码中并不存在,但可以在页面中显示出来。
- 优先级不同:伪类和类选择器的优先级相同,权重为 10;伪元素和元素选择器的优先级相同,权重为 1。
实际应用
- 伪类的实际应用
- 导航栏交互效果:使用
:hover
伪类为导航栏的链接添加悬停效果,增强用户体验。 - 表单验证提示:使用
:valid
和:invalid
伪类根据表单元素的输入有效性显示不同的样式,给用户提供反馈。 - 表格隔行变色:使用
:nth-child
伪类为表格的偶数行或奇数行设置不同的背景颜色,提高表格的可读性。
- 导航栏交互效果:使用
- 伪元素的实际应用
- 清除浮动:使用
::after
伪元素结合clear: both
来清除浮动,避免父元素高度塌陷。
- 清除浮动:使用
.clearfix::after {
content: "";
display: block;
clear: both;
}
- **图标添加**:使用 `::before` 或 `::after` 伪元素为元素添加图标,而无需在 HTML 中额外添加标签。
- **首字母样式**:使用 `::first-letter` 伪元素为文章的首字母设置特殊样式,增加页面的美观度。
- 4.
HTML
文档流的排版规则,CSS
几种定位的规则、定位参照物、对文档流的影响,如何选择最好的定位方式,雪碧图实现原理
答案
- 5.水平垂直居中的方案、可以实现
6
种以上并对比它们的优缺点
答案
- 6.
BFC
实现原理,可以解决的问题,如何创建BFC
答案
- 7.可使用
CSS
函数复用代码,实现特殊效果
答案
- 8.
PostCSS
、Sass
、Less
的异同,以及使用配置,至少掌握一种
答案
- 9.
CSS
模块化方案、如何配置按需加载、如何防止CSS
阻塞渲染
答案
- 10.熟练使用
CSS
实现常见动画,如渐变、移动、旋转、缩放等等
答案
- 11.
CSS
浏览器兼容性写法,了解不同API
在不同浏览器下的兼容性情况
答案
- 12.掌握一套完整的响应式布局方案
答案
手写
1.手写图片瀑布流效果
2.使用
CSS
绘制几何图形(圆形、三角形、扇形、菱形等)3.使用纯
CSS
实现曲线运动(贝塞尔曲线)4.实现常用布局(三栏、圣杯、双飞翼、吸顶),可是说出多种方式并理解其优缺点
三、计算机基础
关于编译原理,不需要理解非常深入,但是最基本的原理和概念一定要懂,这对于学习一门编程语言非常重要
编译原理
1.理解代码到底是什么,计算机如何将代码转换为可以运行的目标程序
2.正则表达式的匹配原理和性能优化
3.如何将
JavaScript
代码解析成抽象语法树(AST
)4.
base64
的编码原理5.几种进制的相互转换计算方法,在
JavaScript
中如何表示和转换
网络协议
1.理解什么是协议,了解
TCP/IP
网络协议族的构成,每层协议在应用程序中发挥的作用2.三次握手和四次挥手详细原理,为什么要使用这种机制
3.有哪些协议是可靠,
TCP
有哪些手段保证可靠交付4.
DNS
的作用、DNS
解析的详细过程,DNS
优化原理5.
CDN
的作用和原理6.
HTTP
请求报文和响应报文的具体组成,能理解常见请求头的含义,有几种请求方式,区别是什么7.
HTTP
所有状态码的具体含义,看到异常状态码能快速定位问题8.
HTTP1.1
、HTTP2.0
带来的改变9.
HTTPS
的加密原理,如何开启HTTPS
,如何劫持HTTPS
请求10.理解
WebSocket
协议的底层原理、与HTTP
的区别
设计模式
1.熟练使用前端常用的设计模式编写代码,如单例模式、装饰器模式、代理模式等
2.发布订阅模式和观察者模式的异同以及实际应用
3.可以说出几种设计模式在开发中的实际应用,理解框架源码中对设计模式的应用
四、数据结构和算法
据我了解的大部分前端对这部分知识有些欠缺,甚至抵触,但是,如果突破更高的天花板,这部分知识是必不可少的,而且我亲身经历——非常有用!
JavaScript编码能力
1.多种方式实现数组去重、扁平化、对比优缺点
2.多种方式实现深拷贝、对比优缺点
3.手写函数柯里化工具函数、并理解其应用场景和优势
4.手写防抖和节流工具函数、并理解其内部原理和应用场景
5.实现一个
sleep
函数
手动实现前端轮子
1.手动实现
call、apply、bind
2.手动实现符合
Promise/A+
规范的Promise
、手动实现async await
3.手写一个
EventEmitter
实现事件发布、订阅4.可以说出两种实现双向绑定的方案、可以手动实现
5.手写
JSON.stringify
、JSON.parse
6.手写一个模版引擎,并能解释其中原理
7.手写
懒加载
、下拉刷新
、上拉加载
、预加载
等效果
数据结构
1.理解常见数据结构的特点,以及他们在不同场景下使用的优缺点
2.理解
数组
、字符串
的存储原理,并熟练应用他们解决问题3.理解
二叉树
、栈
、队列
、哈希表
的基本结构和特点,并可以应用它解决问题4.了解
图
、堆
的基本结构和使用场景
算法
1.可计算一个算法的时间复杂度和空间复杂度,可估计业务逻辑代码的耗时和内存消耗
2.至少理解五种排序算法的实现原理、应用场景、优缺点,可快速说出时间、空间复杂度
3.了解递归和循环的优缺点、应用场景、并可在开发中熟练应用
4.可应用
回溯算法
、贪心算法
、分治算法
、动态规划
等解决复杂问题5.前端处理海量数据的算法方案
五、运行环境
我们需要理清语言和环境的关系:
ECMAScript
描述了JavaScript
语言的语法和基本对象规范
浏览器作为
JavaScript
的一种运行环境,为它提供了:文档对象模型(DOM
),描述处理网页内容的方法和接口、浏览器对象模型(BOM
),描述与浏览器进行交互的方法和接口
Node也是
JavaScript
的一种运行环境,为它提供了操作I/O
、网络等API
浏览器API
1.浏览器提供的符合
W3C
标准的DOM
操作API
、浏览器差异、兼容性2.浏览器提供的浏览器对象模型 (
BOM
)提供的所有全局API
、浏览器差异、兼容性3.大量
DOM
操作、海量数据的性能优化(合并操作、Diff
、requestAnimationFrame
等)4.浏览器海量数据存储、操作性能优化
5.
DOM
事件流的具体实现机制、不同浏览器的差异、事件代理6.前端发起网络请求的几种方式及其底层实现、可以手写原生
ajax
、fetch
、可以熟练使用第三方库7.浏览器的同源策略,如何避免同源策略,几种方式的异同点以及如何选型
8.浏览器提供的几种存储机制、优缺点、开发中正确的选择
9.浏览器跨标签通信
浏览器原理
1.各浏览器使用的
JavaScript
引擎以及它们的异同点、如何在代码中进行区分2.请求数据到请求结束与服务器进行了几次交互
3.可详细描述浏览器从输入
URL
到页面展现的详细过程4.浏览器解析
HTML
代码的原理,以及构建DOM
树的流程5.浏览器如何解析
CSS
规则,并将其应用到DOM
树上6.浏览器如何将解析好的带有样式的
DOM
树进行绘制7.浏览器的运行机制,如何配置资源异步同步加载
8.浏览器回流与重绘的底层原理,引发原因,如何有效避免
9.浏览器的垃圾回收机制,如何避免内存泄漏
10.浏览器采用的缓存方案,如何选择和控制合适的缓存方案
Node
1.理解
Node
在应用程序中的作用,可以使用Node
搭建前端运行环境、使用Node
操作文件、操作数据库等等2.掌握一种
Node
开发框架,如Express
,Express
和Koa
的区别3.熟练使用
Node
提供的API
如Path
、Http
、Child Process
等并理解其实现原理4.
Node
的底层运行原理、和浏览器的异同5.
Node
事件驱动、非阻塞机制的实现原理
六、框架和类库
轮子层出不穷,从原理上理解才是正道
TypeScript
1.理解
泛型
、接口
等面向对象的相关概念,TypeScript
对面向对象理念的实现2.理解使用
TypeScript
的好处,掌握TypeScript
基础语法3.
TypeScript
的规则检测原理4.可以在
React
、Vue
等框架中使用TypeScript
进行开发
React
1.
React
和vue
选型和优缺点、核心架构的区别2.
React
中setState
的执行机制,如何有效的管理状态3.
React
的事件底层实现机制4.
React
的虚拟DOM
和Diff
算法的内部实现5.
React
的Fiber
工作原理,解决了什么问题6.
React Router
和Vue Router
的底层实现原理、动态加载实现原理7.可熟练应用
React API
、生命周期等,可应用HOC
、render props
、Hooks
等高阶用法解决问题8.基于
React
的特性和原理,可以手动实现一个简单的React
Vue
1.熟练使用
Vue
的API
、生命周期、钩子函数2.
MVVM
框架设计理念3.
Vue
双向绑定实现原理、Diff
算法的内部实现4.
Vue
的事件机制5.从
template
转换成真实DOM
的实现机制
多端开发
1.单页面应用(
SPA
)的原理和优缺点,掌握一种快速开发SPA
的方案2.理解
Viewport
、em
、rem
的原理和用法,分辨率、px
、ppi
、dpi
、dp
的区别和实际应用3.移动端页面适配解决方案、不同机型适配方案
4.掌握一种
JavaScript
移动客户端开发技术,如React Native
:可以搭建React Native
开发环境,熟练进行开发,可理解React Native
的运作原理,不同端适配5.掌握一种
JavaScript
PC
客户端开发技术,如Electron
:可搭建Electron
开发环境,熟练进行开发,可理解Electron
的运作原理6.掌握一种小程序开发框架或原生小程序开发
7.理解多端框架的内部实现原理,至少了解一个多端框架的使用
数据流管理
1.掌握
React
和Vue
传统的跨组件通信方案,对比采用数据流管理框架的异同2.熟练使用
Redux
管理数据流,并理解其实现原理,中间件实现原理3.熟练使用
Mobx
管理数据流,并理解其实现原理,相比Redux
有什么优势4.熟练使用
Vuex
管理数据流,并理解其实现原理5.以上数据流方案的异同和优缺点,不情况下的技术选型
实用库
1.至少掌握一种
UI
组件框架,如antd design
,理解其设计理念、底层实现2.掌握一种图表绘制框架,如
Echart
,理解其设计理念、底层实现,可以自己实现图表3.掌握一种
GIS
开发框架,如百度地图API
4.掌握一种可视化开发框架,如
Three.js
、D3
5.工具函数库,如
lodash
、underscore
、moment
等,理解使用的工具类或工具函数的具体实现原理
开发和调试
1.熟练使用各浏览器提供的调试工具
2.熟练使用一种代理工具实现请求代理、抓包,如
charls
3.可以使用
Android
、IOS
模拟器进行调试,并掌握一种真机调试方案4.了解
Vue
、React
等框架调试工具的使用
七、前端工程
前端工程化:以工程化方法和工具提高开发生产效率、降低维护难度
项目构建
1.理解
npm
、yarn
依赖包管理的原理,两者的区别2.可以使用
npm
运行自定义脚本3.理解
Babel
、ESLint
、webpack
等工具在项目中承担的作用4.
ESLint
规则检测原理,常用的ESLint
配置5.
Babel
的核心原理,可以自己编写一个Babel
插件6.可以配置一种前端代码兼容方案,如
Polyfill
7.
Webpack
的编译原理、构建流程、热更新原理,chunk
、bundle
和module
的区别和应用8.可熟练配置已有的
loaders
和plugins
解决问题,可以自己编写loaders
和plugins
nginx
1.正向代理与反向代理的特点和实例
2.可手动搭建一个简单的
nginx
服务器、3.熟练应用常用的
nginx
内置变量,掌握常用的匹配规则写法4.可以用
nginx
实现请求过滤、配置gzip
、负载均衡等,并能解释其内部原理
开发提速
1.熟练掌握一种接口管理、接口
mock
工具的使用,如yapi
2.掌握一种高效的日志埋点方案,可快速使用日志查询工具定位线上问题
3.理解
TDD
与BDD
模式,至少会使用一种前端单元测试框架
版本控制
1.理解
Git
的核心原理、工作流程、和SVN
的区别2.熟练使用常规的
Git
命令、git rebase
、git stash
等进阶命令3.可以快速解决
线上分支回滚
、线上分支错误合并
等复杂问题
持续集成
1.理解
CI/CD
技术的意义,至少熟练掌握一种CI/CD
工具的使用,如Jenkins
2.可以独自完成架构设计、技术选型、环境搭建、全流程开发、部署上线等一套完整的开发流程(包括
Web
应用、移动客户端应用、PC
客户端应用、小程序、H5
等等)
八、项目和业务
后端技能
1.了解后端的开发方式,在应用程序中的作用,至少会使用一种后端语言
2.掌握数据最终在数据库中是如何落地存储的,能看懂表结构设计、表之间的关联,至少会使用一种数据库
性能优化
1.了解前端性能衡量指标、性能监控要点,掌握一种前端性能监控方案
2.了解常见的
Web
、App
性能优化方案3.
SEO
排名规则、SEO
优化方案、前后端分离的SEO
4.
SSR
实现方案、优缺点、及其性能优化5.
Webpack
的性能优化方案6.
Canvas
性能优化方案7.
React
、Vue
等框架使用性能优化方案
前端安全
1.
XSS
攻击的原理、分类、具体案例,前端如何防御2.
CSRF
攻击的原理、具体案例,前端如何防御3.
HTTP
劫持、页面劫持的原理、防御措施
业务相关
1.能理解所开发项目的整体业务形态、业务目标、业务架构,可以快速定位线上业务问题
2.能理解所开发项目整体的技术架构、能快读的根据新需求进行开发规划、能快速根据业务报警、线上日志等定位并解决线上技术问题
3.可以将自己的想法或新技术在业务中落地实践,尽量在团队中拥有一定的不可替代性
九、学习提升
vczh
大神在知乎问题【如何能以后达到温赵轮三位大神的水平?】下的回答:
这十几年我一共做了三件事:
- 1、不以赚钱为目的选择学习的内容;
- 2、以自己是否能造出轮子来衡量学习的效果;
- 3、坚持每天写自己的代码,前10年每天至少6个小时,不包含学习和工作的时间。
上面几点可能有点难,第一点我就做不到,但是做到下面绩点还是比较容易的。
关于写博客说明下,能给别人讲明白的知识会比自己学习掌握的要深刻许多
1.拥有自己的技术博客,或者在一些博客平台上拥有自己的专栏
2.定期的将知识进行总结,不断完善自己的知识体系
3.尽量将自己的知识转换成真实的产出,不要仅仅停留在书面理解层面,更重要的是实际应用
4.坚持输出
自己
的代码,不要盲目的扎进公司业务
十、技术之外
这部分可能比上面九条加起来重要!
1.了解互联网人员术语:
CEO
、CTO
、COO
、CFO
、PM
、QA
、UI
、FE
、DEV
、DBA
、OPS
等2.了解互联网行业术语:
B2B
、B2C
、C2C
、O2O
等3.掌握互联网行业沟通、问答、学习的
4.有一定的
"PPT"
能力5.有一定的理财意识,至少了解储蓄、货币基金、保险、指数基金、股票等基本的理财知识
6.掌握在繁重的工作和长期的电脑辐射的情况下保持健康的方法,建立正确的养生知识体系
小结
希望你阅读本篇文章后可以达到以下几点:
从知识清单中找到自己的知识盲点与欠缺
具有知识体系化的思想,开始建立自己的知识体系
阅读文章时将知识归类到知识体系中,并不断完善自己的知识体系
从文章中获取到了有用的资源
文中如有错误,欢迎在评论区指正,如果这篇文章帮助到了你,欢迎点赞和关注。
如果你有什么好的知识、资源推荐,欢迎在评论区留言。