Loading...
Loading...
Published on 2026-05-21
系统讲解参数校验最佳实践、?.与??语义精确性、断言函数设计、Object.freeze深冻结及Immer结构共享

输入校验的层次设计(前端 → 边界 → 业务层);null / undefined 的安全访问模式(可选链、空值合并、空对象模式);类型守卫与类型断言的运行时安全;Schema 驱动的数据校验(Zod / Yup 模式);不变性(Immutability)与数据保护;以及契约式设计(Design by Contract)的 JavaScript 实践。
防御式编程三原则:
1. 不信任任何外部输入(用户输入、API 响应、localStorage)
2. 尽早失败(Fail Fast)— 在错误传播前立即抛出
3. 明确表达意图 — 代码应说明"假设什么是安全的"
// ❌ 进攻性编程(假设一切正常)
function getUserName(user) {
return user.profile.name.trim();
// 如果 user 是 null、profile 不存在、name 是 null → 崩溃
}
// ✅ 防御性编程(明确处理异常情况)
function getUserName(user) {
if (!user || typeof user !== 'object') {
throw new TypeError(`getUserName: 期望 object,得到 ${typeof user}`);
}
return user?.profile?.name?.trim() ?? '匿名用户';
}
// ✅ 更激进的 Fail Fast 风格(前提:调用者有责任传正确数据)
function getUserNameStrict(user) {
assert(user != null, 'user 不能为 null/undefined');
assert(typeof user.profile?.name === 'string', 'user.profile.name 必须是字符串');
return user.profile.name.trim();
}
// 可选链(?.)— 短路求值,遇到 null/undefined 立即返回 undefined
const name = user?.profile?.name; // undefined 而非报错
const firstTag = post?.tags?.[0]; // 数组访问
const value = obj?.getValue?.(); // 方法调用
const length = (arr?.length) ?? 0; // 结合空值合并
// 空值合并(??)— 仅在 null/undefined 时使用默认值(区别于 ||)
const count = response.count ?? 0; // 0/false/''/NaN 不会被替换
const title = post.title ?? '无标题';
// 对比 || 的陷阱
const a = 0 || 'default'; // 'default'(0 被当成 falsy)
const b = 0 ?? 'default'; // 0(只有 null/undefined 才用默认值)
// 可选链赋值(??=、||=、&&=)
obj.cache ??= {}; // 仅在 null/undefined 时赋值
obj.count ||= 0; // 仅在 falsy 时赋值
obj.config &&= normalize(obj.config); // 仅在 truthy 时赋值
// 避免 null 检查蔓延到调用处
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.());
{
(value) { (value); }
= ();
() {
value != ? .(value) : .;
}
}
{
#value;
() { (); .#value = value; }
() { ; }
() { ; }
() { .((.#value)); }
() { (.#value); }
() { .#value; }
() { ; }
}
{
() { ; }
() { ; }
() { ; }
() { ; }
() { defaultVal; }
() { ; }
}
userName = .(user)
.( u.)
.( p.)
.( n.())
.();
// 基础类型守卫
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 === && v.(). > ,
: v === && v > ,
: .(v) && v. > ,
: ...(v) === ,
};
() {
( value !== ) {
();
}
value;
}
() {
(value == ) {
();
}
value;
}
() {
id = (data., );
name = (data., );
{ id, : name.() };
}
// 运行时 Schema 验证(轻量手写版)
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 ();
( v === && v < length)
();
});
;
}
() {
.#rules.( {
( v === && v. > length)
();
( v === && v > length)
();
});
;
}
() {
.#rules.( {
( v === && !regex.(v))
(message ?? );
});
;
}
() {
.#rules.(fn);
;
}
() {
errors = [];
( rule .#rules) {
{ (value); } (e) { errors.(e.); }
}
{ : errors. === , errors, value };
}
() {
result = .(value);
(!result.) (result..());
value;
}
}
{
#shape;
() {
.#shape = shape;
}
() {
(!is.(data)) {
();
}
result = {};
errors = {};
( [key, validator] .(.#shape)) {
validation = validator.(data[key]);
(!validation.) {
errors[key] = validation.;
} {
result[key] = data[key];
}
}
(.(errors). > ) {
err = ();
err. = errors;
err;
}
result;
}
}
= () => (name);
userSchema = ({
: ().().().().()
.(, ),
: ().().()
.(, ),
: ().().().(),
});
{
user = userSchema.(formData);
} (e) {
(e ) {
(e.);
}
}
// 模拟 Zod 的核心 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();
arr.( itemSchema.(item));
}
}),
: ({
: {
( s schemas) {
{ s.(v); } {}
}
();
}
}),
: ({
: v == ? : schema.(v)
})
};
= z.({
: z.(),
: z.(),
: z.({
: z.(),
: z.(),
}),
: z.(z.()),
: z.(z.()),
});
() {
raw = ().( r.());
.(raw);
}
// 浅冻结
const config = Object.freeze({
apiUrl: 'https://api.example.com',
timeout: 5000,
nested: { debug: false } // ❌ 嵌套对象未被冻结!
});
config.apiUrl = 'hack'; // 静默失败(严格模式抛出 TypeError)
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);
}
// 不变性 Proxy(开发环境检测意外修改)
function createImmutable(target, path = 'root') {
if (typeof target !== 'object' || target === null) return target;
return new Proxy(target, {
get(t, key) {
val = .(t, key);
( val === && val !== ) {
(val, );
}
val;
},
() {
();
},
() {
();
}
});
}
() {
(obj);
}
() {
keys = path.();
() {
(remainingKeys. === ) value;
[key, ...rest] = remainingKeys;
{
...current,
[key]: (current?.[key] ?? {}, rest)
};
}
(obj, keys);
}
state = { : { : { : } } };
newState = (state, , );
// 断言函数(开发环境开启,生产环境可关闭)
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( {
(!(result, ...args)) {
();
}
});
}
result;
};
};
}
divide = ({
: [ a === , b !== ],
: [ (result)]
})( () {
a / b;
});
() {
(is.(name), );
(is.(email) && .(email), );
(is.(age) && age >= && age <= , );
([, , ].(role), );
{
: crypto.(),
: name.(),
: email.(),
age,
role,
: ().()
};
}
// 管道模式:数据经过一系列转换,每步可能失败
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 {}
}
(e. === step.) e;
}
}
current;
}
}
jsonPipeline = ()
.( {
( raw !== ) ();
raw.();
}, { : })
.( {
{ .(str); }
{ (); }
}, { : })
.( {
.(data);
}, { : })
.( {
.(, error.);
;
});
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));
(raw === ) defaultValue;
parsed = .(raw);
schema = .#schema[name];
(schema) {
result = schema.(parsed);
(!result.) {
.();
.(name);
defaultValue;
}
}
parsed;
} (e) {
.(, e.);
defaultValue;
}
}
() {
{ .#storage.(.#(name)); } {}
}
}
storage = (, , {
: ({
: ().().(),
: ().().(),
})
});
storage.(, { : , : });
settings = storage.(, { : , : });
Q1:可选链 ?. 和 && 短路的区别?
&& 是"逻辑与",任何 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 等库。
Q3:前端数据校验能替代后端校验吗?
绝对不能。前端校验的目的是提升用户体验(即时反馈),后端校验是安全保障。前端 JS 代码可以被绕过、修改或禁用,恶意用户可以直接构造 HTTP 请求绕过前端。后端必须对所有输入进行独立验证,前后端应共享 Schema 定义(如通过 OpenAPI 生成)。
Q4:null 和 undefined 在防御性编程中应如何区分对待?
惯例上:undefined 表示"未初始化"或"不存在的属性",是系统级别的缺失;null 表示"有意义的空值",是业务层的"无"。函数参数应检查 == null(同时处理两者);对象属性的缺失通常用 undefined;表示"用户未设置"等业务语义用 null。?? 对两者统一处理,?. 同理。