Skip to content

一、JavaScript基础

前端工程师吃饭的家伙,深度、广度一样都不能差。

变量和类型

  • 1.JavaScript规定了几种语言类型
答案

null undefined number boolean string object symbol(es6) bigInt(es10)

  • 2.JavaScript对象的底层数据结构是什么
答案

map 键值对

  • 3.Symbol类型在实际开发中的应用、可手动实现一个简单的Symbol
答案

javascript
var sym = Symbol('foo');
typeof sym; // 'symbol'
Symbol('foo') === Symbol('foo'); // false

var obj = {[sym]: 1};
obj[sym]; // 1

参考:ES6 系列之模拟实现 Symbol 类型

  • 4.JavaScript中的变量在内存中的具体存储形式
答案

栈(内存空间大小固定,变量值不可变)

基本数据类型的值直接存于栈内存,而引用数据类型在栈内存中存储引用地址,实际数据存于堆内存。

  • 5.基本类型对应的内置对象,以及他们之间的装箱拆箱操作
答案

String Number Boolean

js
var s = 'Hello World';
console.log(s + 'Aaron'); // 先装箱,再拆箱
  • 6.理解值类型和引用类型
答案

都是值,一个是字面量值,一个是内存地址

  • 7.nullundefined的区别
答案
  • null 代表对象为空,undefined 代表变量未赋值
  • typeof null === 'object', typeof undefined === 'undefined'
  • 8.至少可以说出三种判断JavaScript数据类型的方式,以及他们的优缺点,如何准确的判断数组类型
答案
  • typeof 判断基本类型和函数对象很方便,但无法区分 null 和 object(包括数组)。
  • instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上,只能检测对象的类型。
  • Object.prototype.toString 默认返回当前对象的 [[Class]]
js
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]

参考:在JavaScript中,如何判断数组是数组?

  • 9.可能发生隐式类型转换的场景以及转换原则,应如何避免或巧妙应用
答案

隐式类型转换的场景:

  • if 语句和逻辑语句
  • 各种数学运算
  • ==
  • 10.出现小数精度丢失的原因,JavaScript可以存储的最大数字、最大安全数字,JavaScript处理大数字的方法、避免精度丢失的方法
答案

小数精度丢失的原因

在 JavaScript 中,数字采用 IEEE 754 双精度 64 位浮点数格式来表示。这种格式由 1 位符号位、11 位指数位和 52 位尾数位构成。

在进行小数运算时,部分十进制小数无法精准地用二进制来表示。例如,十进制的 0.1 转换为二进制是一个无限循环小数 0.0001100110011...。由于 52 位尾数的存储限制,只能截取部分二进制位,从而造成精度丢失。

以下是一个简单的示例:

javascript
console.log(0.1 + 0.2);

上述代码输出的结果并非 0.3,而是一个近似值 0.30000000000000004

JavaScript 可以存储的最大数字、最大安全数字

  • 最大数字:JavaScript 里能够存储的最大数字是 Number.MAX_VALUE,其值约为 1.7976931348623157e+308。当数字超出这个范围时,会被表示为 Infinity
javascript
console.log(Number.MAX_VALUE);
  • 最大安全数字:JavaScript 中最大的安全整数是 Number.MAX_SAFE_INTEGER,其值为 9007199254740991。安全整数意味着在这个范围内的整数可以精确地表示和进行比较。
javascript
console.log(Number.MAX_SAFE_INTEGER);

JavaScript 处理大数字的方法

  • BigInt:ES2020 引入了 BigInt 类型,用于表示任意大的整数。在数字后面加上 n 或者使用 BigInt() 函数来创建 BigInt 类型的值。
javascript
const bigNumber = 9007199254740991n + 1n;
console.log(bigNumber);
  • 第三方库:像 bignumber.jsdecimal.js 这类第三方库,能处理任意精度的数字运算。以 bignumber.js 为例:
javascript
const BigNumber = require('bignumber.js');

const num1 = new BigNumber('9007199254740991');
const num2 = new BigNumber('1');
const result = num1.plus(num2);
console.log(result.toString());

避免精度丢失的方法

  • 使用整数进行计算:把小数转换为整数进行运算,最后再转换回小数。
javascript
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
答案

手写instanceof

  • 3.实现继承的几种方式以及他们的优缺点
答案

es5 原型继承 es6 extend继承

手写instanceof

  • 4.至少说出一种开源项目(如Node)中应用原型继承的案例
答案

在 Node.js 的 http 模块中就应用了原型继承,下面详细介绍其具体情况。

案例背景

在 Node.js 里,http 模块是用于构建 HTTP 服务器和客户端的核心模块。当我们创建一个 HTTP 服务器时,会用到 http.createServer() 方法,该方法返回的服务器对象继承了一些基础功能,这其中就运用了原型继承。

代码示例及原理分析

javascript
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.Servernet.Server 是 Node.js 中用于创建 TCP 服务器的基础类,http.Server 通过原型继承了 net.Server 的属性和方法。具体来说,http.Server 的原型链指向 net.Server 的实例,这样 http.Server 就拥有了 net.Server 的功能,同时还可以添加自己特有的 HTTP 相关功能。

代码层面解释

在 Node.js 的源码中,可以看到类似如下的继承关系构建(简化示意):

javascript
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
};

在上述代码中:

  1. HttpServer 构造函数通过 net.Server.call(this, { allowHalfOpen: true }); 调用父类构造函数,继承父类的实例属性。
  2. HttpServer.prototype = Object.create(net.Server.prototype);HttpServer 的原型指向 net.Server 原型的一个副本,实现了原型继承,使得 HttpServer 实例可以访问 net.Server 原型上的方法。
  3. HttpServer.prototype.constructor = HttpServer; 修正了构造函数的指向,确保 constructor 属性正确指向 HttpServer

优点

  • 代码复用:通过原型继承,http.Server 复用了 net.Server 的代码,避免了重复编写基础的服务器功能代码,提高了开发效率。
  • 扩展性http.Server 可以在继承的基础上添加自己特有的 HTTP 相关功能,如处理 HTTP 请求和响应等,使得代码具有良好的扩展性。
  • 5.可以描述new一个对象的详细过程,手动实现一个new操作符
答案

手写new

  • 6.理解es6 class构造以及继承的底层实现原理
答案

ES6 引入的 class 关键字为 JavaScript 提供了更简洁、更符合传统面向对象编程风格的类和继承语法,但本质上它还是基于原型链的语法糖。下面详细解释 class 构造以及继承的底层实现原理。

ES6 class 构造的底层原理

ES6 的 class 定义一个类,其实在底层是基于函数和原型实现的。下面通过一个示例来解释:

javascript
// 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 关键字,底层同样基于原型链和构造函数的组合。以下是一个示例:

javascript
// 父类
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 仍然是基于原型的语言,classextends 只是对原型链和构造函数的封装。

作用域和闭包

  • 1.理解词法作用域和动态作用域
答案

词法作用域:又称静态作用域,是在函数声明时即确定的。this使用的是词法作用域 动态作用域:则与调用者有关

词法作用域和动态作用域是编程语言中两种不同的作用域规则,它们决定了变量和函数的可访问性。下面为你详细介绍这两种作用域。

词法作用域

定义

词法作用域(Lexical Scope),也被称为静态作用域,是指变量和函数的作用域是由它们在代码中定义的位置决定的,与函数的调用位置无关。也就是说,在编写代码时,变量和函数的作用域就已经确定了。

示例

javascript
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 伪代码说明)

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 引入)。

全局作用域

全局作用域是最外层的作用域,在全局作用域中声明的变量和函数可以在代码的任何地方访问。

javascript
// 全局作用域
var globalVariable = 'I am a global variable';

function globalFunction() {
    console.log('I am a global function');
}

// 在任何地方都可以访问全局变量和函数
console.log(globalVariable); 
globalFunction();

函数作用域

函数作用域是指在函数内部定义的变量和函数只能在该函数内部访问,外部无法直接访问。

javascript
function myFunction() {
    // 函数作用域
    var functionVariable = 'I am a function variable';

    function innerFunction() {
        console.log(functionVariable); 
    }

    innerFunction();
}

myFunction();
// 下面这行代码会报错,因为 functionVariable 只能在 myFunction 内部访问
// console.log(functionVariable);

块级作用域

ES6 引入了 letconst 关键字,它们可以创建块级作用域。块级作用域是指由 {} 包裹的代码块,如 if 语句、for 循环等。在块级作用域中声明的变量只能在该块内部访问。

javascript
if (true) {
    // 块级作用域
    let blockVariable = 'I am a block variable';
    console.log(blockVariable); 
}

// 下面这行代码会报错,因为 blockVariable 只能在 if 块内部访问
// console.log(blockVariable);

作用域链

作用域链是由多个作用域组成的链表,它用于查找变量和函数的定义。当在某个作用域中访问一个变量或函数时,JavaScript 引擎会首先在当前作用域中查找,如果找不到,就会沿着作用域链向上查找,直到找到该变量或函数的定义或者到达全局作用域。

作用域链的形成

每个函数都有自己的作用域,当一个函数嵌套在另一个函数内部时,就会形成作用域链。内部函数可以访问外部函数的变量和函数,这是因为内部函数的作用域链包含了外部函数的作用域。

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. 执行上下文栈的工作流程

以下是一个简单的示例代码,用于说明执行上下文栈的工作流程:

javascript
function foo() {
    function bar() {
        console.log('Inside bar');
    }
    bar();
    console.log('Inside foo');
}

foo();
console.log('Global scope');

具体步骤如下:

  1. 全局执行上下文入栈:当 JavaScript 代码开始执行时,首先会创建一个全局执行上下文并将其压入执行上下文栈的底部。全局执行上下文包含了全局变量和函数的定义。
  2. foo 函数执行上下文入栈:当调用 foo 函数时,会创建一个新的函数执行上下文并将其压入栈顶。该执行上下文包含了 foo 函数的局部变量和函数定义。
  3. bar 函数执行上下文入栈:在 foo 函数内部调用 bar 函数时,会创建一个新的函数执行上下文并将其压入栈顶。该执行上下文包含了 bar 函数的局部变量和函数定义。
  4. bar 函数执行上下文出栈bar 函数执行完毕后,其执行上下文会从栈顶弹出。
  5. 继续执行 foo 函数bar 函数执行上下文出栈后,继续执行 foo 函数中剩余的代码。
  6. foo 函数执行上下文出栈foo 函数执行完毕后,其执行上下文会从栈顶弹出。
  7. 继续执行全局代码foo 函数执行上下文出栈后,继续执行全局代码中剩余的代码。
  8. 全局执行上下文出栈:当所有代码执行完毕后,全局执行上下文会从栈中弹出,执行上下文栈为空。

4. 利用堆栈信息快速定位问题

当 JavaScript 代码出现错误时,浏览器的开发者工具会提供详细的堆栈信息,通过分析这些信息可以快速定位问题所在。以下是一些常见的错误场景和利用堆栈信息定位问题的方法:

栈溢出错误(Stack Overflow Error)

栈溢出错误通常是由于递归函数没有正确的终止条件,导致执行上下文栈不断增长,最终超出了栈的最大容量。例如:

javascript
function recursiveFunction() {
    recursiveFunction();
}

recursiveFunction();

当出现栈溢出错误时,浏览器的开发者工具会显示堆栈信息,指出错误发生的位置和调用栈的情况。通过查看堆栈信息,可以发现递归函数不断调用自身,没有终止条件,从而定位到问题所在。

函数调用错误

当函数调用出现错误时,堆栈信息可以帮助我们确定错误发生的具体位置和调用路径。例如:

javascript
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

javascript
console.log(this === window); // 在浏览器环境中输出 true

2. 函数作为普通函数调用

当函数作为普通函数调用时,this 指向全局对象(在严格模式下,thisundefined)。

javascript
function foo() {
    console.log(this);
}
foo(); // 在浏览器环境中输出 window 对象

在严格模式下:

javascript
'use strict';
function bar() {
    console.log(this);
}
bar(); // 输出 undefined

3. 函数作为对象的方法调用

当函数作为对象的方法调用时,this 指向调用该方法的对象。

javascript
const obj = {
    name: 'John',
    sayName() {
        console.log(this.name);
    }
};
obj.sayName(); // 输出 'John'

4. 构造函数调用

当函数使用 new 关键字作为构造函数调用时,this 指向新创建的对象。

javascript
function Person(name) {
    this.name = name;
    console.log(this);
}
const person = new Person('Alice'); // 输出 Person { name: 'Alice' }

5. callapplybind 方法调用

callapplybind 是函数对象的方法,它们可以用来显式地指定 this 的值。

  • call 方法:第一个参数是要绑定的 this 值,后续参数是传递给函数的参数。
javascript
function greet(message) {
    console.log(`${message}, ${this.name}`);
}
const person1 = { name: 'Bob' };
greet.call(person1, 'Hello'); // 输出 'Hello, Bob'
  • apply 方法:第一个参数是要绑定的 this 值,第二个参数是一个数组,数组中的元素是传递给函数的参数。
javascript
function greet(message) {
    console.log(`${message}, ${this.name}`);
}
const person2 = { name: 'Charlie' };
greet.apply(person2, ['Hi']); // 输出 'Hi, Charlie'
  • bind 方法:创建一个新的函数,在调用时 this 值会被绑定到指定的对象上。
javascript
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 值。

javascript
const obj = {
    name: 'Eve',
    sayName: function() {
        const arrowFunc = () => {
            console.log(this.name);
        };
        arrowFunc();
    }
};
obj.sayName(); // 输出 'Eve'

总结

this 的值在 JavaScript 中是动态的,取决于函数的调用方式。理解不同使用场景下 this 的取值规则,对于编写高质量的 JavaScript 代码和避免错误非常重要。

参考:更加复杂的场景下this的指向

  • 5.闭包的实现原理和作用,可以列举几个开发中闭包的实际应用
答案

闭包的实现原理

在 JavaScript 里,函数会形成闭包。闭包本质是指有权访问另一个函数作用域里变量的函数。其实现原理和 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 变量。

闭包的作用

  1. 读取函数内部的变量:闭包能够让外部代码访问函数内部的变量,突破了函数作用域的限制。
  2. 让这些变量的值始终保持在内存中:闭包会阻止外部函数的变量被垃圾回收机制回收,使得这些变量的值可以一直保存下来,实现数据的持久化。

开发中闭包的实际应用

  1. 实现私有变量和方法 闭包可以用来创建私有变量和方法,外部代码无法直接访问这些私有成员,只能通过闭包提供的公共接口来操作。
javascript
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 变量是私有的,外部代码无法直接访问,只能通过 incrementdecrementgetCount 方法来操作。

  1. 函数柯里化 函数柯里化是指将一个多参数函数转换为一系列单参数函数的技术。闭包可以用来实现函数柯里化。
javascript
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 函数可以接收两个参数,也可以只接收一个参数并返回一个新的函数,新函数可以接收另一个参数并完成加法运算。

  1. 事件处理中的数据绑定 在事件处理函数中,闭包可以用来保存事件处理所需的数据。
html
<!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中使用 setTimeoutsetInterval 时,如果在回调函数中引用了外部的对象,而在不再需要这个定时器时没有正确地清除它,那么被引用的对象就无法被垃圾回收,从而导致内存泄漏。
  • 防止方法
    • 及时释放动态分配的内存:在使用完动态分配的内存后,一定要调用相应的释放函数来释放内存。例如,在C语言中使用 malloc 分配内存后,要使用 free 函数释放;在Java中,虽然有自动的垃圾回收机制,但对于一些占用大量资源的对象,在使用完毕后可以将其引用置为 null,以便让垃圾回收器及时回收。
    • 避免不必要的全局变量和静态变量:全局变量和静态变量的生命周期较长,如果在程序中大量使用,可能会导致内存泄漏。尽量将变量的作用域限制在最小范围内,避免全局污染。
    • 注意事件监听和回调函数中的内存管理:在添加事件监听器或使用回调函数时,要确保在不再需要时正确地移除它们,避免因残留的引用导致内存泄漏。例如,在JavaScript中,使用 addEventListener 添加事件监听器后,要使用 removeEventListener 来移除。
    • 使用内存分析工具:借助一些内存分析工具来检测程序中的内存泄漏问题。例如,在浏览器中可以使用开发者工具的性能分析面板来查看内存使用情况,在Node.js中可以使用 node -prof 等工具来分析内存泄漏的原因。通过这些工具,可以找出哪些对象没有被正确释放,从而针对性地进行修复。
  • 7.如何处理循环的异步操作
答案

在 JavaScript 里,处理循环的异步操作是常见需求,不过由于异步操作的特性,直接处理可能会引发问题。下面为你介绍几种常见的处理循环异步操作的方法。

1. 使用 for...of 循环结合 async/await

async/await 是 ES2017 引入的语法糖,能让异步代码以同步的方式书写,结合 for...of 循环可按顺序处理异步操作。

javascript
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

javascript
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.racePromise.race 接收一个包含多个 Promise 的数组,当数组中的任意一个 Promise 解决或拒绝时,它会返回该 Promise 的结果。

javascript
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. 使用递归处理异步循环

递归也是处理异步循环的一种方式,特别是在需要按顺序执行异步操作且循环次数不确定时。

javascript
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 中,每个文件就是一个模块,拥有自己独立的作用域。模块通过 exportsmodule.exports 来导出模块中的变量、函数或类,使用 require 函数来引入其他模块。
javascript
// 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 导出了 addsubtract 函数,main.js 模块使用 require 函数引入了 math.js 模块,并使用其中的函数。

2. AMD(Asynchronous Module Definition)

  • 应用场景:主要用于浏览器环境,适合异步加载模块。
  • 原理:AMD 采用异步加载模块的方式,通过 define 函数来定义模块,require 函数来加载模块。define 函数接收三个参数:模块名(可选)、依赖数组和模块定义函数。
html
<!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>
javascript
// 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 关键字来引入其他模块。
javascript
// 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 关键字导出 addsubtract 函数,main.js 模块使用 import 关键字引入这些函数。

4. UMD(Universal Module Definition)

  • 应用场景:兼容 CommonJS、AMD 和全局变量的模块化方案,使得模块可以在不同的环境中使用。
  • 原理:UMD 模块通过判断当前环境是 CommonJS、AMD 还是全局环境,来选择不同的模块定义方式。
javascript
(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
    };
}));

在上述代码中,通过判断 definemodule.exports 是否存在,来确定当前环境是 AMD、CommonJS 还是全局环境,并选择相应的模块定义方式。

执行机制

  • 1.为何try里面放returnfinally还会执行,理解其内部机制
答案

在 JavaScript 或者 Java 等很多编程语言里,当 try 块中存在 return 语句时,finally 块依旧会执行。下面详细解释其内部机制。

基本代码示例

以 JavaScript 为例:

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. 代码示例验证

javascript
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. 回调函数

回调函数是最基础的异步编程方式,把一个函数作为参数传递给另一个函数,在异步操作完成后调用这个回调函数。

javascript
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 方法处理失败的情况。

javascript
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 的语法糖,能让异步代码以同步的方式书写,使代码更易读和维护。

javascript
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)

  • 任务队列:也称为宏任务队列,用于存储异步任务的回调函数,如 setTimeoutsetIntervalsetImmediate 等。
  • 微任务队列:用于存储微任务的回调函数,如 Promise 的 thencatch 方法、MutationObserver 等。微任务的优先级高于宏任务。

4. Event Loop 的执行流程

  1. 执行同步代码:JavaScript 引擎首先会执行调用栈中的同步代码,将同步函数依次压入调用栈并执行,执行完毕后从调用栈中弹出。
  2. 处理异步任务:当遇到异步任务(如 setTimeout、Promise 等)时,会将这些任务交给浏览器的 Web API 处理,同时继续执行后续的同步代码。
  3. 任务完成:当异步任务完成后,其回调函数会被放入相应的任务队列(宏任务队列或微任务队列)中。
  4. Event Loop 检查:当调用栈为空时,Event Loop 会开始工作。它会先检查微任务队列,如果微任务队列中有任务,会依次将微任务队列中的任务取出并放入调用栈中执行,直到微任务队列为空。
  5. 处理宏任务:当微任务队列为空后,Event Loop 会从宏任务队列中取出一个任务放入调用栈中执行,执行完毕后,再次检查微任务队列,重复上述步骤。

5. 示例代码及执行分析

javascript
console.log('Start');

setTimeout(() => {
    console.log('setTimeout');
}, 0);

Promise.resolve().then(() => {
    console.log('Promise then');
});

console.log('End');

执行步骤如下:

  1. 执行同步代码,打印 StartEnd
  2. setTimeout 是异步任务,交给 Web API 处理,其回调函数会在 0 毫秒后放入宏任务队列。
  3. Promise.resolve().then 是微任务,其回调函数会放入微任务队列。
  4. 调用栈为空,Event Loop 开始工作,先处理微任务队列,打印 Promise then
  5. 微任务队列为空,处理宏任务队列,打印 setTimeout

综上所述,Event Loop 机制通过协调调用栈、任务队列和微任务队列,实现了 JavaScript 的异步编程,确保异步任务能在合适的时机执行。

  • 3.宏任务和微任务分别有哪些
答案

宏任务和微任务是JavaScript中用于处理异步操作的两种任务类型。以下分别介绍常见的宏任务和微任务:

宏任务(Macrotask)

  • setTimeout:设置一个定时器,在指定的延迟时间后执行回调函数。
  • setInterval:设置一个定时器,按照指定的时间间隔重复执行回调函数。
  • setImmediate:将回调函数添加到下一个事件循环迭代中执行,优先级高于 setTimeoutsetInterval
  • I/O 操作:例如文件读取、网络请求等,完成后会将相应的回调函数放入宏任务队列。
  • UI 渲染:浏览器在适当的时候会将UI渲染任务添加到宏任务队列中,以更新页面的显示。

微任务(Microtask)

  • Promisethencatch 回调:当 Promise 被解决(resolved)或被拒绝(rejected)后,thencatch 方法中的回调函数会被添加到微任务队列中。
  • 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.可以快速分析一个复杂的异步嵌套逻辑,并掌握分析方法
答案

分析复杂的异步嵌套逻辑时,关键在于理解异步操作的执行顺序,以下为你介绍一套有效的分析方法,并结合示例进行说明。

分析方法

  1. 识别异步操作类型:首先要确定代码中包含哪些异步操作,常见的异步操作有 setTimeoutsetIntervalPromiseasync/await 等。不同类型的异步操作在执行顺序和机制上有所不同。
  2. 明确任务队列:了解宏任务和微任务的区别,以及它们各自包含哪些异步操作。宏任务如 setTimeoutsetInterval 等,微任务如 Promisethencatch 方法。
  3. 梳理执行顺序:按照事件循环机制,同步代码先执行,当遇到异步操作时,将其放入相应的任务队列中。当调用栈为空时,先处理微任务队列中的任务,处理完后再从宏任务队列中取出一个任务执行,重复此过程。
  4. 绘制流程图:对于复杂的异步嵌套逻辑,绘制流程图可以帮助你更清晰地理解代码的执行流程。将每个异步操作和其回调函数用节点表示,用箭头表示执行顺序。

示例分析

javascript
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. 执行同步代码:首先执行同步代码,依次打印 1. 同步代码开始2. 同步代码结束
  2. 处理异步操作
    • 遇到 setTimeout,将其回调函数放入宏任务队列。
    • 遇到 Promise.resolve().then,将其回调函数放入微任务队列。
  3. 处理微任务队列:当调用栈为空时,Event Loop 开始工作,先处理微任务队列中的任务,打印 3. Promise then 回调
  4. 处理宏任务队列:微任务队列处理完毕后,从宏任务队列中取出 setTimeout 的回调函数执行,打印 4. setTimeout 回调开始
  5. 再次处理微任务队列:在 setTimeout 回调中又遇到 Promise.resolve().then,将其回调函数放入微任务队列。继续执行 setTimeout 回调中的同步代码,打印 5. setTimeout 回调结束。此时调用栈再次为空,处理微任务队列,打印 6. Promise then 回调

总结

通过以上步骤,我们可以清晰地分析出复杂异步嵌套逻辑的执行顺序。在实际分析中,要牢记事件循环机制,按照同步代码、微任务、宏任务的顺序依次处理,逐步梳理出代码的执行流程。

  • 5.使用Promise实现串行
答案

在 JavaScript 里,借助 Promise 实现串行操作是一种常见需求,也就是按顺序依次执行多个异步任务。下面介绍几种实现 Promise 串行的方法。

方法一:使用 .then() 链式调用

通过 .then() 方法进行链式调用,能保证每个 Promise 按顺序执行。以下是示例代码:

javascript
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。示例如下:

javascript
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 的串行执行。示例代码如下:

javascript
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` 都是用于处理异步操作的机制,但由于运行环境和使用场景不同,它们存在一些差异,下面从任务队列、执行顺序等方面详细介绍。

任务队列的差异

浏览器的任务队列

  • 宏任务队列:包含 setTimeoutsetIntervalsetImmediate(部分浏览器支持)、I/O 操作(如网络请求)、UI 渲染 等。
  • 微任务队列:包含 Promisethencatch 回调、MutationObserver 等。

Node.js 的任务队列

  • 宏任务队列
    • timer 阶段:执行 setTimeoutsetInterval 的回调函数。
    • I/O callbacks 阶段:处理一些系统级的 I/O 回调,如网络、文件操作等。
    • idle, prepare 阶段:仅供内部使用。
    • poll 阶段:获取新的 I/O 事件,执行 I/O 相关的回调。
    • check 阶段:执行 setImmediate 的回调函数。
    • close callbacks 阶段:执行一些关闭操作的回调,如 socket.on('close')
  • 微任务队列:包含 Promisethencatch 回调、process.nextTick(Node.js 特有的微任务)等。

执行顺序的差异

浏览器的 Event Loop 执行顺序

  1. 执行同步代码。
  2. 当调用栈为空时,检查微任务队列,依次执行微任务队列中的所有任务,直到微任务队列为空。
  3. 从宏任务队列中取出一个任务执行,执行完毕后,再次检查微任务队列,重复上述步骤。

Node.js 的 Event Loop 执行顺序

  1. 执行同步代码。
  2. 当调用栈为空时,进入 Event Loop,依次经过各个阶段:
    • timer 阶段:检查是否有 setTimeoutsetInterval 的回调函数到期,如果有则执行。
    • I/O callbacks 阶段:处理系统级的 I/O 回调。
    • idle, prepare 阶段:内部使用,一般开发者无需关注。
    • poll 阶段
      • 如果 poll 队列中有回调函数,则依次执行。
      • 如果 poll 队列为空:
        • 如果有 setImmediate 回调,则进入 check 阶段。
        • 如果没有 setImmediate 回调,则等待 I/O 事件的到来。
    • check 阶段:执行 setImmediate 的回调函数。
    • close callbacks 阶段:执行关闭操作的回调。
  3. 在每个阶段切换之前,会检查微任务队列,执行微任务队列中的所有任务。

特殊微任务的差异

浏览器

浏览器中主要的微任务是 Promisethencatch 回调、MutationObserver 等,没有像 process.nextTick 这样的特殊微任务。

Node.js

process.nextTick 是 Node.js 特有的微任务,它的优先级高于 Promisethencatch 回调。在每个阶段切换之前,都会先执行 process.nextTick 队列中的所有任务。

示例代码对比

浏览器环境

javascript
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 环境

javascript
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 中用于异步执行代码的一个函数,同时在部分现代浏览器环境中也得到了支持。下面将从其基本概念、使用场景、与 setTimeoutprocess.nextTick 的对比等方面进行详细介绍。

1、基本概念 setImmediate 用于将一个回调函数添加到事件循环的 check 阶段执行。当事件循环进入 check 阶段时,会执行所有通过 setImmediate 注册的回调函数。

2、基本语法

javascript
setImmediate(callback[, ...args]);
  • callback:必需参数,即需要异步执行的回调函数。
  • ...args:可选参数,传递给回调函数的参数。

3、示例代码

javascript
console.log('开始');

setImmediate(() => {
    console.log('setImmediate 的回调函数执行');
});

console.log('结束');

在上述代码中,setImmediate 的回调函数会在当前同步代码执行完毕后,事件循环进入 check 阶段时执行。

4、使用场景

  • 异步 I/O 操作后的回调:在完成一个 I/O 操作(如文件读取、网络请求)后,如果需要执行一些后续处理逻辑,但又不想阻塞其他异步操作,可以使用 setImmediate。例如:
javascript
const fs = require('fs');

fs.readFile('example.txt', 'utf8', (err, data) => {
    if (err) {
        console.error(err);
        return;
    }
    setImmediate(() => {
        // 对读取到的数据进行处理
        console.log('开始处理读取到的数据');
        console.log(data);
    });
});
  • 避免递归调用导致的栈溢出:当需要进行递归操作时,如果递归深度过大,可能会导致栈溢出错误。可以使用 setImmediate 将递归调用转换为异步操作,避免栈溢出。例如:
javascript
function recursiveFunction(count) {
    if (count === 0) {
        return;
    }
    console.log(count);
    setImmediate(() => {
        recursiveFunction(count - 1);
    });
}

recursiveFunction(10000);

5、与 setTimeoutprocess.nextTick 的对比

  • setTimeout 的对比
    • setTimeout 用于在指定的延迟时间后执行回调函数,而 setImmediate 会在当前事件循环的 check 阶段执行回调函数。
    • setTimeout 的延迟时间设置为 0 时,由于 setTimeout 的回调函数会在 timer 阶段执行,而 setImmediate 的回调函数会在 check 阶段执行,它们的执行顺序可能会有所不同,取决于事件循环的状态。例如:
javascript
setTimeout(() => {
    console.log('setTimeout 回调');
}, 0);

setImmediate(() => {
    console.log('setImmediate 回调');
});

在不同的环境和条件下,上述代码的输出顺序可能不同。

  • process.nextTick 的对比
    • process.nextTick 是 Node.js 特有的微任务,它的优先级高于 setImmediate。在每个事件循环阶段切换之前,会先执行 process.nextTick 队列中的所有回调函数。
    • setImmediate 是宏任务,会在 check 阶段执行。例如:
javascript
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.理解ECMAScriptJavaScript的关系
答案

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.熟练运用es5es6提供的语法规范
答案

ES5(ECMAScript 5)和 ES6(ECMAScript 2015)为 JavaScript 带来了诸多实用的语法特性,下面为你详细介绍一些常用语法规范及示例。

ES5 常用语法规范

1. 严格模式

严格模式对 JavaScript 代码增加了更严格的语法和行为检查,有助于编写更规范、更安全的代码。在脚本或函数开头添加 "use strict"; 即可开启。

javascript
function strictFunction() {
    "use strict";
    // 严格模式下不允许未声明就使用变量
    // x = 10;  // 这行代码会报错
    var x = 10;
    return x;
}
console.log(strictFunction());

2. Object.defineProperty()

用于直接在一个对象上定义一个新属性,或者修改一个现有属性的配置,并返回这个对象。

javascript
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():对数组的每个元素执行一次提供的函数。
javascript
var numbers = [1, 2, 3];
numbers.forEach(function (number) {
    console.log(number);
});
  • Array.prototype.map():创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。
javascript
var squaredNumbers = numbers.map(function (number) {
    return number * number;
});
console.log(squaredNumbers);
  • Array.prototype.filter():创建一个新数组,其包含通过所提供函数实现的测试的所有元素。
javascript
var evenNumbers = numbers.filter(function (number) {
    return number % 2 === 0;
});
console.log(evenNumbers);

ES6 常用语法规范

1. 块级作用域

引入 letconst 关键字,用于声明块级作用域的变量和常量。

javascript
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 值。

javascript
var numbers = [1, 2, 3];
var squaredNumbers = numbers.map(number => number * number);
console.log(squaredNumbers);

3. 模板字符串

使用反引号(`)来定义字符串,可以在字符串中嵌入表达式。

javascript
var name = 'John';
var message = `Hello, ${name}!`;
console.log(message);

4. 解构赋值

可以从数组或对象中提取值,并赋值给变量。

javascript
// 数组解构
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 关键字实现类的继承。

javascript
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 对象

用于处理异步操作,避免回调地狱。

javascript
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提供的全局对象(例如DateMath)、全局函数(例如decodeURIisNaN)、全局属性(例如Infinityundefined
答案

在 JavaScript 里,全局对象、全局函数和全局属性是非常重要的基础内容,下面将详细介绍一些常用的全局对象、全局函数和全局属性。

全局对象

Date 对象

Date 对象用于处理日期和时间。可以使用它来创建日期实例、获取和设置日期时间的各个部分,以及进行日期时间的计算。

javascript
// 创建一个表示当前时间的 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 对象提供了许多用于数学计算的属性和方法。

javascript
// 获取圆周率
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);

全局函数

decodeURIencodeURI

  • encodeURI 用于对 URI(统一资源标识符)进行编码,将特殊字符转换为 URI 安全的格式。
  • decodeURI 用于对经过 encodeURI 编码的 URI 进行解码。
javascript
const uri = 'https://example.com/?name=张三';
const encodedUri = encodeURI(uri);
console.log(encodedUri);

const decodedUri = decodeURI(encodedUri);
console.log(decodedUri);

isNaN

isNaN 函数用于判断一个值是否为 NaN(非数字)。

javascript
const num1 = 10;
const num2 = 'abc';
const num3 = NaN;

console.log(isNaN(num1)); 
console.log(isNaN(num2)); 
console.log(isNaN(num3));

parseIntparseFloat

  • parseInt 用于将一个字符串解析为整数。
  • parseFloat 用于将一个字符串解析为浮点数。
javascript
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 表示负无穷大。

javascript
const positiveInfinity = 1 / 0;
const negativeInfinity = -1 / 0;

console.log(positiveInfinity === Infinity); 
console.log(negativeInfinity === -Infinity);

NaN

NaN 表示非数字,通常在进行无效的数学运算时会得到 NaN

javascript
const result = 'abc' / 2;
console.log(result === NaN); 
console.log(isNaN(result));

undefined

undefined 表示变量已声明但未赋值,或者函数没有返回值。

javascript
let variable;
console.log(variable === undefined); 

function noReturnValue() {}
const returnValue = noReturnValue();
console.log(returnValue === undefined);

通过对这些全局对象、全局函数和全局属性的学习和使用,你能够更高效地处理日期时间、数学计算、字符串编码解码等常见的编程任务。

  • 4.熟练应用mapreducefilter 等高阶函数解决问题
答案

在 JavaScript 里,mapreducefilter 都是数组的高阶函数,它们可以让你以声明式的方式处理数组,编写更加简洁和高效的代码。下面会详细介绍这些函数的用法,并且给出一些实际应用的示例。

map 函数

map 函数会对数组中的每个元素执行一次提供的函数,然后返回一个新数组,新数组中的元素是原数组元素经过处理后的结果。

语法

javascript
const newArray = array.map((currentValue, index, array) => {
    // 处理逻辑
    return processedValue;
});
  • currentValue:当前正在处理的数组元素。
  • index:当前元素的索引。
  • array:调用 map 方法的数组。

示例

将数组中的每个元素都乘以 2。

javascript
const numbers = [1, 2, 3, 4, 5];
const doubledNumbers = numbers.map(num => num * 2);
console.log(doubledNumbers);

filter 函数

filter 函数会创建一个新数组,新数组中的元素是原数组中满足所提供函数测试的所有元素。

语法

javascript
const newArray = array.filter((currentValue, index, array) => {
    // 测试条件
    return condition;
});

示例

筛选出数组中的偶数。

javascript
const numbers = [1, 2, 3, 4, 5];
const evenNumbers = numbers.filter(num => num % 2 === 0);
console.log(evenNumbers);

reduce 函数

reduce 函数对数组中的每个元素执行一个由你提供的 reducer 函数,将其结果汇总为单个返回值。

语法

javascript
const result = array.reduce((accumulator, currentValue, index, array) => {
    // 处理逻辑
    return accumulator + processedValue;
}, initialValue);
  • accumulator:累加器,用于存储上一次调用 reducer 函数的返回值。
  • currentValue:当前正在处理的数组元素。
  • index:当前元素的索引。
  • array:调用 reduce 方法的数组。
  • initialValue:可选参数,累加器的初始值。如果没有提供,数组的第一个元素会作为初始值,并且从第二个元素开始执行 reducer 函数。

示例

计算数组中所有元素的总和。

javascript
const numbers = [1, 2, 3, 4, 5];
const sum = numbers.reduce((acc, num) => acc + num, 0);
console.log(sum);

综合应用示例

假设有一个包含对象的数组,每个对象代表一个人,包含 nameage 属性。现在要完成以下任务:

  1. 筛选出年龄大于 18 岁的人。
  2. 提取这些人的名字。
  3. 把这些名字用逗号连接成一个字符串。
javascript
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 函数的返回值。

javascript
// 设置定时器
const intervalId = setInterval(() => {
    console.log('This will repeat every 2 seconds');
}, 2000);

// 5 秒后清除定时器
setTimeout(() => {
    clearInterval(intervalId);
    console.log('Interval cleared');
}, 5000);

2. 执行时机

setInterval 不会等待前一次函数执行完毕就会开始下一次计时。如果函数执行时间超过了设定的时间间隔,那么函数会连续执行,可能会导致一些意外的结果。

javascript
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

javascript
const obj = {
    name: 'Example',
    startInterval() {
        setInterval(function() {
            console.log(this.name); // 这里的 this 指向全局对象或 undefined
        }, 1000);
    }
};

obj.startInterval();

使用 setTimeout 实现 setInterval

可以通过递归调用 setTimeout 来模拟 setInterval 的功能,这样可以避免 setInterval 不等待函数执行完毕就开始下一次计时的问题。

javascript
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 的功能。这种方式会等待前一次回调函数执行完毕后才会开始下一次计时。

同时,为了能够清除这个自定义的定时器,可以对上述代码进行扩展,返回一个清除定时器的函数:

javascript
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 构造函数来创建正则表达式对象。

javascript
// 使用正则表达式字面量
const regex1 = /abc/;

// 使用 RegExp 构造函数
const regex2 = new RegExp('abc');

2. 正则表达式对象的方法

test()

用于测试字符串中是否存在匹配正则表达式的模式,返回布尔值。

javascript
const regex = /abc/;
const str = 'abcd';
console.log(regex.test(str)); // 输出: true
exec()

在字符串中执行查找匹配的正则表达式,返回一个数组,包含匹配的结果信息;如果没有匹配到,则返回 null

javascript
const regex = /abc/;
const str = 'abcd';
const result = regex.exec(str);
console.log(result); 
// 输出: [ 'abc', index: 0, input: 'abcd', groups: undefined ]

3. 字符串对象的方法

match()

在字符串中查找匹配的正则表达式,返回一个数组,包含所有匹配的结果;如果没有匹配到,则返回 null

javascript
const regex = /abc/g; // g 标志表示全局匹配
const str = 'abcabc';
const result = str.match(regex);
console.log(result); // 输出: [ 'abc', 'abc' ]
replace()

用于替换字符串中匹配正则表达式的部分,返回替换后的新字符串。

javascript
const regex = /abc/g;
const str = 'abcabc';
const newStr = str.replace(regex, 'def');
console.log(newStr); // 输出: 'defdef'

在字符串中查找匹配正则表达式的位置,返回第一个匹配项的索引;如果没有匹配到,则返回 -1。

javascript
const regex = /abc/;
const str = 'abcd';
const index = str.search(regex);
console.log(index); // 输出: 0
split()

根据正则表达式将字符串分割成数组,返回分割后的数组。

javascript
const regex = /,/;
const str = 'apple,banana,orange';
const arr = str.split(regex);
console.log(arr); // 输出: [ 'apple', 'banana', 'orange' ]

使用正则表达式解决常见问题

1. 邮箱校验

邮箱地址通常遵循一定的格式,如 username@domain.com。可以使用正则表达式来校验邮箱地址的有效性。

javascript
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 的各个部分,如协议、域名、路径等。

javascript
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() 方法。

javascript
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 中最基本的异常处理机制,其基本语法如下:

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 语句用于手动抛出一个异常,你可以抛出一个内置的错误类型(如 ErrorSyntaxError 等),也可以抛出自定义的错误对象。

javascript
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 被拒绝时抛出的异常。

javascript
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 语句来捕获异常。

javascript
async function main() {
    try {
        const result = await asyncOperation();
        console.log(result);
    } catch (error) {
        console.error('捕获到异常:', error.message);
    }
}

main();

统一的异常处理方案

为了实现统一的异常处理,可以创建一个全局的异常处理函数,并在合适的地方调用它。以下是一个简单的示例:

javascript
// 全局异常处理函数
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 并调用全局异常处理函数:

javascript
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 差异)。
  • 自带属性idclassstyletitle 等通用属性,还可使用 hidden 等布尔属性。
  • 不同浏览器的差异:默认的 margin 和 padding 可能不同,不过差距很小。一些旧版本浏览器可能对某些 CSS3 样式的支持不完全。
  • 处理兼容问题的方式:使用 CSS 重置样式表,如 * { margin: 0; padding: 0; } 来统一不同浏览器的默认样式。

2. <p> 标签

  • 默认样式:块级元素,上下有一定的外边距,用于表示段落文本。
  • 自带属性:通用属性如 idclassstyletitle 等。
  • 不同浏览器的差异:默认的外边距大小可能在不同浏览器中略有不同。
  • 处理兼容问题的方式:通过 CSS 自定义外边距,例如 p { margin: 1em 0; } 来统一段落的外边距。

3. <a> 标签

  • 默认样式:内联元素,文本通常带有下划线,颜色为蓝色(未访问)、紫色(已访问)。
  • 自带属性href(指定链接地址)、target(指定链接打开方式,如 _blank 表示在新窗口打开)、title(鼠标悬停时显示的提示信息)等。
  • 不同浏览器的差异:在某些旧版本浏览器中,target 属性的兼容性可能存在问题;链接颜色和下划线样式可能有细微差别。
  • 处理兼容问题的方式:使用 CSS 统一链接的样式,如 a { color: #007bff; text-decoration: none; } 去除下划线并统一颜色。

4. <img> 标签

  • 默认样式:内联元素,没有边框(在某些旧浏览器中,可能会有默认边框)。
  • 自带属性src(指定图片的源地址)、alt(图片无法显示时的替代文本)、widthheight(指定图片的宽度和高度)等。
  • 不同浏览器的差异:图片的缩放算法可能不同,导致图片在不同浏览器中的显示效果略有差异;旧版本浏览器可能不支持某些图片格式。
  • 处理兼容问题的方式:使用 CSS 控制图片的缩放和布局,如 img { max-width: 100%; height: auto; } 确保图片在不同设备上自适应显示。同时,可以提供多种图片格式供不同浏览器选择。

5. <ul><ol> 标签

  • 默认样式:块级元素,<ul> 列表项前有实心圆点,<ol> 列表项前有数字序号,都有一定的内边距。
  • 自带属性:通用属性如 idclassstyle 等,<ol> 还可以使用 start 属性指定序号的起始值。
  • 不同浏览器的差异:列表项的缩进和符号样式可能不同。
  • 处理兼容问题的方式:使用 CSS 自定义列表样式,如 ul { list-style-type: none; padding: 0; } 去除默认的列表样式和内边距。

6. <input> 标签

  • 默认样式:外观因 type 属性而异,如 type="text" 是文本输入框,type="button" 是按钮等。
  • 自带属性type(指定输入框的类型)、name(表单提交时的名称)、value(输入框的值)、placeholder(输入框的占位文本)等。
  • 不同浏览器的差异:输入框的外观、尺寸和样式在不同浏览器中可能有较大差异;一些 HTML5 新增的 type 属性(如 emaildate 等)在旧版本浏览器中可能不支持。
  • 处理兼容问题的方式:使用 CSS 统一输入框的样式,如 input { border: 1px solid #ccc; padding: 5px; }。对于不支持 HTML5 新增 type 属性的浏览器,可以使用 JavaScript 进行 polyfill 处理。

7. <select> 标签

  • 默认样式:下拉选择框,有特定的外观和交互方式。
  • 自带属性name(表单提交时的名称)、multiple(是否允许多选)等。
  • 不同浏览器的差异:下拉选择框的外观、尺寸和样式在不同浏览器中可能有较大差异。
  • 处理兼容问题的方式:使用 CSS 自定义下拉选择框的样式,如改变背景颜色、边框样式等。也可以使用 JavaScript 插件来实现统一的下拉选择框效果。
  • 3.元信息类标签(headtitlemeta)的使用目的和配置方法
答案

headtitlemeta 等元信息类标签在 HTML 中用于提供关于网页的元数据,帮助浏览器和搜索引擎更好地理解和处理网页。以下是它们的使用目的和配置方法:

<head> 标签

  • 使用目的<head> 标签是 HTML 文档的头部标签,它包含了文档的元数据,如文档标题、样式表、脚本、字符编码声明等。这些信息不会直接显示在网页内容中,但对于浏览器正确渲染页面以及搜索引擎索引页面起着重要作用。
  • 配置方法:在 HTML 文档的开头,紧跟在 <html> 标签之后使用 <head> 标签,然后在 <head> 标签内部添加各种元信息标签和其他相关元素。例如:
html
<head>
  <title>网页标题</title>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="styles.css">
  <script src="script.js"></script>
</head>

<title> 标签

  • 使用目的<title> 标签用于定义网页的标题,它会显示在浏览器的标题栏或标签页上,同时也是搜索引擎结果中显示的重要信息之一,帮助用户快速了解网页的主题。
  • 配置方法<title> 标签必须放在 <head> 标签内部,其内容是网页的标题文本。例如:
html
<head>
  <title>我的精彩网页</title>
</head>

<meta> 标签

  • 使用目的<meta> 标签用于提供各种元数据,如字符编码、页面描述、关键词、作者信息、视口设置等,以帮助搜索引擎优化(SEO)、浏览器渲染和移动设备适配等。
  • 配置方法
    • 设置字符编码:使用 charset 属性指定文档的字符编码,确保浏览器正确解析页面中的字符。例如:
html
<meta charset="UTF-8">
- **设置页面描述**:使用 `name="description"` 和 `content` 属性来描述网页的内容,这有助于搜索引擎在搜索结果中显示更准确的摘要。例如:
html
<meta name="description" content="这是一个关于旅游的网站,提供各种旅游攻略和景点介绍。">
- **设置关键词**:使用 `name="keywords"` 和 `content` 属性来指定与网页内容相关的关键词,多个关键词之间用逗号分隔。不过,现在搜索引擎对关键词元标签的依赖度已经降低。例如:
html
<meta name="keywords" content="旅游,攻略,景点,美食">
- **设置作者信息**:使用 `name="author"` 和 `content` 属性来指定网页的作者。例如:
html
<meta name="author" content="张三">
- **设置视口**:用于移动端网页适配,使用 `name="viewport"` 和 `content` 属性来设置视口的宽度、初始缩放比例等。例如:
html
<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 的工作流程主要分为注册、安装、激活和运行几个阶段:

  1. 注册:在网页中通过 JavaScript 代码注册 Service Worker。通常在页面加载完成后,调用 navigator.serviceWorker.register() 方法指定 Service Worker 脚本的路径进行注册。
javascript
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);
      });
  });
}
  1. 安装:注册成功后,浏览器会尝试安装 Service Worker。在安装阶段,可以监听 install 事件,利用 event.waitUntil() 方法缓存所需的静态资源,如 HTML、CSS、JavaScript 文件和图片等。
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'
        ]);
      })
  );
});
  1. 激活:安装完成后,Service Worker 进入等待状态,直到所有旧的 Service Worker 实例都关闭。当新的 Service Worker 激活时,会触发 activate 事件,通常在此事件中可以清理旧的缓存。
javascript
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);
        })
      );
    })
  );
});
  1. 运行:激活后,Service Worker 开始控制页面,此时可以监听 fetch 事件,拦截页面的网络请求。可以根据请求的 URL 决定是从缓存中获取资源,还是向服务器发起请求。
javascript
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 APISVG等绘制高性能的动画
答案

Canvas APISVG 都是在网页中绘制图形和创建动画的强大工具,它们各有特点,以下分别介绍如何使用它们来绘制高性能的动画。

使用 Canvas API 绘制高性能动画

Canvas 是 HTML5 新增的元素,通过 JavaScript 脚本来动态绘制图形和动画。Canvas 基于像素操作,适合绘制复杂的、频繁更新的动画,例如游戏、数据可视化等。

示例:简单的小球移动动画

html
<!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:避免使用 setIntervalsetTimeoutrequestAnimationFrame 会根据浏览器的刷新频率来调用动画函数,减少不必要的重绘,提高性能。
    • 批量操作:尽量减少对画布的频繁绘制和清除操作,可以将多个图形的绘制操作合并,减少重绘次数。
    • 使用离屏画布:对于复杂的图形绘制,可以先在离屏画布上绘制,然后再将其复制到主画布上,减少主画布的绘制负担。
  • SVG 性能优化

    • 减少元素数量:尽量简化 SVG 图形的结构,减少不必要的元素和嵌套,降低浏览器的渲染负担。
    • 使用 CSS 动画:对于简单的动画效果,可以使用 CSS 动画来实现,因为 CSS 动画由浏览器的渲染引擎直接处理,性能更高。
    • 避免过度使用滤镜和渐变:滤镜和渐变会增加 SVG 的复杂度,影响渲染性能,尽量减少使用。

CSS

  • 1.CSS盒模型,在不同浏览器的差异
答案

CSS布局

  • 2.CSS所有选择器及其优先级、使用场景,哪些可以继承,如何运用at规则
答案

CSS 选择器及其优先级、使用场景

1. 元素选择器

  • 语法:直接使用 HTML 元素名称,如 ph1div 等。
  • 优先级:较低,权重为 1。
  • 使用场景:用于对页面中所有相同元素设置统一的样式,例如设置所有段落的字体颜色和行高。
css
p {
    color: blue;
    line-height: 1.5;
}
  • 继承情况:部分属性可继承,如 colorfont-familyfont-size 等。

2. 类选择器

  • 语法:以 . 开头,后面跟上类名,如 .my-class
  • 优先级:较高,权重为 10。
  • 使用场景:当需要对页面中多个不同元素应用相同样式,或者对某个元素添加特定样式时使用。例如,为所有具有 highlight 类的元素设置背景颜色。
css
.highlight {
    background-color: yellow;
}
  • 继承情况:取决于具体属性,可继承属性会继承。

3. ID 选择器

  • 语法:以 # 开头,后面跟上 ID 名,如 #my-id
  • 优先级:非常高,权重为 100。
  • 使用场景:用于为页面中唯一的元素设置样式,例如为页面的导航栏设置特定样式。
css
#navbar {
    background-color: #333;
    color: white;
}
  • 继承情况:取决于具体属性,可继承属性会继承。

4. 属性选择器

  • 语法:根据元素的属性来选择元素,如 [attribute][attribute=value] 等。
  • 优先级:与类选择器相同,权重为 10。
  • 使用场景:当需要根据元素的属性值来选择元素并设置样式时使用。例如,为所有 disabled 属性的输入框设置灰色背景。
css
input[disabled] {
    background-color: #ccc;
}
  • 继承情况:取决于具体属性,可继承属性会继承。

5. 伪类选择器

  • 语法:以 : 开头,如 :hover:active:first-child 等。
  • 优先级:与类选择器相同,权重为 10。
  • 使用场景:用于根据元素的特定状态或位置来设置样式。例如,为鼠标悬停在链接上时设置不同的颜色。
css
a:hover {
    color: red;
}
  • 继承情况:通常不继承。

6. 伪元素选择器

  • 语法:以 :: 开头,如 ::before::after::first-letter 等。
  • 优先级:与类选择器相同,权重为 10。
  • 使用场景:用于在元素的特定位置插入虚拟的内容或设置样式。例如,为段落的第一个字母设置较大的字体。
css
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 元素设置样式。
css
nav a {
    text-decoration: none;
}
  • 继承情况:取决于具体属性,可继承属性会继承。

CSS 选择器优先级规则

选择器的优先级由权重决定,权重越高,优先级越高。当多个选择器作用于同一个元素且样式冲突时,优先级高的选择器的样式会覆盖优先级低的选择器的样式。优先级从高到低依次为:

  1. 内联样式(权重为 1000)。
  2. ID 选择器(权重为 100)。
  3. 类选择器、属性选择器、伪类选择器(权重为 10)。
  4. 元素选择器、伪元素选择器(权重为 1)。
  5. 通配符选择器 *(权重为 0)。

CSS 可继承属性

可继承的属性通常是与文本相关的属性,如:

  • color:文本颜色。
  • font-family:字体家族。
  • font-size:字体大小。
  • font-style:字体样式(如斜体)。
  • font-weight:字体粗细。
  • line-height:行高。
  • text-align:文本对齐方式。
  • text-indent:文本缩进。

CSS @规则的运用

1. @import

  • 作用:用于在 CSS 文件中导入其他 CSS 文件。
  • 语法
css
@import url('styles.css');
  • 使用场景:当项目中有多个 CSS 文件,需要将它们合并使用时,可以使用 @import 导入。

2. @media

  • 作用:用于根据不同的媒体查询条件应用不同的样式,实现响应式设计。
  • 语法
css
@media (max-width: 768px) {
    body {
        font-size: 14px;
    }
}
  • 使用场景:当需要根据设备的屏幕宽度、高度、分辨率等条件来调整页面样式时使用。

3. @keyframes

  • 作用:用于定义 CSS 动画的关键帧。
  • 语法
css
@keyframes slide {
    0% {
        transform: translateX(0);
    }
    100% {
        transform: translateX(100px);
    }
}

.element {
    animation: slide 2s infinite;
}
  • 使用场景:当需要创建复杂的动画效果时使用。

4. @font-face

  • 作用:用于引入自定义字体。
  • 语法
css
@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 属性或值。
  • 语法
css
@supports (display: flex) {
    .container {
        display: flex;
    }
}
  • 使用场景:当需要根据浏览器的特性支持情况来应用不同的样式时使用。
  • 3.CSS伪类和伪元素有哪些,它们的区别和实际应用
答案

CSS 伪类和伪元素的介绍

常见的 CSS 伪类

  • 链接相关伪类
    • :link:用于选取未访问的链接。例如:
css
a:link {
    color: blue;
}
- `:visited`:用于选取已访问的链接。例如:
css
a:visited {
    color: purple;
}
  • 用户行为伪类
    • :hover:当鼠标悬停在元素上时应用样式。常用于按钮、链接等可交互元素。例如:
css
button:hover {
    background-color: lightgray;
}
- `:active`:当元素被激活(如鼠标点击时)应用样式。例如:
css
a:active {
    color: red;
}
- `:focus`:当元素获得焦点时应用样式,常用于表单元素。例如:
css
input:focus {
    border: 2px solid blue;
}
  • 结构伪类
    • :first-child:选取父元素的第一个子元素。例如:
css
ul li:first-child {
    font-weight: bold;
}
- `:last-child`:选取父元素的最后一个子元素。例如:
css
ul li:last-child {
    color: gray;
}
- `:nth-child(n)`:选取父元素的第 `n` 个子元素,`n` 可以是数字、表达式或关键字。例如,选取偶数项:
css
ul li:nth-child(even) {
    background-color: lightblue;
}
  • 状态伪类
    • :disabled:选取禁用的表单元素。例如:
css
input:disabled {
    background-color: #ccc;
}
- `:checked`:选取被选中的表单元素(如复选框、单选框)。例如:
css
input:checked + label {
    color: green;
}

常见的 CSS 伪元素

  • ::before:在元素内容的前面插入虚拟内容。例如,为每个段落的开头添加引号:
css
p::before {
    content: "“";
}
  • ::after:在元素内容的后面插入虚拟内容。例如,为每个链接的后面添加外部链接图标:
css
a::after {
    content: " 🔗";
}
  • ::first-letter:选取元素的第一个字母。常用于首字母大写等效果。例如:
css
p::first-letter {
    font-size: 2em;
}
  • ::first-line:选取元素的第一行。例如:
css
p::first-line {
    font-weight: bold;
}
  • ::selection:选取用户选中的文本。例如,改变选中文本的背景颜色和文字颜色:
css
::selection {
    background-color: yellow;
    color: red;
}

伪类和伪元素的区别

  • 语法不同:伪类使用单冒号 :,伪元素在 CSS3 中推荐使用双冒号 ::,不过单冒号在很多情况下也能兼容。
  • 本质不同:伪类用于根据元素的特定状态或位置来选择元素,它并没有创建新的元素;而伪元素用于创建虚拟的元素,这些元素在 HTML 代码中并不存在,但可以在页面中显示出来。
  • 优先级不同:伪类和类选择器的优先级相同,权重为 10;伪元素和元素选择器的优先级相同,权重为 1。

实际应用

  • 伪类的实际应用
    • 导航栏交互效果:使用 :hover 伪类为导航栏的链接添加悬停效果,增强用户体验。
    • 表单验证提示:使用 :valid:invalid 伪类根据表单元素的输入有效性显示不同的样式,给用户提供反馈。
    • 表格隔行变色:使用 :nth-child 伪类为表格的偶数行或奇数行设置不同的背景颜色,提高表格的可读性。
  • 伪元素的实际应用
    • 清除浮动:使用 ::after 伪元素结合 clear: both 来清除浮动,避免父元素高度塌陷。
css
.clearfix::after {
    content: "";
    display: block;
    clear: both;
}
- **图标添加**:使用 `::before` 或 `::after` 伪元素为元素添加图标,而无需在 HTML 中额外添加标签。
- **首字母样式**:使用 `::first-letter` 伪元素为文章的首字母设置特殊样式,增加页面的美观度。 
  • 4.HTML文档流的排版规则,CSS几种定位的规则、定位参照物、对文档流的影响,如何选择最好的定位方式,雪碧图实现原理
答案

CSS布局

  • 5.水平垂直居中的方案、可以实现6种以上并对比它们的优缺点
答案

CSS布局

  • 6.BFC实现原理,可以解决的问题,如何创建BFC
答案

CSS布局

  • 7.可使用CSS函数复用代码,实现特殊效果
答案

CSS布局

  • 8.PostCSSSassLess的异同,以及使用配置,至少掌握一种
答案

CSS布局

  • 9.CSS模块化方案、如何配置按需加载、如何防止CSS阻塞渲染
答案

CSS布局

  • 10.熟练使用CSS实现常见动画,如渐变、移动、旋转、缩放等等
答案

CSS布局

  • 11.CSS浏览器兼容性写法,了解不同API在不同浏览器下的兼容性情况
答案

CSS布局

  • 12.掌握一套完整的响应式布局方案
答案

CSS布局

手写

  • 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.1HTTP2.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.stringifyJSON.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操作、海量数据的性能优化(合并操作、DiffrequestAnimationFrame等)

  • 4.浏览器海量数据存储、操作性能优化

  • 5.DOM事件流的具体实现机制、不同浏览器的差异、事件代理

  • 6.前端发起网络请求的几种方式及其底层实现、可以手写原生ajaxfetch、可以熟练使用第三方库

  • 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开发框架,如ExpressExpressKoa的区别

  • 3.熟练使用Node提供的APIPathHttpChild Process等并理解其实现原理

  • 4.Node的底层运行原理、和浏览器的异同

  • 5.Node事件驱动、非阻塞机制的实现原理

六、框架和类库

轮子层出不穷,从原理上理解才是正道

TypeScript

  • 1.理解泛型接口等面向对象的相关概念,TypeScript对面向对象理念的实现

  • 2.理解使用TypeScript的好处,掌握TypeScript基础语法

  • 3.TypeScript的规则检测原理

  • 4.可以在ReactVue等框架中使用TypeScript进行开发

React

  • 1.Reactvue 选型和优缺点、核心架构的区别

  • 2.ReactsetState的执行机制,如何有效的管理状态

  • 3.React的事件底层实现机制

  • 4.React的虚拟DOMDiff算法的内部实现

  • 5.ReactFiber工作原理,解决了什么问题

  • 6.React RouterVue Router的底层实现原理、动态加载实现原理

  • 7.可熟练应用React API、生命周期等,可应用HOCrender propsHooks等高阶用法解决问题

  • 8.基于React的特性和原理,可以手动实现一个简单的React

Vue

  • 1.熟练使用VueAPI、生命周期、钩子函数

  • 2.MVVM框架设计理念

  • 3.Vue双向绑定实现原理、Diff算法的内部实现

  • 4.Vue的事件机制

  • 5.从template转换成真实DOM的实现机制

多端开发

  • 1.单页面应用(SPA)的原理和优缺点,掌握一种快速开发SPA的方案

  • 2.理解Viewportemrem的原理和用法,分辨率、pxppidpidp的区别和实际应用

  • 3.移动端页面适配解决方案、不同机型适配方案

  • 4.掌握一种JavaScript移动客户端开发技术,如React Native:可以搭建React Native开发环境,熟练进行开发,可理解React Native的运作原理,不同端适配

  • 5.掌握一种JavaScript PC客户端开发技术,如Electron:可搭建Electron开发环境,熟练进行开发,可理解Electron的运作原理

  • 6.掌握一种小程序开发框架或原生小程序开发

  • 7.理解多端框架的内部实现原理,至少了解一个多端框架的使用

数据流管理

  • 1.掌握ReactVue传统的跨组件通信方案,对比采用数据流管理框架的异同

  • 2.熟练使用Redux管理数据流,并理解其实现原理,中间件实现原理

  • 3.熟练使用Mobx管理数据流,并理解其实现原理,相比Redux有什么优势

  • 4.熟练使用Vuex管理数据流,并理解其实现原理

  • 5.以上数据流方案的异同和优缺点,不情况下的技术选型

实用库

  • 1.至少掌握一种UI组件框架,如antd design,理解其设计理念、底层实现

  • 2.掌握一种图表绘制框架,如Echart,理解其设计理念、底层实现,可以自己实现图表

  • 3.掌握一种GIS开发框架,如百度地图API

  • 4.掌握一种可视化开发框架,如Three.jsD3

  • 5.工具函数库,如lodashunderscoremoment等,理解使用的工具类或工具函数的具体实现原理

开发和调试

  • 1.熟练使用各浏览器提供的调试工具

  • 2.熟练使用一种代理工具实现请求代理、抓包,如charls

  • 3.可以使用AndroidIOS模拟器进行调试,并掌握一种真机调试方案

  • 4.了解VueReact等框架调试工具的使用

七、前端工程

前端工程化:以工程化方法和工具提高开发生产效率、降低维护难度

项目构建

  • 1.理解npmyarn依赖包管理的原理,两者的区别

  • 2.可以使用npm运行自定义脚本

  • 3.理解BabelESLintwebpack等工具在项目中承担的作用

  • 4.ESLint规则检测原理,常用的ESLint配置

  • 5.Babel的核心原理,可以自己编写一个Babel插件

  • 6.可以配置一种前端代码兼容方案,如Polyfill

  • 7.Webpack的编译原理、构建流程、热更新原理,chunkbundlemodule的区别和应用

  • 8.可熟练配置已有的loadersplugins解决问题,可以自己编写loadersplugins

nginx

  • 1.正向代理与反向代理的特点和实例

  • 2.可手动搭建一个简单的nginx服务器、

  • 3.熟练应用常用的nginx内置变量,掌握常用的匹配规则写法

  • 4.可以用nginx实现请求过滤、配置gzip、负载均衡等,并能解释其内部原理

开发提速

  • 1.熟练掌握一种接口管理、接口mock工具的使用,如yapi

  • 2.掌握一种高效的日志埋点方案,可快速使用日志查询工具定位线上问题

  • 3.理解TDDBDD模式,至少会使用一种前端单元测试框架

版本控制

  • 1.理解Git的核心原理、工作流程、和SVN的区别

  • 2.熟练使用常规的Git命令、git rebasegit stash等进阶命令

  • 3.可以快速解决线上分支回滚线上分支错误合并等复杂问题

持续集成

  • 1.理解CI/CD技术的意义,至少熟练掌握一种CI/CD工具的使用,如Jenkins

  • 2.可以独自完成架构设计、技术选型、环境搭建、全流程开发、部署上线等一套完整的开发流程(包括Web应用、移动客户端应用、PC客户端应用、小程序、H5等等)

八、项目和业务

后端技能

  • 1.了解后端的开发方式,在应用程序中的作用,至少会使用一种后端语言

  • 2.掌握数据最终在数据库中是如何落地存储的,能看懂表结构设计、表之间的关联,至少会使用一种数据库

性能优化

  • 1.了解前端性能衡量指标、性能监控要点,掌握一种前端性能监控方案

  • 2.了解常见的WebApp性能优化方案

  • 3.SEO排名规则、SEO优化方案、前后端分离的SEO

  • 4.SSR实现方案、优缺点、及其性能优化

  • 5.Webpack的性能优化方案

  • 6.Canvas性能优化方案

  • 7.ReactVue等框架使用性能优化方案

前端安全

  • 1.XSS攻击的原理、分类、具体案例,前端如何防御

  • 2.CSRF攻击的原理、具体案例,前端如何防御

  • 3.HTTP劫持、页面劫持的原理、防御措施

业务相关

  • 1.能理解所开发项目的整体业务形态、业务目标、业务架构,可以快速定位线上业务问题

  • 2.能理解所开发项目整体的技术架构、能快读的根据新需求进行开发规划、能快速根据业务报警、线上日志等定位并解决线上技术问题

  • 3.可以将自己的想法或新技术在业务中落地实践,尽量在团队中拥有一定的不可替代性

九、学习提升

vczh大神在知乎问题【如何能以后达到温赵轮三位大神的水平?】下的回答:

这十几年我一共做了三件事:

  • 1、不以赚钱为目的选择学习的内容;
  • 2、以自己是否能造出轮子来衡量学习的效果;
  • 3、坚持每天写自己的代码,前10年每天至少6个小时,不包含学习和工作的时间。

上面几点可能有点难,第一点我就做不到,但是做到下面绩点还是比较容易的。

关于写博客说明下,能给别人讲明白的知识会比自己学习掌握的要深刻许多

  • 1.拥有自己的技术博客,或者在一些博客平台上拥有自己的专栏

  • 2.定期的将知识进行总结,不断完善自己的知识体系

  • 3.尽量将自己的知识转换成真实的产出,不要仅仅停留在书面理解层面,更重要的是实际应用

  • 4.坚持输出自己的代码,不要盲目的扎进公司业务

十、技术之外

这部分可能比上面九条加起来重要!

  • 1.了解互联网人员术语:CEOCTOCOOCFOPMQAUIFEDEVDBAOPS

  • 2.了解互联网行业术语:B2BB2CC2CO2O

  • 3.掌握互联网行业沟通、问答、学习的

  • 4.有一定的"PPT"能力

  • 5.有一定的理财意识,至少了解储蓄、货币基金、保险、指数基金、股票等基本的理财知识

  • 6.掌握在繁重的工作和长期的电脑辐射的情况下保持健康的方法,建立正确的养生知识体系

小结

希望你阅读本篇文章后可以达到以下几点:

  • 从知识清单中找到自己的知识盲点与欠缺

  • 具有知识体系化的思想,开始建立自己的知识体系

  • 阅读文章时将知识归类到知识体系中,并不断完善自己的知识体系

  • 从文章中获取到了有用的资源

文中如有错误,欢迎在评论区指正,如果这篇文章帮助到了你,欢迎点赞和关注。

如果你有什么好的知识、资源推荐,欢迎在评论区留言。

MIT Licensed | 沪ICP备20013265号-1 | Copyright © 2019-present AaronKong