JavaScript随笔

514 阅读38分钟

JS随笔

本文主要记录一些关于JS的起源、应用场景、特性、属性、方法和一些底层的原理,供自己以后查漏补缺,也欢迎同道朋友交流学习。

介绍

起源

JavaScript 在 1995 年由网景公司( Netscape )的布兰登·艾奇( Brendan Eich )创造,最初命名为 Mocha,后来改为 LiveScript,最终定名为 JavaScript。它最初是为了使网页更具交互性而设计的,现在已经成为 Web 开发领域不可或缺的一部分,并且随着时间的发展,其应用范围远远超出了浏览器。

应用场景

  • Web前端开发:通过 JS,开发者可以创建动态的 Web 页面,如交互式表单、动态内容更新、动画效果等。
  • Web后端开发:借助 Node.js,开发者可以在服务器端编写服务端逻辑。
  • 桌面应用程序:通过 Electron 等框架,可以开发跨平台桌面应用
  • 移动应用开发:利用 React NativeFlutterIonicUniApp 等框架,可以开发原生移动应用
  • 物联网(IoT)开发:使用 WebSocketsWebRTCService Workers 可以与物联网设备进行交互,也可以使用 D3.jsChart.js进行数据可视化开发。
  • 游戏开发:尽管 JS 不是主流游戏开发语言,但在H5和微信小游戏领域基于 cocos 也有丰富的应用。

JS特性

  • 基于原型:对象可以通过其他对象继承属性和方法。
  • 动态类型:变量类型可以根据赋值自动改变。
  • 弱类型:比较运算符会自动转换数据类型。
  • 垃圾回收:支持自动内存管理(垃圾回收)。
  • 异步编程:支持异步编程模式,如回调函数、Promises、async/await等,适合处理如用户界面响应、网络请求等异步事件。

常用库和框架

  • 三大框架ReactVueAngular
  • 常用工具库AxiosMoment.jsLodashjQuery
  • 图形可视化D3.jsAntVEchartsThree.js
  • 路由库React RouterVue RouterAngular Router
  • 状态库ReduxMobXVuex

变量与数据类型

变量声明

使用 var、let 或 const 关键字声明变量。var 作用域为函数或全局,而 letconst 提供了块级作用域。

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:

  1. 获取元素

    • document.getElementById(id): 通过元素的ID获取元素
    • document.getElementsByTagName(name): 通过标签名获取元素集合
    • document.getElementsByClassName(names): 通过类名获取元素集合
    • document.querySelector(selector): 根据CSS选择器获取第一个匹配的元素
    • document.querySelectorAll(selector): 根据CSS选择器获取所有匹配的元素集合
  2. 创建和插入元素

    • document.createElement(tagName): 创建一个新的元素节点
    • element.appendChild(node): 将一个节点添加到元素的子节点列表的末尾
    • element.insertBefore(newNode, referenceNode): 在参考节点前插入一个新节点。
    • element.replaceChild(newNode, oldNode): 替换元素的子节点。
  3. 删除元素

    • element.removeChild(node): 从元素中移除一个子节点。
    • element.remove(): 移除元素自身
  4. 属性操作

    • element.getAttribute(name): 获取元素的属性值。
    • element.setAttribute(name, value): 设置或修改元素的属性值。
    • element.removeAttribute(name): 移除元素的属性。
  5. 样式操作

    • element.style.property: 直接设置元素的内联样式
    • element.classList.add(className): 向元素添加一个类。
    • element.classList.remove(className): 从元素移除一个类。
    • element.classList.toggle(className): 切换元素的类。
  6. 文本操作

    • element.textContent: 获取或设置元素的文本内容
    • element.innerText: 获取或设置元素的文本内容,但不包括子元素的文本。
  7. 事件监听

    • element.addEventListener(type, listener[, options]): 为元素添加事件监听器
    • element.removeEventListener(type, listener[, options]): 移除元素的事件监听器。
  8. 遍历DOM树

    • element.parentNode: 获取元素的父元素。
    • element.childNodes: 获取元素的所有子节点。
    • element.firstChild: 获取元素的第一个子节点。
    • element.lastChild: 获取元素的最后一个子节点。
    • element.nextSibling: 获取元素的下一个兄弟节点。
    • element.previousSibling: 获取元素的上一个兄弟节点。
  9. 滚动操作

    • window.scroll(x, y): 滚动到页面的指定位置。
    • window.scrollTo(options): 滚动到页面的指定位置,可以是坐标或元素。
  10. 尺寸和位置

    • 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 主要由以下几个核心对象组成:

  • windowwindow 对象代表浏览器窗口,是 BOM 的核心,包含了与浏览器窗口相关的属性和方法。
  • locationwindow.location 对象提供了与当前窗口中加载的文档有关的信息,如 URL协议主机名等。
  • navigatorwindow.navigator 对象提供了关于浏览器的信息,包括浏览器类型版本操作系统等。
  • historywindow.history 对象保存了浏览器的历史记录,允许网页通过 JS 进行前进后退等操作。
  • screenwindow.screen 对象提供了有关用户屏幕的信息,如屏幕的宽度高度颜色等。
  • documentwindow.document 对象是 DOM 的顶级节点,代表了整个 HTML 文档,可以通过它来操作页面内容。
  • consolewindow.console 对象提供了一组用于向浏览器控制台输出信息的方法,如 console.log()console.error() 等。
  • setTimeout 和 setInterval:用于设置定时器,分别用于在指定的时间后执行代码和每隔指定的时间重复执行代码。
  • fetchwindow.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

MapSet 是新的集合类型,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 关键字声明的函数创建自己的作用域。
  • 块级作用域:使用 letconst 关键字声明的变量具有块级作用域,即它们只在定义它们的 {} 块内可见。
  • 作用域链:当访问一个变量时,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

callapplybind 方法

这些方法是 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 关键字创建新对象时,可以通过 callapply 方法将函数的 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 元素或对象。
  • 定时器:设置的 setIntervalsetTimeout 可能会引用一些不再需要的对象。

性能优化

  • 内存泄漏检测:使用浏览器的开发者工具检测内存泄漏。
  • 优化数据结构:使用合适的数据结构可以减少内存使用和提高性能。
  • 避免不必要的内存分配:例如,避免在循环中创建大量小对象
  • 使用内置方法:JS 引擎优化了内置方法,如 Array.prototype.slice,使用它们通常比自定义方法更高效。
  • 内存限制:在内存受限的环境中,如移动设备或嵌入式设备,优化内存使用尤为重要。
  • 及时清理:例如,移除不再需要的 DOM 元素事件监听器
  • 避免循环引用:特别是在使用闭包时,确保不会创建无法被垃圾回收的循环引用。
  • 合理使用缓存:缓存可以提高性能,但过多的缓存也会导致内存占用增加

单线程与Event Loop

JS 的单线程特性和 Event Loop 是其运行机制的核心部分,它们共同确保了 JS 程序的执行方式。Event Loop 机制使得 JS 能够在不阻塞主线程的情况下处理大量异步操作。

单线程

JavaScript 采用单线程模型,意味着在任何时刻只有一个主线程在执行代码。单线程的优势包括:

  • 简化编程模型:开发者不需要处理多线程编程中的并发同步问题。
  • 安全:单线程避免了多个线程同时读写共享数据造成的竞态条件

单线程也带来了一些挑战,特别是涉及到密集型计算任务时,可能会阻塞用户界面的更新。

Event Loop

它允许 JavaScript 引擎在单线程环境中处理异步操作。Event Loop 的工作流程大致如下:

  1. 调用栈(Call Stack)

    • JavaScript 代码执行时,所有同步任务都在调用栈中执行。
  2. 事件队列(Event Queue)

    • 异步任务(如 setTimeoutPromiseDOM 事件等)完成后,它们的回调函数会被放入事件队列中等待执行。
  3. Event Loop

    • Event Loop 检查调用栈是否为空。如果调用栈为空,Event Loop 会从事件队列中取出第一个任务,放入调用栈中执行。
  4. 宏任务(Macro Tasks)和微任务(Micro Tasks)

    • 宏任务包括 Script(外层同步代码)、 setTimeoutsetIntervalI/OUI 渲染等。
    • 微任务包括 Promise 回调、MutationObserver 等。
  5. 执行顺序

    • 同步任务立即执行。
    • 异步任务的回调函数按照它们被加入队列的顺序执行。
    • Event Loop 在压入事件时,都会判断微任务队列是否还有需要执行的事件:如果有,则优先将需要执行的微任务压入;没有,则依次压入需要执行宏任务!
  6. 浏览器和 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):使用如 setTimeoutsetInterval 来延迟函数的执行。
  • 部分应用(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 WorkersWeb App Manifests 实现离线功能推送通知
  • 无服务器架构:JS 在无服务器架构中扮演着重要角色,提供了可扩展性和成本效益,允许开发者在如 AWS LambdaAzure Functions 等环境中运行 JS 代码。
  • 机器学习和人工智能:借助 TensorFlow.jsBrain.js 等库,JS 应用程序可以集成机器学习人工智能功能,用于开发智能聊天机器人推荐系统等。
  • 语音用户界面(VUI)和自然语言处理(NLP):随着虚拟助手智能扬声器的普及,VUI 越来越受欢迎。JS 框架结合 NLP 库如 NaturalWeb Speech API,可以创建支持语音的应用程序。
  • 增强现实(AR)和虚拟现实(VR):JS 在 ARVR 领域的应用增加,使用如 A-FrameThree.js 等工具在浏览器中创建沉浸式体验
  • 区块链和加密货币应用:JS 通过框架和库如 SolidityWeb3.js 支持去中心化应用程序 ( dApp ) 的开发,与区块链网络交互。
  • 微前端和基于组件的架构:受微服务启发的微前端将大型前端应用划分为更小、更易管理的部分,提高开发和维护的效率
  • Jamstack 架构:强调预渲染静态站点生成以及客户端渲染的 Jamstack 方法,使用 GatsbyNext.js 等框架,提供快速、安全且可扩展的网站。
  • 服务器优先和边缘网络 JS 框架将继续发展服务器优先的方法,利用边缘网络提供更好的性能和用户体验。
  • AI 的融合人工智能可能逐渐融入 JS 开发,提供代码迁移和生成工具,以及实时性能改进。

JS 的未来展望显示出它将继续扩展到传统 Web 开发之外的领域,并且随着新技术和框架的发展,JS 将提供更多创新的解决方案和更好的用户体验。