JS随笔:基础语法与控制结构
本篇是「JS随笔」系列中的基础篇,围绕 JavaScript 的基础语法与控制结构展开,覆盖变量与数据类型、常用运算符、表达式、条件与循环、值的类型转换与相等性判定等核心知识点,便于系统化查漏补缺与面试/复习使用。
原文地址
介绍
起源
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 等。
变量与数据类型
变量声明
使用 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 是复合类型,包括很多内置对象:
StringNumberBooleanObjectFunctionArrayDateRegExpError
tips:
null有时会被当作一种对象类型,但是这其实只是语言本身的一个bug, 即对 null 执行typeof null时会返回字符串object。实际上,null 本身是基本类型。
操作运算符
- 算术运算符:
+,-,*,/,%,**(指数) - 赋值运算符:
=,+=,-=,*=,/=,%= - 比较运算符:
==,===,!=,!==,>,<,>=,<= - 逻辑运算符:
&&,||,!
表达式
表达式是计算并产生值的代码片段。常见类型:
-
字面量表达式:数值、字符串、布尔值、对象、数组、函数、undefined、null。
let age = 30; let name = "Tom"; let flag = true; let now = undefined; let nothing = null; -
变量表达式
let x = 12; let y = x; -
算术表达式
let sum = 3 + 8; // 11 let difference = 3 - 2; // 1 -
赋值表达式
let score = 22; -
函数调用表达式
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"; -
逗号表达式
let x = (console.log("First"), 8);
控制结构
JS 控制结构用于流程控制,包括条件判断和循环。
-
条件语句
if (condition1) { // ... } else if (condition2) { // ... } else { // ... } switch (expression) { case value1: // ... break; case value2: // ... break; default: // ... } let result = condition ? value1 : value2; -
循环语句(for, while, do-while)
for (let i = 0; i < 10; i++) { console.log(i); // 0..9 } var j = 0; while (condition) { if (j === 5) break; console.log(j); // 0..4 } var k = 0; do { if (k === 5) break; console.log(k); // 0..4 } 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; } // ... }
值的类型转换
比较与计算中常见显式与隐式转换:
-
显示转换
let num = Number("123"); // 123 let num2 = parseInt("123", 10);// 123 let str2 = String(123); // "123" let b1 = Boolean(1); // true let b0 = Boolean(0); // false -
隐式转换
let result = "5" + 5; // "55" let result2 = [1, 2]+ [3, 4];// "1,23,4" let n1 = null + 1; // 1 let n2 = undefined + 1; // NaN let n3 = +"123"; // 123 let n4 = +"abc"; // NaN
宽松相等(==)与严格相等(===)
== 允许在相等比较中进行强制类型转换,而 === 不允许。
42 == "42"; // true
"42" == 42; // true
true == "42";// false(true -> 1)
"42" == true;// false(true -> 1)
null == undefined; // true
[42] == 42; // true
JS 中“假”值:""、0、-0、NaN、null、undefined、false
"0" == false; // true
false == 0; // true
"" == 0; // true
0 == []; // true
数组(概要)
数组是灵活的序列容器:
- 特性:动态大小、类型无关、索引从 0 开始
- 基础操作:创建、访问/修改、栈方法(
push/pop)、队列方法(shift/unshift) - 排序:
sort、reverse - 搜索:
indexOf、lastIndexOf - 迭代:
forEach、map、filter、reduce
在实际开发中,数组最容易踩的两个点是:
sort()默认按字符串比较:[10, 2, 3].sort()会得到['10','2','3']的字典序- 变异方法 vs 不变方法:
push/splice/sort/reverse会修改原数组,而slice/map/filter会返回新数组
一般建议:
- 与状态管理或 React/Vue 等框架配合时,优先选择返回新数组的方式,方便做“不可变更新”
- 需要频繁在中间插入/删除大量元素时,评估是否改用链表/映射等结构,避免数组频繁搬移元素
let numbers = [1, 2, 3, 4, 5];
numbers.push(6);
let squares = numbers.map((x) => x * x);
let sum = numbers.reduce((a, b) => a + b);
作用域与提升
- 词法作用域:由代码在编写时的结构决定,嵌套的作用域按从内到外的链条查找标识符
- 变量提升:
var声明会被提升;let/const存在暂时性死区,不可在声明前访问 - 函数提升:函数声明会整体提升;函数表达式不会提升其赋值结果
console.log(a); // undefined(var 提升)
var a = 1;
// let/const 的暂时性死区
// console.log(b); // ReferenceError
let b = 2;
foo(); // OK(函数声明提升)
function foo() { /* ... */ }
// bar(); // TypeError: bar is not a function
var bar = function() { /* ... */ };
this 绑定与调用位置
- 默认绑定:非严格模式下指向全局对象,严格模式下为
undefined - 隐式绑定:作为对象方法调用时,
this绑定到该对象 - 显式绑定:
call/apply/bind指定this - 构造调用:
new调用将this绑定到新实例 - 箭头函数:不绑定
this,捕获外层this
const obj = {
x: 1,
getX() { return this.x; }
};
obj.getX(); // 1(隐式绑定)
const getX = obj.getX;
getX(); // undefined 或报错(严格模式)
getX.call(obj); // 1(显式绑定)
function C() { this.v = 42; }
new C(); // { v: 42 }
const A = () => this;
A.call({ a: 1 }); // 仍为外层 this
函数与闭包
- 函数声明 vs 表达式:声明有提升,表达式按赋值时机可控
- 闭包:函数与其词法作用域的组合;可访问其外层作用域变量
- 常见用途:封装私有变量、部分应用、惰性计算、事件处理
理解闭包的关键在于:函数会“记住”自己定义时所在的词法环境,而不是调用时的环境。 当内部函数被返回或在其他地方被调用时,它仍然可以访问创建它时所在作用域中的变量, 这些变量会一直被保留,直到不再有任何闭包引用它们为止。
function makeCounter() {
let count = 0;
return function() {
count += 1;
return count;
};
}
const counter = makeCounter();
counter(); // 1
counter(); // 2
闭包也有一些常见坑:
- 捕获循环变量时,如果使用
var,所有闭包会共享同一个变量引用,容易出现“循环结束后值全都一样”的现象 - 闭包持有对外部对象的引用时,若不及时释放,会延长这些对象的生命周期,造成不必要的内存占用
柯里化(简述)
柯里化的核心思想是:把接收多个参数的函数拆解成一连串“每次只接收一个(或一部分)参数”的函数。 这样做的直接好处是:
- 更容易进行“预配置”:先固定一部分参数,得到语义更强的函数
- 便于函数组合:将复杂逻辑拆成多个简单小函数,再按需串联
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) return fn.apply(this, args);
return function(...rest) { return curried.apply(this, args.concat(rest)); };
};
}
const add = (a, b, c) => a + b + c;
const add1 = curry(add)(1);
add1(2, 3); // 6
在业务代码中,一个常见用法是对“工具函数”做部分应用:
const match = curry((re, str) => re.test(str));
const isPhone = match(/^1\d{10}$/);
const isEmail = match(/^[^@]+@[^@]+$/);
isPhone('13812345678'); // true
isEmail('foo@bar.com'); // true
柯里化适合配合函数式风格(如 map/filter/reduce 管道)使用,但在日常开发中无需“强行柯里化一切”; 更好的策略是:当你发现某些参数在大量调用中总是固定不变时,再考虑将该函数做成可柯里化的形式。
递归与尾调用
- 递归:函数调用自身解决更小子问题;注意终止条件与栈深
- 尾调用优化:某些实现可对尾调用进行优化,降低栈占用(具体取决于引擎)
function factorial(n, acc = 1) {
if (n <= 1) return acc;
return factorial(n - 1, acc * n);
}
factorial(5); // 120
错误与异常
- 抛出与捕获:
throw抛出异常;try/catch/finally捕获与收尾 - 错误类型:
TypeError、ReferenceError、SyntaxError、RangeError等 - 实践:仅在异常路径抛错;为预期失败返回明确结果或错误码
function parseJSONSafe(str) {
try {
return { ok: true, data: JSON.parse(str) };
} catch (e) {
return { ok: false, error: e.message };
} finally {
// 这里可以做资源清理或统计上报
}
}
在 async 函数中,try/catch 可以捕获 await 的异常:
async function fetchSafe(url) {
try {
const res = await fetch(url);
if (!res.ok) return { ok: false, error: res.statusText };
return { ok: true, data: await res.json() };
} catch (e) {
return { ok: false, error: e.message };
}
}
更通用的实践是:在边界层(如接口适配器、控制器)集中捕获错误,内部函数尽量返回结构化结果, 这样既保持调用方易用,又避免异常在系统内四处传播难以追踪。
正则表达式基础
- 声明:字面量
/pattern/flags或构造器new RegExp() - 常用标志:
i(忽略大小写)、g(全局)、m(多行)、s(点匹配换行) - 常用方法:
test、match、replace、split
从实战角度看,正则的价值主要体现在三个方面:
- 轻量校验:如手机号、邮箱、简单路径匹配
- 文本提取:日志解析、URL 参数截取等
- 批量替换:在重构或格式化场景下快速替换符合模式的片段
同时也有几个常见坑:
- 过于复杂的正则可读性极差,应考虑拆分成多个小步骤或配合解析器
- 带有
g标志的正则在多次test时会改变lastIndex,导致结果看似“随机”
在现代 JS 中,推荐尽量为复杂正则配备示例与注释,并在可能的情况下使用命名捕获与非捕获分组,提升可维护性。
/\d{3}-\d{4}/.test('123-4567'); // true
'abc123'.replace(/\d+/g, '#'); // 'abc#'
小结
- 以词法作用域与提升为基础理解运行时行为
- 正确选择
this绑定与函数形式,减少隐性 bug - 利用闭包与柯里化提升抽象能力与可维护性
- 通过递归/尾调用与正则表达式补足实战技能