JS随笔
本文主要记录一些关于JS的起源、应用场景、特性、属性、方法和一些底层的原理,供自己以后查漏补缺,也欢迎同道朋友交流学习。
介绍
起源
JavaScript
在 1995 年由网景公司( Netscape
)的布兰登·艾奇( Brendan Eich
)创造,最初命名为 Mocha
,后来改为 LiveScript
,最终定名为 JavaScript
。它最初是为了使网页更具交互性而设计的,现在已经成为 Web
开发领域不可或缺的一部分,并且随着时间的发展,其应用范围远远超出了浏览器。
应用场景
- Web前端开发:通过
JS
,开发者可以创建动态的 Web 页面,如交互式表单、动态内容更新、动画效果等。 - Web后端开发:借助
Node.js
,开发者可以在服务器端
编写服务端逻辑。 - 桌面应用程序:通过
Electron
等框架,可以开发跨平台
的桌面应用
。 - 移动应用开发:利用
React Native
、Flutter
、Ionic
、UniApp
等框架,可以开发原生移动应用
。 - 物联网(IoT)开发:使用
WebSockets
、WebRTC
、Service Workers
可以与物联网设备进行交互,也可以使用D3.js
、Chart.js
进行数据可视化开发。 - 游戏开发:尽管 JS 不是主流游戏开发语言,但在
H5
和微信小游戏领域基于cocos
也有丰富的应用。
JS特性
- 基于原型:对象可以通过其他对象继承属性和方法。
- 动态类型:变量类型可以根据赋值自动改变。
- 弱类型:比较运算符会自动转换数据类型。
- 垃圾回收:支持自动内存管理(
垃圾回收
)。 - 异步编程:支持异步编程模式,如回调函数、Promises、async/await等,适合处理如用户界面响应、网络请求等异步事件。
常用库和框架
- 三大框架:
React
、Vue
、Angular
- 常用工具库:
Axios
、Moment.js
、Lodash
、jQuery
- 图形可视化:
D3.js
、AntV
、Echarts
、Three.js
- 路由库:
React Router
、Vue Router
、Angular Router
- 状态库:
Redux
、MobX
、Vuex
变量与数据类型
变量声明
使用 var、let 或 const 关键字声明变量。var
作用域为函数或全局,而 let
和 const
提供了块级作用域。
var x = 8;
let y = 11;
const z = 33;
数据类型
JS基本类型有7种,分别是:
- 空值(
null
) - 未定义(
undefined
) - 布尔值(
boolean
) - 数字(
number
) - 字符串(
string
) - 对象(
object
) - 符号(
symbol
,ES6 中新增 )
其中 object
是复合类型,包括很多内置对象:
String
Number
Boolean
Object
Function
Array
Date
RegExp
Error
tips:
null
有时会被当作一种对象类型,但是这其实只是语言本身的一个bug
, 即对 null 执行typeof null
时会返回字符串object
。实际上,null 本身是基本类型
。
操作运算符
JS 提供了一系列操作运算符来对数据进行操作。以下是常用的操作运算符:
- 算术操作运算符:
+
,-
,*
,/
,%
,**
(指数) - 赋值操作运算符:
=
,+=
,-=
,*=
,/=
,%=
- 比较操作运算符:
==
,===
,!=
,!==
,>
,<
,>=
,<=
- 逻辑操作运算符:
&&
,||
,!
表达式
在 JS 中,表达式是计算并产生值的代码片段。表达式可以简单到只是一个值,也可以复杂到包含多个操作符
和函数调用
。以下是JS 中常见的几种表达式类型:
-
字面量表达式:直接赋予值的表达式,如数值、字符串、布尔值、对象、数组、函数、undefined和null。
let age = 30; // 数字字面量 let name = "Tom"; // 字符串字面量 let flag = true; // 布尔字面量 let now = undefined; // undefined字面量 let nothing = null; // null字面量
-
变量表达式:使用变量名来引用存储的值。
let x = 12; let y = x; // y 的值现在是 12
-
算术表达式:使用算术运算符进行计算。
let sum = 3 + 8; // 11 let difference = 3 - 2; // 1
-
赋值表达式:将一个值赋给变量。
let score = 22; // 将 22 赋值给变量 score
-
函数调用表达式:调用函数并返回结果。
let result = Math.max(1, 2); // 返回 2
-
对象和数组表达式:创建对象或数组。
let person = { name: "Jack", age: 30 }; // 对象字面量 let numbers = [1, 2, 3, 4]; // 数组字面量
-
逻辑表达式:使用逻辑运算符进行布尔逻辑运算。
let isEnable = age >= 30 && flag; // 逻辑与
-
条件(三元)表达式:基于条件进行值的选择。
let status = age >= 30 ? "Adult" : "Minor"; // 如果 age 大于或等于 30 是 "Adult",否则是 "Minor"
-
逗号表达式:顺序执行多个表达式,并返回最后一个表达式的值。
let x = (console.log("First"), 8); // "First" 被打印到控制台,x 的值是 8
控制结构
JS 控制结构用于管理代码的流程,包括条件判断和循环等。以下是常见的控制结构
-
条件语句:
// if else 语句 if (condition1) { // 第一个条件为真时执行的代码 } else if (condition2) { // 第二个条件为真时执行的代码 } else { // 所有条件都不为真时执行的代码 } // switch 语句 switch (expression) { case value1: // 当 expression 等于 value1 时执行的代码 break; case value2: // 当 expression 等于 value2 时执行的代码 break; default: // 所有条件都不为真时执行的代码 } // 三元表达式 condition为true时返回value1,否则返回value2 let result = condition ? value1 : value2;
-
循环语句(for, while, do-while)
// for 循环 for (let i = 0; i < 10; i++) { // 循环体 打印 0 到 9 console.log(i); } // while 循环 var j = 0; while (condition) { if (j === 5) { // while需要有一个跳出循环的语句 break; } // 循环体 打印 0 到 4 console.log(j); } // do-while 循环 var k = 0; do { if (k === 5) { // do-while需要有一个跳出循环的语句 break; } // 循环体 打印 0 到 4 console.log(k); } while (condition);
-
跳转语句(break, continue, return)
for (let i = 0; i < items.length; i++) { if (someCondition) { break; // 退出循环 } } for (let i = 0; i < items.length; i++) { if (skipCondition) { continue; // 跳过本次迭代 } // 执行其他操作 } function myFunction() { if (condition) { return result; // 返回结果 } // 执行其他操作 }
值的类型转换
在 JS 中我们经常要用一个值和另一个值去比较判断。但是,JS 的值类型并不是固定的,而是根据值的类型来判断的。这种转换可以是隐式的,也可以是显式的。以下是常见的值类型转换:
-
显示转换:通过函数或操作符明确指定类型转换。
// 字符串转数字 let num = Number("123"); // 123 let num = parseInt("123", 10); // 123 // 数字转字符串 let str2 = String(123); // "123" // 数字转布尔值 let num = Boolean(1); // true let num = Boolean(0); // false
-
隐式类型转换:字符串和数字之间的隐式强制类型转换 通过+运算符进行字符串拼接
let result = "5" + 5; // "55" let result = [1, 2]+ [3, 4]; // '1,23,4' 数组调用toString()方法进行隐式转换 let num = null + 1; // 1 `null`在与数字进行算术运算时通常转换为`0` let num = undefined + 1; // NaN `undefined`在与数字进行算术运算时通常转换为`NaN` let num = +"123"; // 123 let num = +"abc"; // NaN 使用`+`操作符可以将值转换为数字,如果转换失败则返回`NaN`
宽松相等(==
)和严格相等(===
)
==
允许在相等比较中进行强制类型转换,而 ===
不允许。不管比较的类型是什么,任何比较的结果都是严格的布尔值(true 或者 false)。
宽松相等:
// 字符串和数字之间的相等比较:
// 如果 Type(x) 是数字,Type(y) 是字符串,则返回 x == ToNumber(y) 的结果
42 == "42"; // true
// 如果 Type(x) 是字符串,Type(y) 是数字,则返回 ToNumber(x) == y 的结
"42" == 42; // true
// 其他类型和布尔类型之间的相等比较
// 如果 Type(x) 是布尔类型,则返回 ToNumber(x) == y 的结果;
true == "42"; // false 因为 true被转换为 1, 等式改为 1 == "42"
// 如果 Type(y) 是布尔类型,则返回 x == ToNumber(y) 的结果。
"42" == true; // false 因为 true被转换为 1, 等式改为 "42" == 1
// null 和 undefined 之间的相等比较
// 如果 x 为 null,y 为 undefined,则结果为 true。
null == undefined; // true
// 如果 x 为 undefined,y 为 null,则结果为 true。
undefined == null; // true
// 对象和非对象之间的相等比较
// 如果 Type(x) 是字符串或数字,Type(y) 是对象,则返回 x == ToPrimitive(y) 的结果
[ 42 ] == 42; // true
// 如果 Type(x) 是对象,Type(y) 是字符串或数字,则返回 ToPromitive(x) == y 的结果
[ 42 ] == 42; // true
// 特殊情况
// 返回其他数字
Number.prototype.valueOf = function() {
return 3;
};
new Number( 2 ) == 3; // true
// 根据 ToBoolean 规则,它会进行布尔值的显式强制类型转换。所以 [] == ![] 变成了 [] == false
[] == ![] // true
JS中“假”值列表:
- ""(空字符串)
- 0、-0、NaN
- null、undefined
- false
假值的相等比较:
"0" == null; // false
"0" == undefined; // false
"0" == false; // true
"0" == NaN; // false
"0" == 0; // true
"0" == ""; // false
false == null; // false
false == undefined; // false
false == NaN; // false
false == 0; // true
false == ""; // true
false == []; // true
false == {}; // false
"" == null; // false
"" == undefined; // false
"" == NaN; // false
"" == 0; // true
"" == []; // true
"" == {}; // false
0 == null; // false
0 == undefined; // false
0 == NaN; // false
0 == []; // true
0 == {}; // false
数组
Array
是一种非常灵活的数据结构,可以用来存储一系列的元素。以下是 JS 数组的一些关键特性和操作方法:
特性
- 动态大小:数组的长度可以根据需要动态变化。
- 类型无关:数组可以存储任何类型的数据。
- 索引:数组元素通过索引访问,索引从 0 开始。
数组的基础操作
// 创建数组
let arr = []; // 声明一个空数组
let numbers = [1, 2, 3, 4, 5]; // 使用数组字面量声明并初始化数组
// 访问数组元素
let element = numbers[0]; // 获取第一个元素
// 修改数组元素
numbers[0] = 10; // 修改第一个元素
// 数组的length属性
let length = numbers.length; // 获取数组的长度
numbers.length = 3; // 将数组长度修改为3,数组被截断
// 栈方法(后进先出,LIFO)
// `push()`:在数组末尾添加一个或多个元素,并返回新的长度。
// `pop()`:移除数组的最后一个元素,并返回该元素。
numbers.push(6); // [1, 2, 3, 4, 5, 6]
let lastElement = numbers.pop(); // [1, 2, 3, 4, 5], lastElement 是 6
// 队列方法(先进先出,FIFO)
// `shift()`:移除数组的第一个元素,并返回该元素。
// `unshift()`:在数组开头添加一个或多个元素,并返回新的长度。
let firstElement = numbers.shift(); // [2, 3, 4, 5]
numbers.unshift(0); // [0, 2, 3, 4, 5]
// 排序方法
// `sort()`:对数组元素进行排序,默认按Unicode字符点排序。
// `reverse()`:反转数组元素的顺序。
numbers.sort(); // 默认排序
numbers.sort((a, b) => a - b); // 按升序排序数字
numbers.reverse(); // 反转数组
// 搜索方法
// `indexOf()`:返回数组中元素第一次出现的索引,如果不存在则返回-1。
// `lastIndexOf()`:返回数组中元素最后一次出现的索引。
let index = numbers.indexOf(3); // 2
let lastIndex = numbers.lastIndexOf(3); // 2
// 迭代方法
// `forEach()`:对数组的每个元素执行一次提供的函数。
// `map()`:创建一个新数组,其元素是调用一次提供的函数后的返回值。
// `filter()`:创建一个新数组,包含通过测试的所有元素。
// `reduce()`:将数组元素累加到一个值。
numbers.forEach((item) => console.log(item));
let squares = numbers.map((item) => item * item);
let evenNumbers = numbers.filter((item) => item % 2 === 0);
let sum = numbers.reduce((accumulator, currentValue) => accumulator + currentValue);
一些其他有用的数组方法
slice()
:返回一个新数组,包含从开始到结束(不包括结束)的数组的一部分。splice()
:添加或删除数组元素,修改原数组。includes()
:用来判断数组是否包含某个元素。find()
:返回数组中满足提供的测试函数的第一个元素的值。some()
:测试数组中是不是至少有一个元素通过了被提供的函数测试。every()
:测试数组的所有元素是否都通过了被提供的函数测试。
对象
JS
对象是键值对
的集合
,其中键是字符串(或者符号,Symbol)
,值可以是任意数据类型
。对象在 JS 中用于结构化和存储数据。以下是一些关于 JS 对象的关键概念和用法:
对象的基本操作方法
// 创建对象
// **字面量方式**:使用大括号`{}`创建新对象。
// **构造函数**:使用`new Object()`创建对象。
// **对象创建方法**:如`Object.create()`
let person = { name: 'Tom', age: 30 }; // 字面量方式
let person2 = new Object(); // 构造函数方式
let person3 = Object.create({ name: 'Tom', age: 30 }); // Object.create()
// 访问对象属性
let name = person.name; // 点符号
let age = person['age']; // 方括号
// 修改和添加属性
person.name = 'Tom'; // 修改属性
person.email = 'Tom@example.com'; // 添加属性
// 删除属性
delete person.email; // 删除email属性
// 枚举属性
// 使用`for...in`循环遍历对象的所有可枚举属性。
for (let key in person) {
console.log(key + ': ' + person[key]);
}
// 判断属性是否存在
if ('name' in person) {
console.log('Name exists');
}
// 获取属性值
let values = Object.values(person); // ["Tom", 30]
// 获取属性键
let keys = Object.keys(person); // ["name", "age"]
// 获取属性描述
// 使用`Object.getOwnPropertyDescriptor()`获取属性的描述信息
let description = Object.getOwnPropertyDescriptor(person, 'name');
// 检查对象是否拥有特定类型
// 使用`instanceof`操作符。
let car = {};
if (car instanceof Object) {
console.log('car is an Object');
}
属性特性
- 属性可以有特性,如是否可写、可枚举、可配置。
Object.defineProperty(person, 'age', {
value: 25,
writable: false,
enumerable: true,
configurable: false
});
拷贝对象
在 JS 中拷贝对象是一个常见的需求,但拷贝的方式会根据你想要实现的拷贝深度(浅拷贝
或深拷贝
)和对象内容的复杂性而有所不同。以下是一些常用的拷贝对象的方法:
1. 浅拷贝(Shallow Copy)
浅拷贝
只会复制对象的第一层属性
,而对象内部的引用类型
属性仍然指向原对象的内部数据。
// 使用对象字面量
let original = { name: 'Alice', age: 25 };
// 使用扩展运算符(Spread Operator)
let shallowCopy = { ...original };
// 使用`Object.assign()`方法
let shallowCopy = Object.assign({}, original);
// 数组的slice方法
let copiedArray = originalArray.slice();
2. 深拷贝(Deep Copy)
深拷贝
会递归
地复制对象的所有层级,使得新对象与原对象完全隔离,不存在引用共享。
// 使用`JSON.parse()`和`JSON.stringify()`
// 这种方法不能拷贝函数、undefined、循环引用的对象等
let deepCopy = JSON.parse(JSON.stringify(original));
// 使用第三方库,如`lodash`的`cloneDeep`方法
import { cloneDeep } from 'lodash';
let deepCopy = cloneDeep(original);
手动实现深拷贝函数
function deepCopy(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
let temp = new obj.constructor(); // 对于对象来说,这将创建一个新对象
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
temp[key] = deepCopy(obj[key]);
}
}
return temp;
}
let deepCopyObj = deepCopy(original);
注意事项
- 拷贝包含函数、日期、正则表达式、错误等特殊对象时,需要特别注意,因为这些类型的对象拷贝后可能不会保留原有行为。
- 对于包含循环引用的对象,
JSON.parse()
和JSON.stringify()
方法将无法正确拷贝。 - 深拷贝可能涉及到性能问题,特别是当对象非常大或非常复杂时。
DOM
JavaScript (JS) 中的 DOM(文档对象模型)API 是一种编程接口,允许程序和脚本动态地访问和更新文档的内容、结构和样式。以下是一些常用的 DOM 操作 API:
-
获取元素
document.getElementById(id)
: 通过元素的ID
获取元素
。document.getElementsByTagName(name)
: 通过标签名
获取元素集合
。document.getElementsByClassName(names)
: 通过类名
获取元素集合
。document.querySelector(selector)
: 根据CSS选择器
获取第一个匹配的元素
。document.querySelectorAll(selector)
: 根据CSS选择器
获取所有匹配的元素集合
。
-
创建和插入元素
document.createElement(tagName)
: 创建一个新的元素节点
。element.appendChild(node)
: 将一个节点添加
到元素的子节点列表的末尾
。element.insertBefore(newNode, referenceNode)
: 在参考节点前插入
一个新节点。element.replaceChild(newNode, oldNode)
:替换
元素的子节点。
-
删除元素
element.removeChild(node)
: 从元素中移除
一个子节点。element.remove()
:移除元素自身
。
-
属性操作
element.getAttribute(name)
:获取
元素的属性值。element.setAttribute(name, value)
:设置或修改
元素的属性值。element.removeAttribute(name)
:移除
元素的属性。
-
样式操作
element.style.property
: 直接设置元素的内联样式
。element.classList.add(className)
: 向元素添加
一个类。element.classList.remove(className)
: 从元素移除
一个类。element.classList.toggle(className)
:切换
元素的类。
-
文本操作
element.textContent
: 获取或设置元素的文本内容
。element.innerText
: 获取或设置元素的文本内容,但不包括子元素的文本。
-
事件监听
element.addEventListener(type, listener[, options])
: 为元素添加事件监听器
。element.removeEventListener(type, listener[, options])
:移除
元素的事件监听器。
-
遍历DOM树
element.parentNode
: 获取元素的父元素。element.childNodes
: 获取元素的所有子节点。element.firstChild
: 获取元素的第一个子节点。element.lastChild
: 获取元素的最后一个子节点。element.nextSibling
: 获取元素的下一个兄弟节点。element.previousSibling
: 获取元素的上一个兄弟节点。
-
滚动操作
window.scroll(x, y)
: 滚动到页面的指定位置。window.scrollTo(options)
: 滚动到页面的指定位置,可以是坐标或元素。
-
尺寸和位置
element.offsetWidth
: 获取元素的布局宽度,包括边框和内边距。element.offsetHeight
: 获取元素的布局高度,包括边框和内边距。element.getBoundingClientRect()
: 获取元素的大小及其相对于视口的位置。
事件委托
事件委托
是 JS 中一种常用的技术,它利用了事件冒泡
的原理来处理事件。事件冒泡是指当一个事件在 DOM 树中被触发时,它会从事件的目标元素开始,逐级向上
传播到文档的根元素。利用这个特性,我们可以在父元素上设置监听器来处理子元素的事件,从而减少事件监听器的数量,提高性能
。
以下是使用事件委托的一些示例:
// 假设我们有一个列表,列表项的类名为 'list-item'
var list = document.getElementById('myList');
// 我们在列表的父元素上设置一个点击事件监听器
list.addEventListener('click', function(event) {
// 检查点击的元素是否是我们的列表项
if (event?.target?.className === 'list-item') {
// 执行操作,比如改变列表项的文本
event.target.textContent = 'Clicked!';
}
});
在这个例子中,无论列表有多少项,我们都只需要一个事件监听器。当用户点击任何一个列表项时,事件会冒泡到列表的父元素,然后我们的事件处理函数会检查 event.target
是否是我们想要的列表项,并执行相应的操作。
事件委托的优势包括:
- 性能优化:减少事件监听器的数量,特别是对于动态生成的内容。
- 代码简洁:简化事件处理逻辑,避免为每个元素单独绑定事件。
- 动态内容:适用于动态添加到 DOM 的元素,无需为新元素重新绑定事件。
事件委托是处理大量元素事件的一种高效方法,特别是在处理具有重复模式的列表或表格时。
BOM
BOM(Browser Object Model
,浏览器对象模型)是 Web 浏览器提供的一套 API,它允许 JS 与浏览器及其文档进行交互。BOM
主要由以下几个核心对象组成:
- window:
window
对象代表浏览器窗口,是 BOM 的核心,包含了与浏览器窗口
相关的属性和方法。 - location:
window.location
对象提供了与当前窗口中加载的文档有关的信息,如URL
、协议
、主机名
等。 - navigator:
window.navigator
对象提供了关于浏览器
的信息,包括浏览器类型
、版本
、操作
系统等。 - history:
window.history
对象保存了浏览器的历史记录,允许网页通过 JS 进行前进
、后退
等操作。 - screen:
window.screen
对象提供了有关用户屏幕的信息,如屏幕的宽度
、高度
、颜色
等。 - document:
window.document
对象是DOM
的顶级节点,代表了整个 HTML 文档,可以通过它来操作页面内容。 - console:
window.console
对象提供了一组用于向浏览器控制台输出信息的方法,如console.log()
、console.error()
等。 - setTimeout 和 setInterval:用于设置
定时器
,分别用于在指定的时间后执行代码和每隔指定的时间重复执行代码。 - fetch:
window.fetch
方法用于发起 HTTP 请求,是XMLHttpRequest
的现代替代品。 - XMLHttpRequest:用于在 JS 中发起
HTTP
请求,与服务器交换数据。 - localStorage 和 sessionStorage:提供了一种在用户浏览器中存储数据的方式,
localStorage
数据没有过期时间,而sessionStorage
数据在页面会话结束时会被清除。 - indexedDB:是一个运行在浏览器中的非关系型数据库,用于存储大量
结构化数据
。 - Web Workers:允许 JS 脚本在后台线程中运行,不干扰用户界面的响应。
- 通知 API(现代浏览器中):用于显示桌面或移动设备的通知。
函数
函数
是一段可重复使用
的代码块,用于执行一个特定的任务。以下是 JS 中一些重要的函数概念和特性:
函数声明(Function Declaration)
// 函数声明(Function Declaration)
function functionName(params) {
// 代码
}
// 函数表达式(Function Expression)
const functionName = function(parameters) {
// 代码
};
// 立即执行函数表达式(IIFE - Immediately Invoked Function Expression)
(function() {
// 代码
})();
// 回调函数(Callback Function)
function someFunction(callback) {
// 某些操作后调用回调函数
callback();
}
someFunction(function() {
console.log('回调函数被调用');
});
闭包(Closure)
函数可以记住
并访问
其创建时的作用域
,即使该作用域已经执行完毕。
function createClosure() {
let secret = "I am Tom";
return function() {
console.log(secret);
};
}
const myClosure = createClosure();
myClosure(); // 输出: I am Tom
闭包是 JavaScript 中一个强大的特性,它提供了许多优势,但同时也带来了一些潜在的缺点。
优点:
- 封装性:闭包提供了一种将数据和功能封装在一起的方式,有助于创建
模块化
和可重用
的代码。 - 数据隐藏:闭包允许创建
私有变量
,这些变量不能从外部直接访问,从而保护了数据的完整性。 - 状态维护:闭包可以在函数调用之间
保持状态
,这对于需要持续跟踪状态的应用程序非常有用。 - 独立作用域:闭包提供了独立的作用域,有助于
避免全局命名冲突
。 - 记忆功能:闭包可以
记住之前的计算结果
,有助于实现记忆化,减少重复计算,提高性能。 - 函数工厂:闭包可以创建
函数工厂
,即返回另一个函数的函数,这有助于创建具有特定行为的定制函数。 - 异步编程:闭包在异步编程中非常有用,尤其是在处理回调函数时,可以
捕获
和维护
异步操作的状态。 - 事件处理:在
事件处理
程序中,闭包确保回调函数能够访问正确的数据,即使在事件发生时外部函数已经执行完毕。
缺点:
- 内存消耗:由于闭包会持续访问其外部作用域的变量,这可能导致
内存消耗增加
,因为相关变量不能被垃圾回收器回收
。 - 性能问题:在某些情况下,如果不当使用闭包,可能会导致
性能问题
,尤其是在创建大量闭包或在循环中使用闭包
时。 - 复杂性:对于初学者来说,闭包可能会增加代码的
复杂性
,因为它们的行为可能不如普通函数直观。 - 意外的行为:如果开发者对闭包的理解不够深入,可能会导致一些
意外的行为
,比如在循环中使用闭包时产生的意外结果。 - 调试困难:闭包可能会使调试变得更加困难,因为它们的
作用域链
可能不容易追踪
。 - 依赖管理:在某些情况下,闭包可能会隐藏函数之间的依赖关系,使得代码的理解和
维护变得更加困难
。 - 全局变量的使用:如果不当使用闭包,可能会无意中创建全局变量,这与使用闭包的初衷相违背。
函数提升(Hoisting)
在 JavaScript 中,函数和变量声明会被提升到它们所在作用域的顶部。
console.log(hello()); // 无错误,输出 "Hello, World!"
function hello() {
console.log("Hello, World!");
}
作用域(Scope)
作用域(Scope)是指变量和函数的可见性上下文,决定了代码中变量和其他资源的可访问性。JS 有全局作用域和局部作用域,函数可以创建局部作用域,后面原理会详细讲解作用域。
递归(Recursion)
函数调用自身,用于解决可以分解为相似子问题的问题。
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}
ES6的使用
ECMAScript 6
(简称 ES6)是 JavaScript 语言的下一代标准,于 2015 年正式被 ECMA 国际组织定为正式标准,它引入了许多新特性,极大地提高了语言的表达能力,并使得 JavaScript 编程更加模块化、简洁和功能丰富。以下是一些 ES6 的主要使用特性:
// let 和 const
// `let` 和 `const` 声明的变量具有块级作用域,`const` 用于声明常量。
for (let i = 0; i < 5; i++) {
// ...
}
// i 在这里是不可访问的
// 箭头函数(Arrow Functions)
// 箭头函数提供了一种更简洁的函数书写方式,并能自动绑定 `this`
// 没有 arguments 对象, 不适用于构造函数, 不绑定 call、apply 和 bind 方法
const arrowFn = () => { ... };
// 模板字符串(Template Literals)
// 允许在字符串中通过 `${expression}` 嵌入表达式
console.log(`Hello, ${name}!`);
// 解构赋值(Destructuring Assignment)
// 允许从数组或对象中提取值,并赋值给新的变量。
const [a, b] = [1, 2]; // 数组解构
const { name, age } = { name: 'Tom', age: 1 }; // 对象解构
// 默认参数值
// 函数参数可以指定默认值,当调用函数时参数被省略时使用
function greet(name = 'World') {
console.log(`Hello, ${name}!`);
}
// 剩余参数和展开运算符
function sum(...numbers) {
return numbers.reduce((a, b) => a + b, 0);
}
// 展开运算符
const [a, b, ...rest] = [1, 2, 3, 4];
模块(Modules)
ES6 引入了模块的概念,允许开发者将代码划分为可重用的模块。
// math.js 模块
export function add(x, y) {
return x + y;
}
// app.js
import { add } from './math.js';
console.log(add(1, 2));
类(Class)
使用 class
关键字定义类和构造函数,可以更易读地实现面向对象编程。
class Person {
constructor(name) {
this.name = name;
}
greet() {
console.log(`Hello, ${this.name}!`);
}
}
const tom = new Person('Tom');
tom.greet();
Promise
Promise 是异步编程的一种解决方案,用于表示一个尚未完成但预期将来会完成的操作。
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Data');
}, 1000);
});
promise.then(data => console.log(data));
Symbol
Symbol
是 ES6 新增的数据类型,提供了一个唯一的、不可变的数据类型作为对象属性的键。
const mySymbol = Symbol('mySymbol');
const obj = {
[mySymbol]: 'Only one key'
};
Proxy 和 Reflect
Proxy
用于创建一个对象的代理,从而可以拦截和定义基本操作的自定义行为。Reflect
提供了拦截 JavaScript 操作的方法。
Map 和 Set
Map
和 Set
是新的集合类型,Map
是键值对的集合,Set
是值的集合,它们都支持更复杂的数据类型作为键。
迭代器(Iterator)和生成器(Generator)
迭代器允许遍历容器对象中的数据,生成器是一种特殊的函数,可以返回一个迭代器。
Async/Await
使用 async await 来进行异步转同步操作
async function main() {
var ret = await step1();
var ret2 = await step2( ret );
}
编译原理
JS 是一种解释型语言
,其代码通常在运行时由 JS 引擎编译和执行。尽管 JS 不需要传统意义上的编译过程(如编译成机器码
),但它仍然涉及几个关键步骤,这些步骤可以被视为编译原理的一部分:
- 词法分析(Lexical Analysis):将
源代码字符串
分解成一个个的词素(tokens
),如关键字
、变量名
、操作符
等。 - 语法分析(Parsing):将词素序列转换成一个由语法规则定义的抽象语法树(
AST
)。AST 表示了代码的层次结构
。 - 作用域分析(Scope Analysis):确定代码中变量和函数的
作用域
,以及它们是如何互相关联的。 - 类型推断(Type Inference):尽管 JS 是一种动态类型语言,但编译器可以进行
类型推断
以优化代码执行。 - 优化(Optimization):编译器尝试优化
AST
,以提高代码执行效率。这可能包括死代码消除
、循环优化
等。 - 代码生成(Code Generation):将优化后的 AST 转换成可执行代码。这通常意味着转换成字节码或其他中间形式,然后由 JS 引擎执行。
- 即时编译(Just-In-Time Compilation, JIT):在运行时,
JIT
编译器将热点代码(经常执行的代码)编译成机器码
,以提高执行效率。 - 内存管理:编译器需要与
垃圾收集器
协同工作,以管理内存分配
和回收
。 - 异常处理:编译器生成的代码需要能够处理运行时异常,并在错误发生时提供有用的调试信息。
- 模块加载:编译器需要理解
ES6
模块系统,以及如何处理模块的导入
和导出
。
作用域
作用域是指变量和函数的可访问性范围
。在 JS 中,作用域的规则主要基于词法作用域
(lexical scoping),即变量的作用域
由它们在代码中的位置
决定。以下是 JS 作用域的一些关键概念:
- 全局作用域:在程序的最外层定义的变量,它们在整个程序范围内都可见。
- 局部作用域:在函数或代码块内部定义的变量,它们只在该函数或代码块内部可见。
- 词法作用域:JS 引擎根据
变量
和函数声明
的位置
来确定它们的作用域,而不是它们的调用位置。 - 提升(Hoisting):变量和函数声明会被提升到它们所在作用域的顶部。但是,只有声明部分被
提升
,初始化部分不会。 - 函数作用域:使用
function
关键字声明的函数创建自己的作用域。 - 块级作用域:使用
let
和const
关键字声明的变量具有块级作用域,即它们只在定义它们的{}
块内可见。 - 作用域链:当访问一个变量时,JS 引擎首先在当前作用域中查找,如果没有找到,就会向上查找外层作用域,直到全局作用域。这个链式结构就是作用域链。
- 闭包(Closure):函数可以访问创建时的作用域中的变量,即使该函数在其原始作用域之外被调用。
- 变量遮蔽(Variable Shadowing):内层作用域中的变量可以与外层作用域中的变量同名,从而
遮蔽外层作用域
中的变量。
this绑定
在 JS 中,this
关键字是一个非常重要的概念,它表示函数执行时的上下文对象。this
的值取决于函数的调用方式,而不仅仅是在哪个对象中声明的。
理解 this
的绑定对于 JavaScript 开发者来说非常重要,因为它影响着函数的行为和代码的执行结果。以下是几种常见的 this
绑定方式:
全局上下文
在全局函数中,this
指向全局对象,浏览器中是 window
,Node.js 中是 global
。
function sayHello() {
console.log('Hello, ' + this.name);
}
this.name = 'World';
sayHello(); // this指向外层调用的window对象 输出: Hello, World
对象方法调用
当函数作为对象的方法调用时,this
指向调用该方法的对象。
const person = {
name: 'Tom',
greet: function() {
console.log('Hello, ' + this.name);
}
};
person.greet(); // this指向调用的preson对象 输出: Hello, Tom
构造函数调用
在构造函数中,this
指向新创建的对象实例。
function Person(name) {
this.name = name;
}
const tom = new Person('Tom');
console.log(tom.name); // this指向新创建的实例tom 输出: Tom
箭头函数
箭头函数没有自己的 this
上下文,它会捕获其所在上下文的 this
值。
const person = {
name: 'Tom',
greet: () => {
console.log('Hello, ' + this.name);
}
};
person.greet(); // 输出: Hello, undefined,因为箭头函数没有自己的 this
call
、apply
和 bind
方法
这些方法是 Function
原型链上的方法,可以用来显式设置 this
的值。
const person = {
name: 'Tom'
};
function sayHello() {
console.log('Hello, ' + this.name);
}
// this指向call或者apply调用的person
sayHello.call(person); // 输出: Hello, Tom
sayHello.apply(person); // 输出: Hello, Tom
// this指向bind的person,bind不会立即执行,需要加()执行
sayHello.bind(person)(); // 输出: Hello, Tom
隐式绑定
如果函数作为对象属性被引用,但是以非对象上下文调用,this
会指向全局对象或 undefined
(严格模式下)。
const person = {
name: 'Tom',
greet: function() {
console.log('Hello, ' + this.name);
}
};
const { greet } = person;
greet(); // 输出: Hello, undefined
原型链和继承
原型链
和继承
是实现对象属性和方法共享的核心机制。以下是关于原型链和继承的一些关键概念:
原型链
- 原型对象:每个 JS 对象都有一个内部属性
[[Prototype]]
,它引用了另一个对象,称为该对象的原型。 - 原型链的查找:当访问对象的属性或方法时,如果该属性或方法在对象自身上不存在,JS 引擎会沿着
[[Prototype]]
链接到原型对象上查找。 - Object.prototype:所有对象的原型链最终都会指向
Object.prototype
,它是所有原型的基石。 - 原型继承:对象的属性和方法可以通过原型链实现继承。
继承
- 构造函数:使用构造函数
new
关键字创建新对象时,可以通过call
或apply
方法将函数的this
绑定到原型上,从而继承原型的属性和方法。
function Parent() {
this.property = 'parent';
}
function Child() {
Parent.call(this);
this.childProperty = 'child';
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
const child = new Child();
console.log(child.property); // 'parent'
- 原型链继承:直接将一个对象的原型指向另一个对象,实现继承。
const parent = {
property: 'parent'
};
const child = Object.create(parent);
child.childProperty = 'child';
console.log(child.property); // 'parent'
- 组合继承:结合构造函数和原型链继承,使用构造函数继承实例属性,通过原型链继承方法。
function Parent(name) {
this.name = name;
}
Parent.prototype.getName = function() {
return this.name;
};
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
Child.prototype.getAge = function() {
return this.age;
};
- 原型式继承:使用一个函数作为工厂,创建一个具有指定原型和属性的对象。
function createObject(proto) {
function F() {}
F.prototype = proto;
return new F();
}
const newObject = createObject({ property: 'parent' });
- 寄生式继承:在原型式继承的基础上,通过修改对象来增强功能。
function createObject(proto) {
const result = Object.create(proto);
result.someNewProperty = 'new property';
return result;
}
- ES6 类的继承:使用
class
关键字和extends
来实现继承。
class Parent {
constructor(name) {
this.name = name;
}
}
class Child extends Parent {
constructor(name, age) {
super(name);
this.age = age;
}
}
- Object.assign:用于对象属性的复制,但不会复制函数和方法。
const parent = { property: 'parent' };
const child = Object.assign({}, parent, { childProperty: 'child' });
内存管理
JS 的内存管理主要涉及几个方面:内存分配
、垃圾回收
、内存泄漏
以及性能优化
。以下是一些关键概念:
内存分配
当 JavaScript 创建对象
、数组
、函数
等时,内存会被分配
给这些数据结构
。
垃圾回收 Garbage Collection(简称 GC)
- 标记-清除(Mark-and-Sweep):这是最常见的垃圾回收算法。垃圾回收器会遍历所有可达对象,并将不可达的
对象标记
为可回收。之后,进行清除阶段,释放那些被标记的内存。 - 引用计数(Reference Counting):每个对象维护一个
引用计数
,当引用计数为零
时,对象的内存可以被回收。但由于循环引用问题,许多现代 JS 引擎不再使用此方法。 - 分代收集(Generational Collection):基于这样一个假设,即大多数对象都是短暂存在的。因此,内存被分为两个空间:
新生代
和老生代
。新生代中的对象如果经过多次垃圾回收仍然存活,会被移动到老生代。
内存泄漏
- 全局变量:意外创建的全局变量可能会导致内存泄漏,因为它们会一直存在于全局作用域中。
- 闭包:如果不正确使用闭包,可能会无意中保持对不再需要的变量的引用。
- 事件监听器:未正确移除的事件监听器可能会造成内存泄漏,尤其是当它们引用了 DOM 元素或对象。
- 定时器:设置的
setInterval
或setTimeout
可能会引用一些不再需要的对象。
性能优化
- 内存泄漏检测:使用浏览器的开发者工具检测内存泄漏。
- 优化数据结构:使用合适的数据结构可以减少内存使用和提高性能。
- 避免不必要的内存分配:例如,避免在循环中创建大量
小对象
。 - 使用内置方法:JS 引擎优化了内置方法,如
Array.prototype.slice
,使用它们通常比自定义方法更高效。 - 内存限制:在内存受限的环境中,如移动设备或嵌入式设备,优化内存使用尤为重要。
- 及时清理:例如,移除不再需要的
DOM 元素
和事件监听器
。 - 避免循环引用:特别是在使用闭包时,确保不会创建无法被垃圾回收的循环引用。
- 合理使用缓存:缓存可以提高性能,但
过多的缓存
也会导致内存占用增加
。
单线程与Event Loop
JS 的单线程特性和 Event Loop
是其运行机制的核心部分,它们共同确保了 JS 程序的执行方式。Event Loop 机制使得 JS 能够在不阻塞主线程的情况下处理大量异步操作。
单线程
JavaScript 采用单线程
模型,意味着在任何时刻只有一个主线程在执行代码。单线程的优势包括:
- 简化编程模型:开发者不需要处理多线程编程中的
并发
和同步
问题。 - 安全:单线程避免了多个线程同时
读写共享数据
造成的竞态条件
。
单线程也带来了一些挑战,特别是涉及到密集型计算任务
时,可能会阻塞
用户界面的更新。
Event Loop
它允许 JavaScript 引擎在单线程环境中处理异步操作。Event Loop
的工作流程大致如下:
-
调用栈(Call Stack):
- JavaScript 代码执行时,所有同步任务都在调用栈中执行。
-
事件队列(Event Queue):
- 异步任务(如
setTimeout
、Promise
、DOM
事件等)完成后,它们的回调函数会被放入事件队列中等待执行。
- 异步任务(如
-
Event Loop:
- Event Loop 检查调用栈是否为空。如果调用栈为空,Event Loop 会从事件队列中取出第一个任务,放入调用栈中执行。
-
宏任务(Macro Tasks)和微任务(Micro Tasks):
- 宏任务包括
Script
(外层同步代码)、setTimeout
、setInterval
、I/O
、UI
渲染等。 - 微任务包括
Promise
回调、MutationObserver
等。
- 宏任务包括
-
执行顺序:
- 同步任务立即执行。
- 异步任务的回调函数按照它们被加入队列的顺序执行。
- Event Loop 在压入事件时,都会判断微任务队列是否还有需要执行的事件:如果有,则优先将需要执行的微任务压入;没有,则依次压入需要执行宏任务!
-
浏览器和 Node.js 的差异:
- 浏览器环境和 Node.js 环境的 Event Loop 有所不同。浏览器更侧重于 UI 渲染和用户交互,而 Node.js 更侧重于 I/O 操作。
示例
console.log('Script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('Script end');
执行顺序将是:
Script start
Script end
promise1
promise2
setTimeout
函数式编程
函数式编程(Functional Programming,简称 FP
)是一种编程范式,它将计算视为数学函数的评估,并避免使用程序状态以及易变对象。以下是函数式编程的一些核心概念:
- 纯函数(Pure Functions):纯函数是指给定
相同的输入
总是产生相同输出
,并且没有副作用
的函数。这意味着它们不依赖于或修改外部状态。 - 无副作用(Side Effects):函数式编程强调
避免副作用
,即函数的执行不应影响外部环境的状态。 - 不可变性(Immutability):函数式编程鼓励使用
不可变
数据结构,这意味着一旦数据结构被创建,它就不能被改变。 - 高阶函数(Higher-order Functions):
高阶函数
是指接收一个或多个函数作为参数,或返回一个函数的函数。 - 函数组合(Function Composition):
函数组合
是将多个函数组合成一个新函数的过程,每个函数接收另一个函数
的输出作为输入。 - 柯里化(Currying):
柯里化
是一种将一个多参数的函数转换成一系列单参数函数的技术。 - 函数式编程的数组方法:
map
用于将函数应用于集合的每个元素,filter
用于创建符合某些条件的元素集合,reduce
用于将集合归约为单个值。
高阶函数
高阶函数(Higher-order Function)是 JS 中一个重要的概念,指的是满足以下条件之一的函数:
- 接受一个或多个函数作为参数:高阶函数可以将其他函数作为输入,这使得它们能够操作或改变其他函数的行为。
- 返回一个函数:高阶函数可以返回一个
新创建
的函数,通常这个新函数会结合输入的参数和逻辑来执行特定的任务。
返回函数的示例:
function createAdder(x) {
return function(y) {
return x + y;
};
}
const addFive = createAdder(5);
console.log(addFive(3)); // 输出 8
JS 原生高阶函数:
JS 原生提供了一些高阶函数,如:
Array.prototype.map()
:接受一个函数作为参数,返回一个新数组,其中的元素是调用原始数组每个元素的函数返回的结果。Array.prototype.filter()
:接受一个函数作为参数,返回一个新数组,包含通过测试(函数返回 true)的所有元素。Array.prototype.reduce()
:接受一个函数作为参数,将数组元素累加起来,返回最终的累加值。Array.prototype.forEach()
:接受一个函数作为参数,对数组的每个元素执行该函数。Function.prototype.apply()
和Function.prototype.call()
:允许你调用函数,并指定this
的值和参数。
高阶函数的应用:
高阶函数可以用于多种场景:
- 函数装饰器(Decorators):创建
装饰器
来增强或修改函数的行为。 - 延迟执行(Deferring Execution):使用如
setTimeout
或setInterval
来延迟函数的执行。 - 部分应用(Partial Application):创建已经预设了一些参数的新函数。
- 函数工厂(Function Factory):根据输入参数动态创建函数。
- 中间件(Middleware):在 Web 应用或库中,用于处理请求/响应的中间件通常使用高阶函数。
函数柯里化
柯里化(Currying)是一种将一个多参数
的函数转换成一系列单参数
函数的技术。这种技术允许你将一个复杂函数分解成几个更简单的函数,每个函数只接受一个参数。柯里化在函数式编程中非常常见,它有助于提高代码的灵活性和可重用性。
柯里化的关键特点:
- 参数分解:将一个接受多个参数的函数转换成一系列接受单个参数的函数。
- 函数返回函数:每个转换后的函数在接收到一个参数后,返回一个新的函数等待下一个参数。
- 延迟计算:直到所有参数都被提供后,实际的计算才会执行。
- 部分应用:通过
柯里化
,可以创建一个已经预设了一些参数的函数,这种技术称为部分应用(Partial Application)。 - 函数组合:柯里化函数可以很容易地进行组合,形成更复杂的函数。
示例:
// 假设我们有一个计算两个数相加的函数:
function add(a, b) {
return a + b;
}
// 我们可以通过柯里化将其转换为接受单个参数的函数:
function curriedAdd(a) {
return function(b) {
return a + b;
};
}
const addFive = curriedAdd(5); // addFive 是一个函数,等待接收第二个参数
console.log(addFive(3)); // 输出 8
实现柯里化:
在现代 JS 中,可以使用函数式编程库如 Lodash
来轻松实现柯里化,或者使用原生 JS 进行柯里化:
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
};
}
};
}
// 使用柯里化函数封装 add 函数
const curriedAdd = curry(function(...numbers) {
return numbers.reduce((a, b) => a + b, 0);
});
const sum = curriedAdd(1,2,3); // 6
未来展望
JS 作为 Web 开发的主导语言,一直在不断地发展和适应技术变革。以下是一些关于 JS 未来展望的关键点:
- 渐进式 Web 应用程序(PWA):
PWA
结合了 Web 和移动应用的最佳特性,提供类似应用的体验,并通过技术如Service Workers
和Web App Manifests
实现离线功能
和推送通知
。 - 无服务器架构:JS 在
无服务器架构
中扮演着重要角色,提供了可扩展性和成本效益,允许开发者在如AWS Lambda
和Azure Functions
等环境中运行 JS 代码。 - 机器学习和人工智能:借助
TensorFlow.js
和Brain.js
等库,JS 应用程序可以集成机器学习
和人工智能
功能,用于开发智能聊天机器人
、推荐系统
等。 - 语音用户界面(VUI)和自然语言处理(NLP):随着
虚拟助手
和智能扬声器
的普及,VUI
越来越受欢迎。JS 框架结合NLP
库如Natural
和Web Speech API
,可以创建支持语音
的应用程序。 - 增强现实(AR)和虚拟现实(VR):JS 在
AR
和VR
领域的应用增加,使用如A-Frame
和Three.js
等工具在浏览器中创建沉浸式体验
。 - 区块链和加密货币应用:JS 通过框架和库如
Solidity
和Web3.js
支持去中心化应用程序 (dApp
) 的开发,与区块链
网络交互。 - 微前端和基于组件的架构:受微服务启发的
微前端
将大型前端应用划分为更小、更易管理的部分,提高开发和维护的效率 - Jamstack 架构:强调
预渲染
、静态站点
生成以及客户端渲染的Jamstack
方法,使用Gatsby
和Next.js
等框架,提供快速、安全且可扩展的网站。 - 服务器优先和边缘网络 JS 框架将继续发展
服务器优先
的方法,利用边缘网络
提供更好的性能和用户体验。 - AI 的融合:
人工智能
可能逐渐融入 JS 开发,提供代码迁移和生成工具,以及实时性能改进。
JS 的未来展望显示出它将继续扩展到传统 Web 开发之外的领域,并且随着新技术和框架的发展,JS 将提供更多创新的解决方案和更好的用户体验。