- 文档
- 教程
【免费下载链接】learnxinyminutes-docs
Code documentation written as code! How novel and totally my idea!
导读
本篇以 learnxinyminutes-docs 仓库中的 马来语版 JavaScript 教程(与 英文原版 内容同源)为骨架,将其中以"可运行带注释代码"形式呈现的全部知识点系统化展开,覆盖 JavaScript 的数值与字符串、变量与数组、对象与原型链、控制结构与函数作用域、闭包以及 ES6 新特性。读完本文,你不仅能逐行理解 JavaScript 这门语言的底层行为(如自动分号插入、隐式类型转换、this绑定规则、原型链查找),还能掌握可直接复制运行的代码范例,理解 learnxinyminutes-docs 项目"把文档写成可运行的代码"这一教学组织方式。
一、项目背景:一份"以代码为文档"的 JavaScript 教程
learnxinyminutes-docs 是一个用"带注释的可运行代码"来讲解编程语言的开源仓库,正如 README.md 所述:"Whirlwind tours of popular and ought-to-be-more-popular programming languages, presented as valid, commented code and explained as they go."(以有效、带注释的代码形式,快速巡游流行编程语言)。
仓库中的 JavaScript 教程存在多个语言版本,包括 英文原版 javascript.md、中文版 zh-cn/javascript.md 以及本文所依据的 马来语版 ms/javascript.md。每个文档头部都有 YAML front matter,记录name、contributors、translators、filename等元数据;仓库的 lint/frontmatter.py 脚本会对这些元数据进行键名与类型校验(允许的键为name、where_x_eq_name、category、filename、contributors、translators),确保每一篇教程的格式统一、可被工具链自动检查。
JavaScript 由 Netscape 的 Brendan Eich 于 1995 年发明,最初定位为网站的简单脚本语言,作为更复杂的 Java Web 应用的补充;但由于与网页的紧密集成以及浏览器内置支持,它最终在 Web 前端领域远比 Java 普及。它也不局限于浏览器——基于 Google Chrome V8 引擎的 Node.js 提供了独立运行时。JavaScript 语法接近 C 语言(熟悉 C 或 Java 的读者会发现基础语法很眼熟),但其对象模型与 Java 有本质差异,这正是本文后半部分要重点讲解的内容。
二、语法基础:注释、语句与分号自动插入(ASI)
JavaScript 的注释风格与 C 一致:单行注释以//开头,多行注释以/*包裹。
// 单行注释 /* 多行 注释 */语句可以用分号;结束,也可以省略——因为 JavaScript 会在换行处"自动插入分号"(Automatic Semicolon Insertion,ASI):
doStuff(); doStuff() // 合法,分号被自动插入关键陷阱:ASI 只在某些情形下生效,省略分号可能产生意外结果。最典型的例子是return换行问题——如果返回值与return不在同一行,ASI 会在return后立即插入分号,导致函数永远返回undefined:
function myFunction() { return // <- 此处自动插入分号 {thisIsAn: 'object literal'}; } myFunction(); // = undefined因此教程与实践中都建议始终显式使用分号,尤其是采用 Allman 大括号风格(大括号另起一行)时更要小心。
三、数值、字符串与操作符
3.1 单一数值类型:64 位 IEEE 754 双精度浮点
JavaScript 只有一种数值类型——64 位 IEEE 754 双精度浮点数(double)。double 的尾数(mantissa)为 52 位,足以精确存储约9✕10¹⁵以内的整数:
3; // = 3 1.5; // = 1.53.2 基本算术、取模与位运算
1 + 1; // = 2 0.1 + 0.2; // = 0.30000000000000004 (浮点精度问题) 8 - 1; // = 7 10 * 2; // = 20 35 / 5; // = 7 5 / 2; // = 2.5 (非整除除法) 10 % 2; // = 0 (取模) 30 % 4; // = 2 18.5 % 7; // = 4.5 (对浮点取模) 1 << 2; // = 4 (位运算,浮点被转换为至多 32 位有符号整数) (1 + 3) * 2; // = 8 (括号控制优先级)0.1 + 0.2不等于0.3是 IEEE 754 浮点表示的经典结果,理解这一点对处理金额、精确计算等场景至关重要。
3.3 特殊数值与布尔值
Infinity; // 例如 1/0 的结果 -Infinity; // 例如 -1/0 的结果 NaN; // 例如 0/0 的结果,意为 "Not a Number"(非数值) true; false;3.4 字符串、比较与隐式类型转换
字符串可用单引号'或双引号"创建:
'abc'; "Hello, world";比较与逻辑运算:
!true; // = false 否定 !false; // = true 1 === 1; // = true 全等 2 === 1; // = false 1 !== 1; // = false 不全等 2 !== 1; // = true 1 < 10; // = true 1 > 10; // = false 2 <= 2; // = true 2 >= 2; // = true字符串拼接与比较:
"Hello " + "world!"; // = "Hello world!" "1, 2, " + 3; // = "1, 2, 3" 数字被转成字符串拼接 "Hello " + ["world", "!"]; // = "Hello world,!" "a" < "b"; // = true 字符串按字典序比较 13 + !0; // 14 !0 为 true,true 参与算术被转成 1 "13" + !0; // '13true' true 被转成字符串 "true"**隐式类型转换(type coercion)**是 JavaScript 著名的"怪异"行为来源:使用双等号==比较时会做类型转换,而三等号===不做转换:
"5" == 5; // = true 发生类型转换 null == undefined; // = true "5" === 5; // = false null === undefined;// = false因此实践中应始终使用===/!==,避免隐式转换带来的意外。
字符串常用方法(教程明确提醒:length是属性,不是方法,不要加括号):
"This is a string".charAt(0); // = 'T' 按索引取字符 "Hello world".substring(0, 5); // = "Hello" 取子串 "Hello".length; // = 5 属性访问3.5 null、undefined 与真值/假值
null; // 表示刻意设置的"非值" undefined; // 表示当前"值不存在"(尽管 undefined 本身也是一个值)假值(falsy)集合:false、null、undefined、NaN、0、""——这些在条件判断中为假;除此之外的一切值都为真(truthy)。特别提醒:0是假值而"0"是真值,尽管0 == "0"为 true。
四、变量、数组与对象
4.1 变量声明与赋值
JavaScript 是动态类型语言,变量用var关键字声明,无需指定类型,赋值用单个=:
var someVar = 5;危险行为:省略var直接赋值不会报错,但变量会被创建在全局作用域而非当前作用域,这是全局污染的重要来源:
someOtherVar = 10; // 创建在全局作用域!其他声明细节:
var someThirdVar; // 未赋值即声明 -> undefined var someFourthVar = 2, someFifthVar = 4; // 逗号分隔声明多个变量复合赋值与自增自减:
someVar += 5; // 等价于 someVar = someVar + 5 someVar *= 10; someVar++; // 加 1 someVar--; // 减 14.2 数组:有序、可变的异构列表
数组是任意类型值的有序列表,索引从 0 开始,使用方括号下标语法访问:
var myArray = ["Hello", 45, true]; myArray[1]; // = 45 myArray.push("World"); // 末尾追加 myArray.length; // = 4 myArray[3] = "Hello"; // 在指定索引处写入完整的前后端增删操作:
myArray.unshift(3); // 在首位添加元素 someVar = myArray.shift();// 移除首位元素并返回 myArray.push(3); // 在末尾添加元素 someVar = myArray.pop(); // 移除末尾元素并返回连接、切片与拼接(splice 会原地修改数组并返回被移除的子数组):
var myArray0 = [32,false,"js",12,56,90]; myArray0.join(";"); // = "32;false;js;12;56;90" myArray0.slice(1,4); // = [false,"js",12] 左闭右开 myArray0.splice(2,4,"hi","wr","ld"); // = ["js",12,56,90] // 现在 myArray0 === [32,false,"hi","wr","ld"]4.3 对象:无序的键值对集合
JavaScript 对象等价于其他语言中的"字典"(dictionary)或"映射"(map)——无序的键值对集合:
var myObj = {key1: "Hello", key2: "World"};键本质是字符串,但若是合法的 JavaScript 标识符,可以省略引号;值可以是任意类型:
var myObj = {myKey: "myValue", "my other key": 4};属性访问有两种语法——方括号下标语法与点语法(后者要求键是合法标识符):
myObj["my other key"]; // = 4 myObj.myKey; // = "myValue"对象是可变的:可以修改已有值、添加新键;访问未设置的属性得到undefined:
myObj.myThirdKey = true; myObj.myFourthKey; // = undefined五、逻辑与控制结构
控制结构语法与 Java 高度相似。if / else if / else、while、do-while:
var count = 1; if (count == 3) { // count 为 3 时执行 } else if (count == 4) { // count 为 4 时执行 } else { // 其他情况执行 } while (true) { // 无限循环! } var input; do { input = getInput(); } while (!isValid(input)); // 至少执行一次for循环与 C/Java 一致(初始化;继续条件;迭代步进):
for (var i = 0; i < 5; i++) { // 循环 5 次 }带标签的循环可以跳出外层(类似 Java):
outer: for (var i = 0; i < 10; i++) { for (var j = 0; j < 10; j++) { if (i == 5 && j == 5) { break outer; // 跳出外层循环而非仅内层 } } }for/in遍历对象属性(会沿原型链向上遍历):
var description = ""; var person = {fname:"Paul", lname:"Ken", age:18}; for (var x in person) { description += person[x] + " "; } // description = 'Paul Ken 18 'for/of遍历可迭代对象(内置的 String、Array、类数组的 arguments 或 NodeList、TypedArray、Map、Set,以及用户自定义的可迭代对象):
var myPets = ""; var pets = ["cat", "dog", "hamster", "hedgehog"]; for (var pet of pets) { myPets += pet + " "; } // myPets = 'cat dog hamster hedgehog '注意:for/in不应用于遍历需要保序的数组——它不保证按索引顺序返回。
逻辑运算符&&(与)与||(或),二者都会短路求值(short circuit),因此常用于设置默认值:
if (house.size == "big" && house.colour == "blue") { house.contains = "bear"; } if (colour == "red" || colour == "blue") { // colour 为 red 或 blue } var name = otherName || "default"; // otherName 为假值时使用 "default"switch语句使用===判断相等;每个 case 之后必须写break,否则匹配 case 之后的其余 case 也会依次执行:
grade = 'B'; switch (grade) { case 'A': console.log("Great job"); break; case 'B': console.log("OK job"); break; case 'C': console.log("You can do better"); break; default: console.log("Oy vey"); break; }六、函数、作用域与闭包
6.1 函数声明与一等公民地位
函数用function关键字声明:
function myFunction(thing) { return thing.toUpperCase(); } myFunction("foo"); // = "FOO"函数是一等对象(first class objects),可以被重新赋值给不同变量名,也可以作为参数传给其他函数——例如作为事件处理器:
function myFunction() { // 5 秒后执行的代码 } setTimeout(myFunction, 5000); // 注意:setTimeout 并非 JS 语言本身的一部分,而是由浏览器与 Node.js 提供的 API浏览器提供的另一个 API 是setInterval(每 5 秒执行一次);函数对象甚至可以不命名,直接把匿名函数定义写入另一个函数的实参:
setTimeout(function(){ // 5 秒后执行的代码 }, 5000);6.2 函数作用域:块没有作用域
JavaScript 具有函数作用域(function scope):函数拥有自己的作用域,但代码块(如if块)没有:
if (true) { var i = 5; } i; // = 5 —— 在块级作用域的语言中这里应为 undefined这一特性催生了经典的 **IIFE(立即执行匿名函数)**模式,用于防止临时变量泄漏到全局作用域:
(function(){ var temporary = 5; // 可通过给"全局对象"赋值来访问全局作用域: // 浏览器中通常是 window,Node.js 等非浏览器环境中名称可能不同 window.permanent = 10; })(); temporary; // 抛出 ReferenceError permanent; // = 106.3 闭包:JavaScript 最强大的特性之一
如果一个函数定义在另一个函数内部,内部函数可以访问外部函数的所有变量——即使外部函数已经执行完毕:
function sayHelloInFiveSeconds(name) { var prompt = "Hello, " + name + "!"; // 内部函数默认被放入局部作用域,如同用 var 声明 function inner() { alert(prompt); } setTimeout(inner, 5000); // setTimeout 是异步的:sayHelloInFiveSeconds 立即返回, // setTimeout 稍后才调用 inner。但由于 inner "闭包"于 // sayHelloInFiveSeconds,它被调用时仍能访问变量 prompt。 } sayHelloInFiveSeconds("Adam"); // 5 秒后弹出 "Hello, Adam!"闭包是回调、事件处理、模块封装等一切异步与高阶编程模式的基石。
七、对象进阶:this 绑定、构造函数与原型链
7.1 对象方法中的 this
对象可以包含函数;方法被调用时可通过this访问其所属对象:
var myObj = { myFunc: function() { return "Hello world!"; } }; myObj.myFunc(); // = "Hello world!" myObj = { myString: "Hello world!", myFunc: function() { return this.myString; } }; myObj.myFunc(); // = "Hello world!"核心规则:this的值取决于函数"如何被调用",而非"在哪里定义"。把方法从对象上拆下来调用,this即丢失:
var myFunc = myObj.myFunc; myFunc(); // = undefined反过来,函数可以在定义之后被挂到对象上,从而通过this获得该对象访问权:
var myOtherFunc = function() { return this.myString.toUpperCase(); }; myObj.myOtherFunc = myOtherFunc; myObj.myOtherFunc(); // = "HELLO WORLD!"7.2 call、apply 与 bind:显式指定 this
调用函数时可以用call或apply显式指定其执行上下文:
var anotherFunc = function(s) { return this.myString + s; }; anotherFunc.call(myObj, " And Hello Moon!"); // = "Hello World! And Hello Moon!" anotherFunc.apply(myObj, [" And Hello Sun!"]); // = "Hello World! And Hello Sun!"区别在于:call逐个传入参数,apply接受一个参数数组。后者在处理"接受参数序列的函数 + 数组输入"时极为实用:
Math.min(42, 6, 27); // = 6 Math.min([42, 6, 27]); // = NaN (把数组当作单个参数,出错了) Math.min.apply(Math, [42, 6, 27]); // = 6call/apply只对单次调用生效;要持久绑定上下文则使用bind:
var boundFunc = anotherFunc.bind(myObj); boundFunc(" And Hello Saturn!"); // = "Hello World! And Hello Saturn!"bind还可以**部分应用(curry)**函数——预先固定部分参数:
var product = function(a, b) { return a * b; }; var doubler = product.bind(this, 2); doubler(8); // = 167.3 构造函数与 new
用new关键字调用函数时,会创建一个新对象并通过this提供给该函数;为此设计、以这种方式调用的函数称为构造函数(constructor):
var MyConstructor = function() { this.myNumber = 5; }; myNewObj = new MyConstructor(); // = {myNumber: 5} myNewObj.myNumber; // = 57.4 原型链:JavaScript 的继承机制
与多数主流面向对象语言"由类蓝图创建实例"不同,JavaScript没有 class 蓝图,而是把实例化与继承合并为单一概念:原型(prototype)。每个对象都有原型;当你访问一个对象上不存在的属性时,解释器会去它的原型上查找。
部分实现允许通过魔法属性__proto__访问原型——它便于讲解,但不是标准的一部分(后文会给出标准做法):
var myObj = { myString: "Hello world!" }; var myPrototype = { meaningOfLife: 42, myFunc: function() { return this.myString.toLowerCase(); } }; myObj.__proto__ = myPrototype; myObj.meaningOfLife; // = 42 myObj.myFunc(); // = "hello world!" (函数同样沿原型查找)如果属性不在原型上,会继续查找原型的原型,如此沿链向上直至null:
myPrototype.__proto__ = { myBoolean: true }; myObj.myBoolean; // = true这里没有任何复制:每个对象保存的是对其原型的引用。因此修改原型会即时反映到所有继承它的对象上:
myPrototype.meaningOfLife = 43; myObj.meaningOfLife; // = 43for/in会沿原型链一直遍历到null原型;若只想考虑对象自身的属性,用hasOwnProperty()过滤:
for (var x in myObj) { console.log(myObj[x]); } // 输出: // Hello world! // 43 // [Function: myFunc] // true for (var x in myObj) { if (myObj.hasOwnProperty(x)) { console.log(myObj[x]); } } // 输出: // Hello world!7.5 创建带指定原型的新对象
__proto__并非标准,且标准中没有"修改既有对象原型"的方法;但有两种标准方式创建带指定原型的新对象。
方式一:Object.create(较新的 API,早期实现可能没有):
var myObj = Object.create(myPrototype); myObj.meaningOfLife; // = 43方式二:通过构造函数。构造函数有一个名为prototype的属性——注意它不是构造函数自身的原型,而是"用该构造函数配合new创建新对象时赋予新对象的原型":
MyConstructor.prototype = { myNumber: 5, getMyNumber: function() { return this.myNumber; } }; var myNewObj2 = new MyConstructor(); myNewObj2.getMyNumber(); // = 5 myNewObj2.myNumber = 6; myNewObj2.getMyNumber(); // = 67.6 包装对象、内置类型原型与 polyfill
字符串、数字等内置类型也有对应的构造函数,可创建包装对象(wrapper object):
var myNumber = 12; var myNumberObj = new Number(12); myNumber == myNumberObj; // = true 宽松比较相等但它们并不完全等同:
typeof myNumber; // = 'number' typeof myNumberObj; // = 'object' myNumber === myNumberObj; // = false if (0) { // 不执行,因为 0 是假值 } if (new Number(0)) { // 会执行,因为包装对象是对象,而对象永远是真值 }不过,包装对象与普通内置类型共享同一个原型,因此可以给字符串等类型扩展功能:
String.prototype.firstCharacter = function() { return this.charAt(0); }; "abc".firstCharacter(); // = "a"这一事实常用于polyfill(垫片):在旧版 JavaScript 子集中实现较新的语言特性,使其可用于老旧浏览器等环境。例如Object.create尚未在所有实现中可用时,可以这样 polyfill:
if (Object.create === undefined) { // 若已存在则不覆盖 Object.create = function(proto) { // 创建一个带正确原型的临时构造函数 var Constructor = function() {}; Constructor.prototype = proto; // 用它创建带正确原型的新对象 return new Constructor(); }; }八、ES6 新增:let、const 与箭头函数
ES6(ECMAScript 2015)引入了块级作用域变量声明与新的函数语法。
let:在**词法作用域(块级)**中定义变量,区别于var的函数作用域;let声明的变量可以被重新赋值:
let name = "Billy"; name = "William"; // 允许重新赋值const:同样按词法作用域定义变量,但一旦赋值不可重新赋值:
const pi = 3.14; pi = 4.13; // 不允许!箭头函数(lambda 语法):允许像const/let变量一样在词法作用域中定义函数:
const isEven = (number) => { return number % 2 === 0; }; isEven(7); // false传统语法中的"等价"写法:
function isEven(number) { return number % 2 === 0; }之所以给"等价"打引号,是因为箭头函数不能在定义之前被调用(不存在函数声明提升),下面的用法是非法的:
add(1, 8); // 报错:add 尚未定义 const add = (firstNumber, secondNumber) => { return firstNumber + secondNumber; };九、深入仓库:如何继续学习与验证
learnxinyminutes-docs 仓库把同一份 JavaScript 内容以多种语言组织:需要对照英文精确理解时查看 javascript.md,中文读者可对照 zh-cn/javascript.md,本文所依据的马来语版本位于 ms/javascript.md。这些文档的代码块本身就是可运行、带预期输出注释的完整示例——你可以将任意一节代码保存为.js文件,用 Node.js 运行验证(node example.js),或在浏览器控制台中逐行执行。
仓库还提供了配套的质量保障工具:lint/frontmatter.py 用于校验每篇文档 YAML front matter 的键名与类型(只允许name、where_x_eq_name、category、filename、contributors、translators六个键),体现了该项目"文档即代码、格式可验证"的组织理念。如果你打算为其他语言撰写类似的快速入门,可参考 CONTRIBUTING.md 中的贡献规范,并模仿现有javascript.md的分节结构与注释风格。
十、常见误区速查
以下是本文全部知识点中初学者最容易踩坑的几处,整理成速查清单:
| 误区 | 正确理解 | 来源章节 |
|---|---|---|
==与===混用 | ==做隐式类型转换("5" == 5为 true),===不做(为 false);应始终用=== | 3.4 |
return换行 | ASI 会在return后插入分号,导致函数返回undefined | 2 |
省略var赋值 | 变量泄漏到全局作用域 | 4.1 |
for/in遍历数组 | for/in不保证数组索引顺序,数组遍历应用for或for/of | 5 |
this与定义位置 | this由调用方式决定;拆出方法调用会丢失上下文,可用call/apply/bind显式绑定 | 7.1–7.2 |
| 包装对象与原始值 | new Number(12)是对象(恒真值),与原始数字===不相等 | 7.6 |
| 箭头函数提升 | 箭头函数不可在定义前调用,不存在声明提升 | 8 |
结语
从数值与隐式转换、数组与对象、控制结构,到函数作用域、闭包、this绑定与原型链,再到let/const/箭头函数——learnxinyminutes-docs 的 JavaScript 教程用最少的篇幅覆盖了这门语言"反直觉"的核心机制。对照 仓库源码 逐行运行这些注释示例,是理解 JavaScript 真实行为最直接的方式;而原型链、闭包与this这三块,正是后续学习模块化、框架源码与现代异步编程的必备地基。
- 文档
- 教程
【免费下载链接】learnxinyminutes-docs
Code documentation written as code! How novel and totally my idea!
相关推荐
learnxinyminutes-docs 意大利语版 JavaScript 快速入门指南:从基础语法到原型链的完整速成教程
learnxinyminutes docs 意大利语版 JavaScript 快速入门指南:从基础语法到原型链的完整速成教程 本仓库(learnxinyminu
文档教程learnxinyminutes-docs 西班牙语版 JavaScript 入门指南:从基础语法到原型链的完整实战解析
learnxinyminutes docs 西班牙语版 JavaScript 入门指南:从基础语法到原型链的完整实战解析 本指南以 learnxinyminut
文档教程JavaScript 快速入门完全指南:基于 learnxinyminutes-docs 丹麦语教程的语法、作用域、原型链与 ES6 深度解析
JavaScript 快速入门完全指南:基于 learnxinyminutes docs 丹麦语教程的语法、作用域、原型链与 ES6 深度解析 本指南以 lea
文档教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考