1. JavaScript 是单线程的吗?
没错,JavaScript 是 一种 单线程语言。这意味着它只有 一个调用栈和一个内存堆。每次只执行一组指令。
此外,JavaScript 是同步和阻塞 的性质。这意味着代码是逐行执行的,一个任务必须在下一个任务开始之前完成。
然而,JavaScript 也具有异步能力,允许某些操作独立于主执行线程执行。这通常通过回调、Promise、async/await 和事件监听器等机制实现。这些异步特性使 JavaScript 能够处理诸如获取数据、处理用户输入和执行 I/O 操作等任务,而不会阻塞主线程,使其适用于构建响应式和交互式的 Web 应用程序。
2. 解释 JavaScript 引擎的主要组件及其工作原理。
每个浏览器都有一个 JavaScript 引擎,用于执行 JavaScript 代码并将其转换为机器码。
当执行 JavaScript 代码时,解析器首先读取代码并生成 AST,然后将其存储在内存中。解释器接着处理这个 AST 并生成字节码或机器码,计算机执行它。
性能分析器是 JavaScript 引擎的一个组件,监视 代码的执行情况。
字节码与性能分析数据一起被优化编译器使用。 “优化编译器” 或即时 (JIT) 编译器根据性能分析数据做出某些假设,并生成高度优化的机器码。
有时候,“优化” 的假设是不正确的,然后它通过 “取消优化” 阶段(对我们来说实际上会产生开销)回到之前的版本。
JS 引擎通常会优化 “热门函数”,并使用内联缓存技术优化代码。
在此过程中,调用栈跟踪当前正在执行的函数,内存堆用于内存分配。
最后,垃圾收集器开始管理内存,从未使用的对象中回收内存。
Google Chrome 𝗩𝟴 引擎:
- 解释器称为 “Ignition”。
- 优化编译器称为 “TurboFan”。
- 除了解析器外,还有一个 “pre-parser” 用于检查语法和标记。
- 引入了 “Sparkplug”,位于 “Ignition” 和 “TurboFan” 之间,也称为 快速编译器。
3. 解释 JavaScript 中的事件循环。
事件循环是 JavaScript 运行时环境的核心组件。它负责调度和执行异步任务。事件循环通过持续监视两个队列来工作:调用栈和事件队列。
调用栈 是一个 堆栈(后进先出)数据结构,用于存储当前正在执行的函数(存储代码执行期间创建的执行上下文)。
Web API 是异步操作(setTimeout、fetch 请求、Promise)和它们的回调等待完成的地方。它从线程池中借用线程以在后台完成任务,而不会阻塞主线程。
作业队列(或微任务) 是一个 先进先出 结构,其中包含 准备执行的异步/等待、Promise、process.nextTick() 的回调。例如,已完成 Promise 的 resolve 或 reject 回调会排队在作业队列中。
任务队列(或宏任务) 是一个 先进先出 结构,其中包含 准备执行的异步操作的回调(类似于定时器 setInterval、setTimeout)。例如,已超时的 setTimeout()
的回调 - 准备执行 - 会排队在任务队列中。
事件循环持续监视 调用栈是否为空。如果调用栈为空,事件循环会查看作业队列或任务队列,并将准备执行的任何回调 出队到调用栈中执行。
4. JavaScript 中的不同数据类型
JavaScript 是一种动态且松散类型(也称鸭子类型)的语言。这意味着我们不需要指定变量的类型,因为 JavaScript 引擎会根据变量的值动态确定变量的数据类型。
JavaScript 中的原始数据类型是最基本的数据类型,表示单一值。它们是不可变的(无法更改),直接包含特定的值。
在 JavaScript 中,Symbol 是一种原始数据类型,在 ECMAScript 6 (ES6) 中引入,表示唯一且不可变的值。它通常用于作为对象属性的标识符,以避免命名冲突。
const mySymbol = Symbol('key');
const obj = {
[mySymbol]: 'value'
};
当 Symbol 被用作属性键时,它不会与其他属性键(包括字符串键)发生冲突。
5. 什么是回调函数和回调地狱?
在 JavaScript 中,回调通常用于处理异步操作。
回调函数 是一种作为参数传递给另一个函数并在特定任务完成或特定时间执行的函数。
function fetchData(url, callback) {
// 模拟从服务器获取数据
setTimeout(() => {
const data = 'Some data from the server';
callback(data);
}, 1000);
}
function processData(data) {
console.log('Processing data:', data);
}
fetchData('https://example.com/data', processData);
在这个例子中,fetchData
函数接受一个 URL 和一个回调函数作为参数。在从服务器获取数据后(使用 setTimeout
模拟),它调用回调函数并将检索到的数据传递给它。
回调地狱,也称为**“金字塔地狱”**,是指在 JavaScript 编程中,当多个嵌套回调用于异步函数时,会产生代码难以阅读和维护的情况。
“它发生在异步操作依赖于先前异步操作的结果时,导致深度嵌套且难以阅读的代码。”
回调地狱是一种反模式,包含多个嵌套回调,这使得处理异步逻辑的代码难以阅读和调试。
fs.readFile('file1.txt', 'utf8', function (err, data) {
if (err) {
console.error(err);
} else {
fs.readFile('file2.txt', 'utf8', function (err, data) {
if (err) {
console.error(err);
} else {
fs.readFile('file3.txt', 'utf8', function (err, data) {
if (err) {
console.error(err);
} else {
// 继续更多的嵌套回调...
}
});
}
});
}
});
在这个例子中,我们使用 fs.readFile
函数顺序读取三个文件,每个文件读取操作都是异步的。结果,我们不得不将回调嵌套在一起,形成一个回调金字塔。
为了避免回调地狱,现代 JavaScript 提供了像 Promise 和 async/await 这样的替代方案。 这里是使用 Promise 的相同代码:
const readFile = (file) => {
return new Promise((resolve, reject) => {
fs.readFile(file, 'utf8', (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
};
readFile('file1.txt')
.then((data1) => {
return readFile('file2.txt');
})
.then((data2) => {
return readFile('file3.txt');
})
.then((data3) => {
// 继续处理更多基于 promise 的代码...
})
.catch((err) => {
console.error(err);
});
6. 什么是 Promise 和 Promise 链?
Promise: Promise 是 JavaScript 中用于异步计算的对象。它代表一个异步操作的结果,这个结果可能是已解决或已拒绝的。
Promise 有三种状态:
- Pending(等待中):初始状态。在此状态下,Promise 的最终值尚不可用。
- Fulfilled(已解决):Promise 已成功解决,最终值现在可用。
- Rejected(已拒绝):Promise 遇到错误或被拒绝,最终值不可用。
Promise 构造函数 有两个参数 (resolve, reject),它们是函数。如果异步任务没有错误地完成,则调用 resolve
函数并传递消息或获取的数据以解决 promise。如果发生错误,则调用 reject
函数并传递错误信息。
我们可以使用 .then()
方法来访问 promise 的结果,并且可以使用 .catch()
方法来捕获错误。
// 创建一个 Promise
const fetchData = new Promise((resolve, reject) => {
// 模拟从服务器获取数据
setTimeout(() => {
const data = 'Some data from the server';
// 使用获取的数据解决 Promise
resolve(data);
// 使用错误信息拒绝 Promise
// reject(new Error('Failed to fetch data'));
}, 1000);
});
// 使用 Promise
fetchData
.then((data) => {
console.log('Data fetched:', data);
})
.catch((error) => {
console.error('Error fetching data:', error);
});
Promise 链:执行一系列异步任务的方法,通过使用 promise 按顺序一个接一个地执行异步任务,称为 Promise 链。
它涉及将多个 .then()
方法链接到一个 Promise 以按特定顺序执行一系列任务。
new Promise(function (resolve, reject) {
setTimeout(() => resolve(1), 1000);
})
.then(function (result) {
console.log(result); // 1
return result * 2;
})
.then(function (result) {
console.log(result); // 2
return result * 3;
})
.then(function (result) {
console.log(result); // 6
return result * 4;
});
7. 什么是 async/await?
Async/await 是处理 JavaScript 中异步代码的现代方法。它提供了一种更简洁和可读的方式来处理 Promise 和异步操作,有效地避免了“回调地狱”,并改善了异步代码的整体结构。
在 JavaScript 中,async 关键字用于定义一个异步函数,该函数返回一个 Promise。
在异步函数中,await 关键字用于暂停函数的执行,直到 Promise 被解决,有效地使代码看起来像是同步的,同时处理异步操作。
async function fetchData() {
try {
const data = await fetch('https://example.com/data');
const jsonData = await data.json();
return jsonData;
} catch (error) {
throw error;
}
}
// 使用异步函数
fetchData()
.then((jsonData) => {
// 处理获取的数据
})
.catch((error) => {
// 处理错误
});
在这个例子中,fetchData
函数被定义为一个异步函数,它使用 await
关键字暂停执行并等待 fetch
和 json
操作,实际工作时像处理同步代码一样处理 Promise。
8. ==
和 ===
运算符有啥区别?
==
(宽松相等运算符):这家伙会进行类型转换,意味着在比较之前会把操作数转化为相同的类型。它只检查值是否相等,而不考虑它们的数据类型。比如,1 == '1'
会返回 true
,因为 JavaScript 在比较前会把字符串 '1'
转换为数字。
===
(严格相等运算符):这个就讲究多了,不进行类型转换,直接比较。它不仅看值是否相等,还要求数据类型一样。所以 1 === '1'
就会返回 false
,毕竟一个是数字,另一个是字符串嘛。
简单说,==
比较时会“睁一只眼闭一只眼”,而 ===
则是严格的“一视同仁”,连类型都得一样。
执行效率上,==
相比 ===
会快一点点哦。
几个例子帮你感受下这俩的区别:
0 == false // true
0 === false // false
1 == "1" // true
1 === "1" // false
null == undefined // true
null === undefined // false
'0' == false // true
'0' === false // false
[]==[] 或者 []===[] // 都是 false,因为指向不同的内存对象
{}=={} 或者 {}==={} // 同样都是 false,每个花括号都代表独一无二的对象
9. 在 JavaScript 中创建对象有几种方法?
JavaScript 创建对象的方法多着呢,这里给你数数常见的几种:
a) 对象字面量:最直接的方式,用花括号 {}
把属性和方法一股脑儿包起来。
let person = {
firstName: '约翰',
lastName: '杜',
greet: function() {
return '你好,' + this.firstName + ' ' + this.lastName;
}
};
b) 构造函数:用 new
关键字调用构造函数,可以创建多个实例,属性和方法通过 this
分配。
function Person(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
this.greet = function() {
return '你好,' + this.firstName + ' ' + this.lastName;
};
}
let person1 = new Person('约翰', '杜');
let person2 = new Person('简', '史密斯');
c) Object.create()
:这个方法允许你指定一个原型对象来创建新对象,对原型控制更精细。
let personProto = {
greet: function() {
return '你好,' + this.firstName + ' ' + this.lastName;
}
};
let person = Object.create(personProto);
person.firstName = '约翰';
person.lastName = '杜';
d) ES6 的 Class 语法:新潮的 ES6 引入了类的概念,用 class
关键字定义对象和方法,看起来更正规了。
class Person {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
greet() {
return '你好,' + this.firstName + ' ' + this.lastName;
}
}
let person = new Person('约翰', '杜');
e) 工厂函数:这类函数能“生产”对象,返回一个对象实例,方便定制属性。
function createPerson(firstName, lastName) {
return {
firstName: firstName,
lastName: lastName,
greet: function() {
return '你好,' + this.firstName + ' ' + this.lastName;
}
};
}
let person1 = createPerson('约翰', '杜');
let person2 = createPerson('简', '史密斯');
f) Object.setPrototypeOf()
:这个方法能给已存在的对象设置原型,提供另一种调整原型链的方式。
let personProto = {
greet: function() {
return '你好,' + this.firstName + ' ' + this.lastName;
}
};
let person = {};
person.firstName = '约翰';
person.lastName = '杜';
Object.setPrototypeOf(person, personProto);
g) Object.assign()
:通过拷贝源对象的所有可枚举自有属性到目标对象,实现对象合并或浅拷贝。
let target = { a: 1, b: 2 };
let source = { b: 3, c: 4 };
let mergedObject = Object.assign({}, target, source);
h) 原型继承:JavaScript 的精髓之一,通过构造函数的 .prototype
属性或类定义共享行为。
function Animal(name) {
this.name = name;
}
Animal.prototype.greet = function() {
return '你好,我是 ' + this.name;
};
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
let myDog = new Dog('麦克斯', '贵宾犬');
i) 单例模式:保证一个类只有一个实例,常用闭包和立即执行函数表达式(IIFE)实现。
let singleton = (() => {
let instance;
function createInstance() {
return {
// 属性和方法
};
}
return {
getInstance: () => {
if (!instance) {
instance = createInstance();
}
return instance;
}
};
})();
10. 什么是展开(Spread)和剩余(Rest)运算符?
剩余运算符(...
),在函数参数列表中使用时,可以把一堆传入的参数收集到一个数组里。这样,你就可以向函数传递任意数量的参数,无需一一列出它们的名字。
function 加和(...数字们) {
return 数字们.reduce((总数, 当前数) => 总数 + 当前数, 0);
}
console.log(加和(1, 2, 3, 4)); // 输出 10
展开运算符,同样用三个点(...
),不过这次是把数组或对象的元素“拆开”到另一个数组或对象里。有了它,复制数组、拼接数组、合并对象都变得轻而易举。
const 数组1 = [1, 2, 3];
const 数组2 = [4, 5, 6];
const 合并数组 = [...数组1, ...数组2];
// 合并数组是 [1, 2, 3, 4, 5, 6]
const 对象1 = { a: 1, b: 2 };
const 对象2 = { b: 3, c: 4 };
const 合并对象 = { ...对象1, ...对象2 };
// 合并对象是 { a: 1, b: 3, c: 4 }
11. 高阶函数是啥?
高阶函数在 JavaScript 中,就是要么接受一个或多个函数作为参数,要么返回一个函数作为结果,或者两者兼备。说白了,它处理的就是函数本身,要么把函数当“食材”用,要么产出函数这个“成品”。
function 数组操作(数组, 处理函数) {
let 结果 = [];
for (let 元素 of 数组) {
结果.push(处理函数(元素));
}
return 结果;
}
function 乘以二(x) {
return x * 2;
}
let 数字们 = [1, 2, 3, 4];
let 两倍数字们 = 数组操作(数字们, 乘以二);
console.log(两倍数字们); // 输出 [2, 4, 6, 8]
它们使得函数组合、柯里化、基于回调的异步操作成为可能,是写出优雅且函数式风格 JavaScript 代码的关键。
单参数函数(即一元函数),是指恰好接收一个参数的函数。就像它的名字那样,独来独往,只接受一个“客人”。
总结
今天,咱们主要学习了一些关于Javascript的基础知识,设计基本的事件循环、如何进行异步编程,解释了JavaScript中==
与===
的区别、创建对象的多种方法、剩余与展开运算符的用法,以及高阶函数的概念。