Back to list防御式编程与数据校验
Site Owner
Published on 2026-05-21
系统讲解参数校验最佳实践、?.与??语义精确性、断言函数设计、Object.freeze深冻结及Immer结构共享
防御式编程与数据校验
输入校验的层次设计(前端 → 边界 → 业务层);null / undefined 的安全访问模式(可选链、空值合并、空对象模式);类型守卫与类型断言的运行时安全;Schema 驱动的数据校验(Zod / Yup 模式);不变性(Immutability)与数据保护;以及契约式设计(Design by Contract)的 JavaScript 实践。
目录
- 防御式编程核心思想
- Null 安全访问模式
- 类型守卫系统
- Schema 驱动校验
- 数据不变性保护
- 函数级契约设计
- 安全的数据转换管道
- 实战案例
- 深度追问
防御式编程核心思想
防御式编程三原则:
1. 不信任任何外部输入(用户输入、API 响应、localStorage)
2. 尽早失败(Fail Fast)— 在错误传播前立即抛出
3. 明确表达意图 — 代码应说明"假设什么是安全的"
进攻性编程 vs 防御性编程
function getUserName(user) {
return user...();
}
() {
(!user || user !== ) {
();
}
user?.?.?.() ?? ;
}
() {
(user != , );
( user.?. === , );
user...();
}
JavaScript防御式编程—参数校验、不可变数据与Optional Chaining|新宇宙博客profile
name
trim
function
getUserName
user
if
typeof
'object'
throw
new
TypeError
`getUserName: 期望 object,得到 ${typeof user}`
return
profile
name
trim
'匿名用户'
function
getUserNameStrict
user
assert
null
'user 不能为 null/undefined'
assert
typeof
profile
name
'string'
'user.profile.name 必须是字符串'
return
profile
name
trim
Null 安全访问模式
可选链与空值合并
const name = user?.profile?.name;
const firstTag = post?.tags?.[0];
const value = obj?.getValue?.();
const length = (arr?.length) ?? 0;
const count = response.count ?? 0;
const title = post.title ?? '无标题';
const a = 0 || 'default';
const b = 0 ?? 'default';
obj.cache ??= {};
obj.count ||= 0;
obj.config &&= normalize(obj.config);
Null Object 模式
class NullUser {
get id() { return null; }
get name() { return '访客'; }
get email() { return ''; }
get avatar() { return '/images/default-avatar.png'; }
isAuthenticated() { return false; }
hasPermission() { return false; }
toString() { return '[NullUser]'; }
}
const NULL_USER = Object.freeze(new NullUser());
function getCurrentUser(session) {
return session?.user ?? NULL_USER;
}
const user = getCurrentUser(session);
console.log(user.name);
console.log(user.isAuthenticated());
class Option {
static Some(value) { return new SomeOption(value); }
static None = new NoneOption();
static of(value) {
return value != null ? Option.Some(value) : Option.None;
}
}
class SomeOption extends Option {
#value;
constructor(value) { super(); this.#value = value; }
get isSome() { return true; }
get isNone() { return false; }
map(fn) { return Option.of(fn(this.#value)); }
flatMap(fn) { return fn(this.#value); }
getOrElse(_) { return this.#value; }
toString() { return `Some(${this.#value})`; }
}
class NoneOption extends Option {
get isSome() { return false; }
get isNone() { return true; }
map(_) { return this; }
flatMap(_) { return this; }
getOrElse(defaultVal) { return defaultVal; }
toString() { return 'None'; }
}
const userName = Option.of(user)
.map(u => u.profile)
.map(p => p.name)
.map(n => n.trim())
.getOrElse('匿名用户');
类型守卫系统
运行时类型检查
const is = {
string: (v) => typeof v === 'string',
number: (v) => typeof v === 'number' && !isNaN(v) && isFinite(v),
integer: (v) => Number.isInteger(v),
boolean: (v) => typeof v === 'boolean',
object: (v) => v !== null && typeof v === 'object' && !Array.isArray(v),
array: (v) => Array.isArray(v),
function: (v) => typeof v === 'function',
null: (v) => v === null,
undefined: (v) => v === undefined,
nullish: (v) => v == null,
defined: (v) => v != null,
nonEmptyString: (v) => typeof v === 'string' && v.trim().length > 0,
positiveNumber: (v) => typeof v === 'number' && v > 0,
nonEmptyArray: (v) => Array.isArray(v) && v.length > 0,
plainObject: (v) => Object.prototype.toString.call(v) === '[object Object]',
};
function assertString(value, name = 'value') {
if (typeof value !== 'string') {
throw new TypeError(`期望 ${name} 是 string,得到 ${typeof value}(值: ${JSON.stringify(value)})`);
}
return value;
}
function assertDefined(value, name = 'value') {
if (value == null) {
throw new TypeError(`${name} 不能为 null 或 undefined`);
}
return value;
}
function processUser(data) {
const id = assertString(data.id, 'data.id');
const name = assertString(data.name, 'data.name');
return { id, name: name.trim() };
}
复杂结构验证
class Validator {
#rules = [];
#name;
constructor(name = 'value') {
this.#name = name;
}
required() {
this.#rules.push((v) => {
if (v == null) throw new Error(`${this.#name} 是必填项`);
});
return this;
}
string() {
this.#rules.push((v) => {
if (v != null && typeof v !== 'string')
throw new TypeError(`${this.#name} 必须是字符串`);
});
return this;
}
min(length) {
this.#rules.push((v) => {
if (typeof v === 'string' && v.length < length)
throw new RangeError(`${this.#name} 最少 ${length} 个字符`);
if (typeof v === 'number' && v < length)
throw new RangeError(`${this.#name} 最小值为 ${length}`);
});
return this;
}
max(length) {
this.#rules.push((v) => {
if (typeof v === 'string' && v.length > length)
throw new RangeError(`${this.#name} 最多 ${length} 个字符`);
if (typeof v === 'number' && v > length)
throw new RangeError(`${this.#name} 最大值为 ${length}`);
});
return this;
}
pattern(regex, message) {
this.#rules.push((v) => {
if (typeof v === 'string' && !regex.test(v))
throw new Error(message ?? `${this.#name} 格式不正确`);
});
return this;
}
custom(fn) {
this.#rules.push(fn);
return this;
}
validate(value) {
const errors = [];
for (const rule of this.#rules) {
try { rule(value); } catch (e) { errors.push(e.message); }
}
return { valid: errors.length === 0, errors, value };
}
parse(value) {
const result = this.validate(value);
if (!result.valid) throw new ValidationError(result.errors.join('; '));
return value;
}
}
class ObjectSchema {
#shape;
constructor(shape) {
this.#shape = shape;
}
parse(data) {
if (!is.object(data)) {
throw new TypeError(`期望对象,得到 ${typeof data}`);
}
const result = {};
const errors = {};
for (const [key, validator] of Object.entries(this.#shape)) {
const validation = validator.validate(data[key]);
if (!validation.valid) {
errors[key] = validation.errors;
} else {
result[key] = data[key];
}
}
if (Object.keys(errors).length > 0) {
const err = new ValidationError('数据校验失败');
err.fields = errors;
throw err;
}
return result;
}
}
const v = (name) => new Validator(name);
const userSchema = new ObjectSchema({
username: v('用户名').required().string().min(3).max(20)
.pattern(/^[a-zA-Z0-9_]+$/, '用户名只能包含字母、数字和下划线'),
email: v('邮箱').required().string()
.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, '邮箱格式不正确'),
age: v('年龄').string().min(0).max(150),
});
try {
const user = userSchema.parse(formData);
} catch (e) {
if (e instanceof ValidationError) {
displayErrors(e.fields);
}
}
Schema 驱动校验
Zod 风格的链式 Schema API
const z = {
string: () => ({
min: (n) => ({ ...this, _min: n }),
max: (n) => ({ ...this, _max: n }),
email: () => ({ ...this, _email: true }),
parse: (v) => { return v; }
}),
number: () => ({ }),
object: (shape) => ({
parse: (data) => {
const result = {};
for (const [k, schema] of Object.entries(shape)) {
result[k] = schema.parse(data[k]);
}
return result;
}
}),
array: (itemSchema) => ({
parse: (arr) => {
if (!Array.isArray(arr)) throw new TypeError('期望数组');
return arr.map(item => itemSchema.parse(item));
}
}),
union: (...schemas) => ({
parse: (v) => {
for (const s of schemas) {
try { return s.parse(v); } catch {}
}
throw new TypeError('不匹配任何联合类型');
}
}),
optional: (schema) => ({
parse: (v) => v == null ? undefined : schema.parse(v)
})
};
const ArticleSchema = z.object({
id: z.string(),
title: z.string(),
author: z.object({
id: z.string(),
name: z.string(),
}),
tags: z.array(z.string()),
publishedAt: z.optional(z.string()),
});
async function fetchArticle(id) {
const raw = await fetch(`/api/articles/${id}`).then(r => r.json());
return ArticleSchema.parse(raw);
}
数据不变性保护
Object.freeze vs Proxy 不变性
const config = Object.freeze({
apiUrl: 'https://api.example.com',
timeout: 5000,
nested: { debug: false }
});
config.apiUrl = 'hack';
config.nested.debug = true;
function deepFreeze(obj) {
Object.getOwnPropertyNames(obj).forEach(name => {
const value = obj[name];
if (value && typeof value === 'object') {
deepFreeze(value);
}
});
return Object.freeze(obj);
}
function createImmutable(target, path = 'root') {
if (typeof target !== 'object' || target === null) return target;
return new Proxy(target, {
get(t, key) {
const val = Reflect.get(t, key);
if (typeof val === 'object' && val !== null) {
return createImmutable(val, `${path}.${String(key)}`);
}
return val;
},
set(_, key) {
throw new TypeError(`不允许修改不变对象: ${path}.${String(key)}`);
},
deleteProperty(_, key) {
throw new TypeError(`不允许删除不变对象属性: ${path}.${String(key)}`);
}
});
}
function safeClone(obj) {
return structuredClone(obj);
}
function updateNested(obj, path, value) {
const keys = path.split('.');
function update(current, remainingKeys) {
if (remainingKeys.length === 0) return value;
const [key, ...rest] = remainingKeys;
return {
...current,
[key]: update(current?.[key] ?? {}, rest)
};
}
return update(obj, keys);
}
const state = { user: { profile: { name: 'Alice' } } };
const newState = updateNested(state, 'user.profile.name', 'Bob');
函数级契约设计
前置条件 / 后置条件 / 不变量
function assert(condition, message) {
if (process.env.NODE_ENV !== 'production' && !condition) {
throw new Error(`断言失败: ${message}`);
}
}
function contract({ pre = [], post = [] } = {}) {
return function(fn) {
return function(...args) {
if (process.env.NODE_ENV !== 'production') {
pre.forEach((check, i) => {
if (!check(...args)) {
throw new Error(`前置条件 ${i} 失败: ${fn.name}(${args.join(', ')})`);
}
});
}
const result = fn.apply(this, args);
if (process.env.NODE_ENV !== 'production') {
post.forEach((check, i) => {
if (!check(result, ...args)) {
throw new Error(`后置条件 ${i} 失败: ${fn.name} → ${result}`);
}
});
}
return result;
};
};
}
const divide = contract({
pre: [(a, b) => typeof a === 'number', (a, b) => b !== 0],
post: [(result) => isFinite(result)]
})(function divide(a, b) {
return a / b;
});
function createUser({ name, email, age, role = 'user' } = {}) {
assert(is.nonEmptyString(name), `name 必须是非空字符串,得到: ${JSON.stringify(name)}`);
assert(is.string(email) && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email), `email 格式不正确: ${email}`);
assert(is.integer(age) && age >= 0 && age <= 150, `age 必须是 0-150 的整数`);
assert(['user', 'admin', 'moderator'].includes(role), `role 无效: ${role}`);
return {
id: crypto.randomUUID(),
name: name.trim(),
email: email.toLowerCase(),
age,
role,
createdAt: new Date().toISOString()
};
}
安全的数据转换管道
class Pipeline {
#steps = [];
#errorHandlers = [];
pipe(fn, options = {}) {
this.#steps.push({ fn, name: options.name ?? fn.name ?? 'anonymous' });
return this;
}
catch(handler) {
this.#errorHandlers.push(handler);
return this;
}
async run(input) {
let current = input;
for (const step of this.#steps) {
try {
current = await step.fn(current);
} catch (e) {
e.step = step.name;
e.input = current;
for (const handler of this.#errorHandlers) {
try {
const recovered = await handler(e, current);
if (recovered !== undefined) {
current = recovered;
break;
}
} catch {}
}
if (e.step === step.name) throw e;
}
}
return current;
}
}
const jsonPipeline = new Pipeline()
.pipe(raw => {
if (typeof raw !== 'string') throw new TypeError('输入必须是字符串');
return raw.trim();
}, { name: 'validate-input' })
.pipe(str => {
try { return JSON.parse(str); }
catch { throw new SyntaxError(`JSON 解析失败: ${str.slice(0, 100)}`); }
}, { name: 'parse-json' })
.pipe(data => {
return ArticleSchema.parse(data);
}, { name: 'validate-schema' })
.catch((error, input) => {
console.warn(`管道步骤 "${error.step}" 失败:`, error.message);
return null;
});
实战案例
安全的 localStorage 封装
class SafeStorage {
#storage;
#prefix;
#schema;
constructor(storage, prefix = 'app', schema = {}) {
this.#storage = storage;
this.#prefix = prefix;
this.#schema = schema;
}
#key(name) { return `${this.#prefix}:${name}`; }
set(name, value) {
try {
const schema = this.#schema[name];
if (schema) schema.parse(value);
this.#storage.setItem(this.#key(name), JSON.stringify(value));
return true;
} catch (e) {
console.warn(`SafeStorage.set(${name}) 失败:`, e.message);
return false;
}
}
get(name, defaultValue = null) {
try {
const raw = this.#storage.getItem(this.#key(name));
if (raw === null) return defaultValue;
const parsed = JSON.parse(raw);
const schema = this.#schema[name];
if (schema) {
const result = schema.validate(parsed);
if (!result.valid) {
console.warn(`SafeStorage: ${name} 数据格式已损坏,使用默认值`);
this.remove(name);
return defaultValue;
}
}
return parsed;
} catch (e) {
console.warn(`SafeStorage.get(${name}) 失败:`, e.message);
return defaultValue;
}
}
remove(name) {
try { this.#storage.removeItem(this.#key(name)); } catch {}
}
}
const storage = new SafeStorage(localStorage, 'myapp', {
settings: new ObjectSchema({
theme: v('theme').required().string(),
language: v('language').required().string(),
})
});
storage.set('settings', { theme: 'dark', language: 'zh-CN' });
const settings = storage.get('settings', { theme: 'light', language: 'en' });
深度追问
&& 是"逻辑与",任何 falsy 值(0、''、false、null、undefined)都会短路;?. 只在 null 或 undefined 时短路,0、false、'' 会继续向下访问。因此 obj?.count 会正确返回 0,而 obj && obj.count 在 obj 为 0 时短路返回 0(虽然通常 obj 不是数字,但体现了语义差异)。
Q2:Object.freeze 是真正的不变性吗?
不是。freeze 只是浅冻结,直接属性不可修改,但嵌套对象的属性仍然可以改变。且 freeze 只阻止属性的增删改,不阻止对象被垃圾回收或被 Object.assign 浅复制后修改副本。真正的深度不变性需要递归 deepFreeze 或使用 Immer/Immutable.js 等库。
绝对不能。前端校验的目的是提升用户体验(即时反馈),后端校验是安全保障。前端 JS 代码可以被绕过、修改或禁用,恶意用户可以直接构造 HTTP 请求绕过前端。后端必须对所有输入进行独立验证,前后端应共享 Schema 定义(如通过 OpenAPI 生成)。
Q4:null 和 undefined 在防御性编程中应如何区分对待?
惯例上:undefined 表示"未初始化"或"不存在的属性",是系统级别的缺失;null 表示"有意义的空值",是业务层的"无"。函数参数应检查 == null(同时处理两者);对象属性的缺失通常用 undefined;表示"用户未设置"等业务语义用 null。?? 对两者统一处理,?. 同理。