语言概述
JavaScript是一门动态的、弱类型的、基于原型的解释型编程语言,由布兰登·艾克(Brendan Eich)于1995年在网景公司仅用十天时间设计实现。尽管名字中包含"Java",但两者除语法表面相似外,在设计哲学、类型系统和运行模型上截然不同。JavaScript最初被命名为Mocha,后改为LiveScript,最终因市场营销考量定名为JavaScript。
JavaScript遵循ECMAScript规范,由Ecma国际(原欧洲计算机制造商协会)的技术委员会TC39负责标准化。ECMAScript 3(1999年)奠定了现代JavaScript的基础;ECMAScript 5(2009年)引入了严格模式、JSON支持、数组方法等;ECMAScript 6(2015年,又称ES2015)是语言历史上最大的一次更新,带来了类、模块、箭头函数、Promise等现代特性。此后,TC39改为每年发布一个新版本,以年份命名(ES2016、ES2017等),采用提案驱动的开发流程。
JavaScript的运行环境主要分为浏览器端与服务器端(Node.js)。在浏览器中,JavaScript通过全局对象window访问浏览器提供的API(DOM、BOM、定时器等)。在Node.js中,全局对象为global(或ES2020引入的globalThis),提供文件系统、网络、进程等服务器端能力。尽管运行环境不同,核心语言机制(类型系统、原型链、事件循环等)保持一致。
数据类型
JavaScript拥有七种原始数据类型(Primitive Types)和一种引用类型(Reference Type)。原始类型包括:undefined、null、boolean、number、string、symbol(ES6引入)和bigint(ES2020引入)。引用类型即对象(Object),涵盖普通对象、数组、函数、日期、正则表达式等。
原始类型与引用类型的核心区别在于存储方式:原始类型的值直接存储在栈内存中,引用类型的值存储在堆内存中,变量保存的是指向堆内存的地址(引用)。因此,原始类型的比较是值的比较,而引用类型的比较是引用的比较——两个内容相同的对象使用===比较时结果为false,因为它们指向不同的内存地址。
undefined表示变量已声明但未赋值,或对象属性不存在,或函数未返回值。null表示一个空值或不存在对象的占位符。一个历史遗留的怪癖是typeof null返回"object",这是语言设计初期的bug,因修复会破坏现有代码而保留至今。
number类型采用IEEE 754双精度浮点数格式,能够精确表示的整数范围是-2^53到2^53(安全整数范围,由Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER定义)。超出此范围的整数运算可能丢失精度。bigint类型通过追加n后缀(如123n)或BigInt构造函数创建,可以表示任意精度的整数,但不能与number类型直接混合运算。
string在JavaScript中是不可变的(immutable),任何对字符串的"修改"操作实际上都是创建新的字符串。模板字面量(Template Literals,使用反引号包裹)支持多行字符串、字符串插值(${expression})和标签模板(Tagged Templates),极大地改善了字符串操作的体验。
类型转换是JavaScript中容易出错的领域。显式转换通过Number()、String()、Boolean()等函数完成。隐式转换发生在运算符操作数类型不匹配时,遵循一套复杂的规则。例如,+运算符在任一操作数为字符串时执行字符串拼接;==运算符在比较前会进行类型转换(因此推荐使用不进行类型转换的===);逻辑运算符&&和||返回操作数本身而非布尔值(短路求值)。
作用域与闭包
作用域(Scope)定义了变量和函数的可访问范围。在ES6之前,JavaScript只有全局作用域和函数作用域,没有块级作用域。var声明的变量存在变量提升(Hoisting)现象——声明会被提升到其作用域的顶部,但赋值不会。这导致在声明之前访问var变量会得到undefined而非报错。
ES6引入的let和const声明拥有块级作用域(Block Scope),在代码块(由{}包围)内部声明的变量仅在该块内可见。它们不存在变量提升(严格来说是存在暂时性死区——Temporal Dead Zone,在声明之前访问会抛出ReferenceError)。const声明的变量必须在声明时初始化,且不能重新赋值(但对于对象和数组,其内部属性或元素仍可以修改,因为const保证的是引用的不变性)。
作用域链(Scope Chain)是JavaScript查找变量的机制。当访问一个变量时,引擎首先在当前作用域查找,若未找到则向上一级作用域查找,直至全局作用域。若全局作用域中仍未找到,则抛出ReferenceError(严格模式下)或在非严格模式下隐式创建全局变量(历史遗留行为)。
闭包(Closure)是JavaScript中最强大也最令人困惑的概念之一。当一个函数能够记住并访问其词法作用域(lexical scope),即使该函数在当前词法作用域之外执行,闭包就形成了。简而言之,闭包使得函数可以"携带"其定义时的环境状态。闭包的典型应用包括:数据私有化(模块模式)、函数工厂、回调函数保持状态、防抖与节流等。
模块作用域随着ES6模块系统的引入而标准化。每个ES模块都拥有独立的作用域,通过export导出、import导入实现代码的模块化组织。这与传统的立即执行函数表达式(IIFE)模式实现的模块封装在理念上一脉相承,但语法更为清晰,且支持静态分析和树摇优化(Tree Shaking)。
原型与继承
JavaScript采用基于原型(Prototype-based)的继承模型,而非传统的基于类(Class-based)的继承。每个对象(除null外)都有一个内部属性[[Prototype]],指向其原型对象。当访问对象的属性或方法时,若对象本身不存在该属性,则会沿着原型链向上查找,直至找到该属性或到达原型链的顶端(Object.prototype,其[[Prototype]]为null)。
__proto__是[[Prototype]]的浏览器端访问方式(现已标准化),Object.getPrototypeOf()和Object.setPrototypeOf()是标准的读写方法。构造函数(constructor function)的prototype属性指向的对象,会成为由该构造函数创建的实例的原型。例如,new Foo()创建的实例的[[Prototype]]指向Foo.prototype。
原型链的顶端是Object.prototype,它包含了所有对象共有的方法,如toString()、valueOf()、hasOwnProperty()等。数组的原型链为:实例 → Array.prototype → Object.prototype → null。函数的原型链为:实例 → Function.prototype → Object.prototype → null。
ES6引入的class关键字提供了一种更清晰的语法糖来定义构造函数和原型方法,但底层机制仍然是原型继承。class声明的"类"实际上仍是函数,其方法定义在prototype上,静态方法定义在构造函数本身上。extends关键字实现原型链的链接,super关键字用于访问父类的构造函数或方法。这种语法改善显著提升了代码的可读性和面向对象编程的体验,但理解底层原型机制对于调试和高级应用仍然至关重要。
继承的实现方式除了class语法外,还包括原型链继承(直接设置子构造函数的prototype为父构造函数的实例)、构造函数继承(在子构造函数中调用父构造函数)、组合继承(结合前两者)、寄生组合继承(通过Object.create()创建中间对象避免不必要的父实例化)等。在现代开发中,这些手动实现方式已被class语法所取代,但了解其原理有助于深入理解语言机制。
this 绑定
this是JavaScript中最容易出错的机制之一,其值取决于函数的调用方式,而非定义方式。this的绑定规则按优先级可分为四种:默认绑定、隐式绑定、显式绑定和new绑定。
默认绑定发生在独立函数调用时(非严格模式下),this指向全局对象(浏览器中为window,Node.js中为global)。严格模式下,独立调用的函数中this为undefined。这是许多"丢失this"问题的根源——当一个方法被赋值给变量再调用时,其隐式绑定的上下文会丢失。
隐式绑定发生在函数作为对象的方法被调用时,this指向调用该方法的对象。例如obj.foo()中,foo内部的this指向obj。但需注意,隐式绑定仅在调用时生效,将方法引用传递出去后再次调用,隐式绑定会丢失。
显式绑定通过函数的call、apply和bind方法实现。call和apply立即调用函数,并允许指定this值和参数(call接受参数列表,apply接受参数数组)。bind返回一个永久绑定指定this值的新函数,不会立即执行。这些方法对于明确控制函数执行上下文非常有用。
new绑定发生在使用new关键字调用构造函数时。new操作执行四步:创建一个新对象;将新对象的[[Prototype]]链接到构造函数的prototype;将构造函数中的this绑定到新对象;若构造函数未返回对象,则返回新对象。因此,构造函数中的this指向新创建的实例。
箭头函数(Arrow Functions)不遵循上述绑定规则,它没有自己的this,而是继承外层作用域的this值。这一特性使箭头函数非常适合作为回调函数使用,避免了传统函数中this指向意外改变的问题。但这也意味着箭头函数不能用作构造函数(不能使用new调用),也不适合作为对象的方法(若需要访问对象本身)。
异步编程
JavaScript是单线程语言,这意味着同一时间只能执行一段代码。异步编程机制使得JavaScript能够在等待耗时操作(如网络请求、定时器、文件读写)完成的同时,继续执行其他代码,避免了线程阻塞。
回调函数(Callback)是最早的异步编程模式。将函数作为参数传递给异步操作,操作完成时调用该函数。这种模式简单直接,但在处理多个嵌套异步操作时,容易形成"回调地狱"(Callback Hell)——代码横向扩展,可读性和维护性急剧下降。
Promise是ES6引入的异步编程解决方案,代表一个尚未完成但预期将来会完成的操作。Promise有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败),状态一旦改变便不可再变。通过then方法注册成功和失败的回调,通过catch方法捕获错误,通过finally方法执行无论成败都会运行的清理逻辑。Promise链式调用解决了回调地狱的缩进问题,但链式调用过多时仍可能影响代码清晰度。
async/await是ES2017引入的语法糖,建立在Promise之上,使异步代码看起来像同步代码。async函数总是返回一个Promise,await关键字只能在async函数内部使用,它会暂停函数执行,等待Promise解决,然后返回解决值。这种写法极大地提升了异步代码的可读性,使错误处理可以通过传统的try/catch结构完成。但需注意,await会阻塞async函数内部的后续代码执行(尽管不会阻塞主线程),在不需要顺序依赖的多个异步操作中,应使用Promise.all并行执行。
事件循环(Event Loop)是理解JavaScript异步机制的核心。调用栈(Call Stack)执行同步代码;当遇到异步操作时,将其交给浏览器或Node.js的Web APIs处理;操作完成后,回调函数被推入任务队列(Task Queue,又称宏任务队列Macrotask Queue)。事件循环不断检查调用栈是否为空,若为空则将任务队列中的回调推入调用栈执行。Promise的回调进入微任务队列(Microtask Queue),微任务队列的优先级高于宏任务队列,在当前宏任务执行完毕后、下一个宏任务开始前,会清空所有微任务。
ES6+ 新特性
自ES2015以来,JavaScript每年都在稳步进化,引入了大量提升开发效率和代码质量的特性。
解构赋值(Destructuring)允许从数组或对象中提取值并赋给变量,支持嵌套解构、默认值和剩余模式。扩展运算符(Spread Operator ...)可以将可迭代对象展开为个体,在数组字面量、对象字面量(ES2018)和函数调用中都有广泛应用。剩余参数(Rest Parameters)将函数的多余参数收集为数组。
Promise.allSettled(ES2020)等待所有Promise完成(无论成功或失败),返回每个Promise的状态和结果。Promise.any(ES2021)返回第一个成功的Promise,仅当所有Promise都失败时才拒绝。Promise.race返回第一个落定(无论成功或失败)的Promise。
可选链操作符(Optional Chaining ?.,ES2020)允许安全地访问深层嵌套属性,若中间某属性为null或undefined,则表达式短路返回undefined,不会抛出错误。空值合并运算符(Nullish Coalescing ??,ES2020)仅在左侧为null或undefined时返回右侧值,与||运算符不同(后者对0、空字符串、false也会返回右侧值)。
BigInt(ES2020)支持任意精度整数。globalThis(ES2020)提供跨环境的全局对象访问方式。动态import(ES2020)允许运行时按需加载模块,返回一个Promise。顶层await(ES2022)允许在模块顶层直接使用await,简化了异步模块的编写。
WeakMap和WeakSet是Map和Set的"弱引用"版本,其键必须是对象,且当键对象不再被其他地方引用时,垃圾回收器可以回收该键值对。这适合用于与对象生命周期绑定的私有数据存储,避免内存泄漏。Proxy对象(ES6)可以拦截对象的基本操作(如属性访问、赋值、函数调用等),实现数据验证、日志记录、属性默认值等高级功能。Reflect对象提供了一组与Proxy拦截器对应的标准化方法。