JQuery学习文档(六)
第六部分:底层原理与架构设计

目录
6.1.1. 整体架构设计:沙箱模式 (IIFE) 与变量隔离
6.1.2. 无 new 构建机制:jQuery.fn.init 的巧妙返回
6.2.1. 原型链:jQuery.fn === jQuery.prototype
6.2.2. 深浅拷贝:$.extend() 源码逻辑与插件开发基础
6.2.3. 冲突处理:$.noConflict() 源码解析与多库共存
6.3.1. Sizzle 选择器引擎:词法分析 -> 块匹配 -> 过滤流程
6.3.3. 钩子机制:jQuery.cssHooks, jQuery.valHooks 的扩展能力
6.5.1. 数组与对象处理:$.map(), $.grep(), $.each(), $.merge()
6.5.2. 类型检测:$.type() 与 $.isPlainObject()
6.5.3. 函数绑定:$.proxy() 与 this 指向修正(对比 ES6 Arrow Functions)
6.1. jQuery 源码解析与架构思想
6.1.1. 整体架构设计:沙箱模式 (IIFE) 与变量隔离
优秀的库设计首先要解决的是“领土问题”。
|
设计要素 |
实现方式 |
核心价值 |
|
变量隔离 |
(function(window) { ... })(window) |
内部 var 变量外部不可访问,避免命名冲突。 |
|
全局注入 |
window.jQuery = window.$ = jQuery |
仅向全局暴露两个变量,接口极简。 |
|
依赖管理 |
参数传入 window |
压缩优化时可将 window 压缩为 w,提升压缩率。 |
jQuery 的源码整体被包裹在一个立即执行函数表达式(IIFE)中,这种沙箱模式构建了一个私有的作用域,确保内部定义的变量、辅助函数不会污染全局命名空间,同时也防止外部意外修改内部状态。
/* 源码简化结构演示 */
(function(window, undefined) {
var jQuery = function(selector) {
return new jQuery.fn.init(selector); /* 内部构造逻辑 */
};
/* 内部使用的工具函数,外部无法直接调用 */
var internalHelper = function() { /* ... */ };
/* 将 jQuery 挂载到全局对象 */
window.jQuery = window.$ = jQuery;
})(window); /* 立即执行,传入 window 对象 */
/* 尝试在外部访问内部变量 */
// console.log(internalHelper); /* 报错:internalHelper is not defined */
这就好比在一片公共土地上圈出了一块私人领地。
沙箱模式就像是盖了一座全封闭的工厂,工厂内部的机器(变量)和流程对外不可见,只在门口开了一个小小的收发室(window.$),外界只能通过这个窗口与工厂交互,既保护了商业机密,也维持了街道的整洁。
6.1.2. 无 new 构建机制:jQuery.fn.init 的巧妙返回
在使用 jQuery 时,无需像传统面向对象那样手动 new 一个实例,直接 $('div') 即可得到一个 jQuery 对象。
这种便捷性源于其内部实现了一个工厂模式:jQuery 函数内部通过 new 调用了原型上的 init 方法,自动完成了实例化过程。
/* jQuery 核心构造函数简化源码 */
var jQuery = function(selector, context) {
/* jQuery 函数直接返回 init 实例,无需用户手动 new */
return new jQuery.fn.init(selector, context);
};
/* 原型挂载 */
jQuery.fn = jQuery.prototype = {
constructor: jQuery,
length: 0,
/* ... */
};
/* 关键点:将 init 的原型指向 jQuery 的原型 */
jQuery.fn.init.prototype = jQuery.fn;
/* 验证 */
console.log($('body') instanceof jQuery); /* true */
console.log($('body') instanceof jQuery.fn.init); /* true */
这就像是自动售货机与人工小卖部的区别。
|
对比项 |
传统构造函数 |
jQuery 模式 |
|
调用方式 |
var obj = new MyLib(); |
var obj = $('selector'); |
|
写法冗余 |
需记住 new 关键字 |
极简,直接调用 |
|
实现原理 |
this 指向新实例 |
内部 new jQuery.fn.init() |
传统模式需要你自己去找店员,说“我要买个东西”,还要遵循一堆流程。
jQuery 模式则是自动售货机,你只需要投币按按钮(调用函数),机器内部自动完成出货流程,对用户来说极其直观,完全感知不到内部复杂的实例化过程。
6.1.3. 链式调用实现:return this 的奥秘
jQuery 最著名的特性之一是其“链式调用”能力,允许在一行代码中连续执行多个操作。
这背后的原理极其简单且优雅:每个原型方法在执行完毕后,都显式返回了 this,即调用该方法的对象本身,从而为下一个方法调用提供上下文。
/* 模拟链式调用实现 */
var myLib = {
value: 0,
add: function(num) {
this.value += num;
return this; /* 关键:返回对象自身 */
},
multiply: function(num) {
this.value *= num;
return this; /* 关键:返回对象自身 */
},
print: function() {
console.log(this.value);
return this; /* 保持链式能力 */
}
};
/* 实际调用 */
myLib.add(5).multiply(2).print(); /* 输出 10 */
/* jQuery 源码中的实现片段 */
jQuery.fn.extend({
addClass: function(value) {
/* ...逻辑代码... */
return this; /* 绝大多数方法都以此结尾 */
}
});
这就像是接力赛跑。
第一棒跑完,必须把接力棒交回给跑道,第二棒才能继续跑。
return this 就是把“当前对象”这根接力棒交回给调用者,让下一个方法能够无缝衔接。
如果哪个方法中途“掉棒”(返回了其他数据),整个链条就断了。
6.2. 原型与扩展
6.2.1. 原型链:jQuery.fn === jQuery.prototype
jQuery.fn 是 jQuery.prototype 的简写别名。
理解这一点对于插件开发至关重要。
|
属性/方法 |
归属对象 |
调用方式 |
典型成员 |
|
静态方法 |
jQuery (构造函数) |
$.ajax(), $.each() |
工具类函数,与 DOM 无关 |
|
实例方法 |
jQuery.fn (原型) |
$('div').css() |
操作 DOM 集合的方法 |
所有 jQuery 对象实例共享的原型方法都定义在这个对象上,将工具方法与实例方法分离,极大地节省了内存开销。
/* 验证两者指向同一内存地址 */
console.log(jQuery.fn === jQuery.prototype); /* true */
/* 扩展实例方法 (插件开发的本质) */
jQuery.fn.highlight = function(color) {
/* this 指向当前的 jQuery 实例对象 */
this.css('background-color', color);
return this; /* 保持链式 */
};
/* 立即可用 */
$('p').highlight('yellow');
原型链就像是“族谱”或“家规”。
jQuery 是老祖宗,jQuery.fn 是家规手册。
老祖宗规定,所有出生的子孙(jQuery 实例对象)都必须遵守家规。
只要在家规手册上加一条规则,所有子孙立刻学会这项新技能,而不需要给每个人单独培训。
6.2.2. 深浅拷贝:$.extend() 源码逻辑与插件开发基础
$.extend() 是 jQuery 中实现对象合并的核心工具,同时也是插件开发时合并默认配置与用户配置的基础。
|
调用形式 |
第一个参数 |
行为描述 |
|
浅拷贝 |
false (默认) |
遇到对象属性只复制引用,修改会互不影响。 |
|
深拷贝 |
true |
递归复制对象内部结构,生成全新的副本。 |
|
插件扩展 |
jQuery.fn |
将对象属性复制到 jQuery 原型上,实现插件挂载。 |
其逻辑核心在于对 deep 参数的判断,决定了是递归复制(深拷贝)还是仅复制引用(浅拷贝)。
var defaults = { mode: 'edit', config: { theme: 'dark' } };
var options = { config: { theme: 'light' } };
/* 浅拷贝 - 引用被覆盖,defaults.config 丢失 */
var settings1 = $.extend({}, defaults, options);
/* 结果: { mode: 'edit', config: { theme: 'light' } } */
/* 深拷贝 - 递归合并,保留 defaults 中未覆盖的属性 */
var settings2 = $.extend(true, {}, defaults, options);
/* 结果: { mode: 'edit', config: { theme: 'light' } } */
/* 注:如果 options 未提供其他 config 属性,defaults 中的属性会被保留 */
/* 插件开发标准模板 */
$.fn.myPlugin = function(options) {
var opts = $.extend({}, $.fn.myPlugin.defaults, options);
/* 使用合并后的配置 opts 进行逻辑处理 */
};
$.fn.myPlugin.defaults = { color: 'red', size: 12 }; /* 默认配置暴露出来以便修改 */
深浅拷贝就像是复印文件。
浅拷贝只是在复印件上写了个“详见原件”,原件改了,复印件看到的也变了。
深拷贝则是把原件里的每一张图、每一个字都重新描了一遍,生成了一份完全独立的新文件,原件怎么改都不影响复印件。
6.2.3. 冲突处理:$.noConflict() 源码解析与多库共存
在 jQuery 流行之初,$ 符号是极其抢手的变量名。
为了解决与其他库(如 Prototype、MooTools)的冲突,jQuery 提供了 $.noConflict(),其原理是在库加载时先保存全局中现有的 $,调用该方法时再恢复旧值。
/* 模拟源码逻辑 */
var _$ = window.$; /* 加载时:先备份全局原有的 $ */
jQuery.noConflict = function(removeAll) {
if (removeAll) {
window.jQuery = undefined; /* 彻底让出 jQuery 变量 */
}
window.$ = _$; /* 恢复备份的旧 $ */
return jQuery; /* 返回 jQuery 对象引用,用于重命名 */
};
/* 实际使用场景 */
/* 场景一:让出 $,仅使用 jQuery */
$.noConflict();
jQuery('div').hide(); /* 只能用全名 */
/* 场景二:自定义别名 */
var j = $.noConflict();
j('div').hide(); /* 使用 j 作为别名 */
/* 场景三:在闭包内安全使用 $ */
$.noConflict();
(function($) {
/* 这里的 $ 局部变量指向 jQuery,不受外部影响 */
$('div').show();
})(jQuery);
这就像是绅士的礼仪。
jQuery 进门发现座位($)上已经有人了,它先把那个人记在心里(备份),然后自己站着。
当你喊“让座”时,它立刻把座位还给原来的人,并递给你一张写着自己名字(jQuery)的名片,告诉你以后可以用这个名字叫他。
6.3. 核心引擎剖析
6.3.1. Sizzle 选择器引擎:词法分析 -> 块匹配 -> 过滤流程
jQuery 的选择器功能强大,核心依赖于其内置的 Sizzle 引擎。
|
解析阶段 |
执行逻辑 |
示例 (div.card > p:first) |
|
词法分析 |
将选择器字符串拆分为 Token 数组 |
['div', '.card', '>', 'p', ':first'] |
|
块匹配 |
从右向左查找,利用原生 API 加速 |
先找 p,再过滤父级是否为 div.card |
|
过滤筛选 |
根据关系符 (>, ~) 和伪类过滤 |
验证父子关系,执行 :first 逻辑 |
Sizzle 是一个纯 JavaScript 实现的 CSS 选择器引擎,它不依赖浏览器原生的 querySelectorAll(虽然会优先使用),而是实现了一套完整的解析逻辑,保证了跨浏览器的一致性。
/* Sizzle 伪类扩展:自定义选择器 */
$.expr[':'].inline = function(elem) {
/* 返回 true 表示匹配成功 */
return $(elem).css('display') === 'inline';
};
/* 使用自定义选择器 */
$('div:inline').css('border', '1px solid red');
Sizzle 就像是一个精密的侦探。
拿到通缉令(选择器字符串)后,它不会盲目地满城搜查,而是先把通缉令拆解成特征(词法分析)。
如果通缉令说“住在富人区、穿红衣服的人”,侦探会先去富人区蹲点,而不是在全市搜查。
这种“从右向左”的查找策略,大幅缩小了搜索范围,提升了效率。
6.3.2. 回溯查找:pushStack 方法维护栈结构
jQuery 对象内部维护了一个栈结构,用于记录 DOM 查找的历史。
当使用 .find()、.children() 等方法缩小范围时,新结果集会被推入栈顶,而 .end() 方法则能弹出栈顶元素,回退到上一级状态。
这一机制是实现复杂链式操作的关键。
/* 链式操作中的状态回溯 */
$('ul.list') /* 1. 选中所有 ul */
.find('li') /* 2. 在 ul 中找 li (入栈) */
.addClass('item') /* 给 li 加样式 */
.end() /* 3. 回退到 ul (出栈) */
.addClass('container'); /* 给 ul 加样式 */
/* 源码简化模拟 */
pushStack: function(elems) {
var ret = jQuery.merge(this.constructor(), elems);
ret.prevObject = this; /* 关键:保存上一级引用 */
return ret;
},
end: function() {
return this.prevObject || this.constructor(null);
}
这就像是浏览器的前进/后退按钮。
- .find() 就像是点击了一个链接进入新页面,把当前页面压入历史记录栈。
- .end() 就像是点击后退按钮,浏览器会从栈中取出上一个页面显示。
这种栈结构让 DOM 操作拥有了“记忆”,可以在复杂的层级中自由穿梭。
6.3.3. 钩子机制:jQuery.cssHooks, jQuery.valHooks 的扩展能力
为了解决浏览器兼容性问题,jQuery 引入了钩子机制。
在处理 CSS 属性或表单值时,jQuery 不直接操作 DOM,而是先查询钩子对象。
开发者可以通过覆盖钩子,实现对特殊属性(如 opacity 在 IE 下的处理)或特殊表单元素(如 select-multiple)的自定义逻辑。
|
钩子类型 |
作用对象 |
典型应用 |
|
$.cssHooks |
.css() 方法 |
修复 opacity 兼容性,添加自定义属性 margin-center。 |
|
$.valHooks |
.val() 方法 |
处理 option 标签的取值逻辑,处理单选框/复选框状态。 |
|
$.attrHooks |
.attr() 方法 |
兼容旧版 IE 中 href 或 type 属性的读写。 |
示例:通过 CSS Hook 实现自定义属性
$.cssHooks.marginCenter = {
get: function(elem) {
return $(elem).css('margin-left') === 'auto';
},
set: function(elem, value) {
/* 当设置 marginCenter 为 true 时,自动居中 */
if (value) {
$(elem).css({
'margin-left': 'auto',
'margin-right': 'auto'
});
}
}
};
/* 使用 */
$('.box').css('marginCenter', true); /* 自动设置水平居中 */
钩子机制就像是操作系统的驱动程序接口。
jQuery 是操作系统,DOM 是硬件。
有些标准硬件直接用就行,但对于特殊的硬件(老旧浏览器或特殊属性),就需要安装特定的“驱动”。
钩子就是这些驱动程序,让上层应用(API 调用)不用关心底层细节,只要调用标准接口即可。
6.4. 插件开发与生态
6.4.1. 插件开发规范:标准模板、闭包封装与默认配置
一个高质量的 jQuery 插件应遵循特定的代码规范,确保其健壮性、可维护性与生态兼容性。
标准模板通常包括 IIFE 封装、避免 $ 冲突、提供默认配置、保持链式调用以及对集合迭代的支持。
/* 标准插件开发模板 */
;(function($, window, document, undefined) {
/* 1. 构造函数定义 */
var Plugin = function(element, options) {
this.elem = element;
this.$elem = $(element);
this.options = $.extend({}, Plugin.DEFAULTS, options);
this.init(); /* 初始化逻辑 */
};
/* 2. 默认配置 */
Plugin.DEFAULTS = {
color: '#333',
fontSize: '14px'
};
/* 3. 原型方法 */
Plugin.prototype = {
init: function() {
this.$elem.css({
'color': this.options.color,
'font-size': this.options.fontSize
});
},
destroy: function() {
this.$elem.removeData('pluginName'); /* 清理数据 */
}
};
/* 4. 挂载到 jQuery */
$.fn.pluginName = function(options) {
return this.each(function() { /* 支持 each 迭代 */
var $this = $(this);
var instance = $this.data('pluginName');
if (!instance) {
instance = new Plugin(this, options);
$this.data('pluginName', instance); /* 单例存储 */
}
if (typeof options === 'string') {
instance[options](); /* 支持方法调用 */
}
});
};
})(jQuery, window, document);
|
规范要素 |
要求 |
目的 |
|
闭包封装 |
(function($){...})(jQuery) |
避免污染全局,解决 $ 冲突。 |
|
分号开头 |
;(function... |
防止前一个脚本未闭合导致的错误。 |
|
链式保持 |
return this.each(...) |
确保插件方法返回 jQuery 对象本身。 |
|
单例模式 |
data() 存储实例 |
防止重复初始化,便于获取插件状态。 |
这就像是工业化的模具设计。
标准模板定义了插件的“安全壳”和“接口槽”。
无论内部逻辑多复杂,外壳必须符合标准尺寸,这样用户在使用时,无论是拧螺丝(配置参数)还是接线(方法调用),都能与现有的系统完美适配。
6.4.2. 最佳实践:链式调用保持、单例模式与数据管理
在插件开发中,不仅要实现功能,更要关注性能与用户体验。
|
实践模式 |
错误示例 |
正确示例 |
原因分析 |
|
返回值 |
return this.css(...) |
return this.each(...) |
支持集合操作,保证 this 指向正确。 |
|
状态存储 |
全局变量存储 |
$(elem).data('key') |
避免内存泄漏,关联数据与元素生命周期。 |
|
默认配置 |
硬编码在逻辑中 |
$.extend(defaults, opts) |
提高灵活性,用户可覆盖配置。 |
核心原则包括:始终保持链式调用能力、使用 data() 方法管理实例状态、以及提供合理的销毁机制。
/* 最佳实践示范 */
$.fn.smartToggle = function(options) {
var settings = $.extend({ speed: 'fast' }, options);
return this.each(function() {
var $btn = $(this);
var state = $btn.data('toggle-state'); /* 读取状态 */
if (!state) {
$btn.data('toggle-state', { count: 0 }); /* 初始化状态 */
}
$btn.on('click', function() {
$btn.toggleClass('active');
var count = $btn.data('toggle-state').count + 1;
$btn.data('toggle-state').count = count;
console.log('点击次数:', count);
});
});
};
插件开发就像是装修房子。
链式调用保证了装修完一个房间能立刻装修下一个,不用重新请人。
数据管理则是给每个房间装了独立的电表,互不干扰。
而单例模式确保了同一个房间不会被两个装修队同时乱改,避免了“撞车”事故。
6.4.3. 经典插件生态:轮播、弹窗与表单验证的集成思路
jQuery 生态中诞生了无数经典插件,它们解决特定问题的思路至今仍有借鉴价值。
|
插件类型 |
代表作 |
核心逻辑 |
集成难点 |
|
轮播图 |
Slick, Owl Carousel |
定时器 + CSS Transform 位移 |
触屏事件支持、响应式宽度计算 |
|
弹窗层 |
Layer, Bootstrap Modal |
遮罩层管理 + 焦点捕获 + ESC 关闭 |
z-index 层级管理、Body 滚动锁定 |
|
表单验证 |
jQuery Validation |
规则对象匹配 + 事件监听 |
异步验证、错误提示的 DOM 插入位置 |
理解这些插件的集成逻辑,有助于在现代开发中更好地复用代码或迁移方案。
/* 集成思路示例:简易表单验证 */
$.fn.simpleValidate = function(rules) {
return this.on('submit', function(e) {
var $form = $(this);
var isValid = true;
/* 遍历规则 */
$.each(rules, function(field, rule) {
var val = $form.find('[name=' + field + ']').val();
if (!rule.test(val)) {
alert(rule.message || '格式错误');
isValid = false;
return false; /* 跳出循环 */
}
});
if (!isValid) e.preventDefault(); /* 阻止提交 */
});
};
/* 调用 */
$('#myForm').simpleValidate({
username: { test: /\w{3,}/, message: '用户名至少3位' },
email: { test: /@/, message: '邮箱格式不正确' }
});
经典插件就像是建筑里的预制件。
轮播图是旋转门,弹窗层是活动隔断,表单验证是安检门。
它们把复杂的机械结构封装在内部,只留下简单的安装接口。
理解了这些预制件的安装原理,即使面对现代框架,也能快速构建出功能完善的建筑。
6.5. 现代工具方法集
6.5.1. 数组与对象处理:$.map(), $.grep(), $.each(), $.merge()
在 ES5 标准普及之前,jQuery 提供了一套完善的数组与对象处理工具,弥补了原生 JavaScript 在集合操作上的不足。
|
方法 |
等效原生方法 |
核心差异 |
|
$.each() |
Array.prototype.forEach |
可遍历对象,回调参数顺序为。 |
|
$.map() |
Array.prototype.map |
可遍历对象,返回 null 时自动过滤该项。 |
|
$.grep() |
Array.prototype.filter |
仅用于数组,无法过滤对象。 |
|
$.merge() |
Array.prototype.concat |
原地修改第一个数组,而非生成新数组。 |
虽然现代开发推荐优先使用原生方法,但在维护旧项目或处理类数组对象时,这些工具依然高效。
var arr = [1, 2, 3, 4, 5];
/* $.grep: 过滤偶数 */
var evens = $.grep(arr, function(n, i) {
return n % 2 === 0;
}); /* 结果: [2, 4] */
/* $.map: 值翻倍 */
var doubled = $.map(arr, function(n) {
return n * 2;
}); /* 结果: [2, 4, 6, 8, 10] */
/* $.merge: 合并数组 (注意是修改原数组) */
var target = [1, 2];
$.merge(target, [3, 4]);
/* target 变为 [1, 2, 3, 4],这点与 concat 不同 */
这些工具方法就像是瑞士军刀里的基础工具。
虽然现在有了专业的螺丝刀套装(原生 ES6 数组方法),但在某些紧急情况或老旧环境中,这把军刀里的小剪刀($.grep)和小锉刀($.merge)依然能快速解决问题,且对“不规则的木头”(类数组对象)有更好的适应性。
6.5.2. 类型检测:$.type() 与 $.isPlainObject()
JavaScript 的 typeof 运算符在判断 null、Array、Date 等类型时存在诸多缺陷。
|
检测对象 |
typeof 结果 |
$.type() 结果 |
$.isPlainObject() |
|
null |
"object" |
"null" |
false |
|
[1, 2] |
"object" |
"array" |
false |
|
new Date() |
"object" |
"date" |
false |
|
{ a: 1 } |
"object" |
"object" |
true |
|
function(){} |
"function" |
"function" |
false |
jQuery 封装了 $.type() 方法,利用 Object.prototype.toString.call() 返回精确的类型字符串,并提供了 $.isPlainObject() 用于判断纯粹的对象字面量。
/* 精确类型判断 */
console.log($.type(null)); /* "null" */
console.log($.type(document.body)); /* "element" (原生 typeof 是 object) */
console.log($.type(/regex/)); /* "regexp" */
/* 纯对象判断 (常用于深拷贝或插件配置) */
var obj = { name: 'test' };
var cls = new Object(); /* 虽然 cls 也是对象,但某些场景被视为纯净 */
var inst = new $.fn.init(); /* 实例对象 */
console.log($.isPlainObject(obj)); /* true */
console.log($.isPlainObject(inst)); /* false */
typeof 就像是一个只能分辨“有腿”和“没腿”的幼稚门卫,把桌子、椅子和人都统称为“有腿的物体”。
$.type() 则是配备了 DNA 检测仪的高级安检员,能透过表象准确识别出“这是桌子”、“那是椅子”。
而 $.isPlainObject() 更是专门的“纯净度检测”,只对那些没有任何额外构造函数污染的“纯净”数据放行。
6.5.3. 函数绑定:$.proxy() 与 this 指向修正(对比 ES6 Arrow Functions)
在事件绑定或定时器回调中,函数内部的 this 往往会丢失或改变。
|
方案 |
语法 |
原理 |
兼容性 |
|
$.proxy() |
$.proxy(fn, context) |
利用闭包保存 context |
全兼容 |
|
ES5 Bind |
fn.bind(context) |
原生方法,返回新函数 |
IE9+ |
|
ES6 Arrow |
() => { ... } |
词法作用域继承 this |
现代浏览器 |
jQuery 提供了 $.proxy() 方法,用于强制绑定函数的执行上下文。
虽然在 ES6 中箭头函数已完美解决此问题,但在旧代码重构中 $.proxy() 仍有一席之地。
var obj = {
name: 'Module A',
init: function() {
/* 错误示范:setTimeout 回调中 this 指向 window */
setTimeout(function() {
console.log(this.name); /* 输出: undefined */
}, 100);
/* 方案一:使用 $.proxy 修正 this */
setTimeout($.proxy(function() {
console.log(this.name); /* 输出: Module A */
}, this), 100);
/* 方案二:ES6 箭头函数 (推荐) */
setTimeout(() => {
console.log(this.name); /* 输出: Module A */
}, 100);
}
};
/* 事件处理中的 proxy */
$('#btn').on('click', $.proxy(obj.handler, obj));
/* 确保 handler 中的 this 指向 obj 而非 #btn */
this 的指向问题就像是变形金刚的身份识别。
普通函数是变色龙,到了哪个环境就变成那个环境的颜色。
$.proxy() 就像是给变形金刚穿上了一件写有名字的固定制服,无论走到哪里,大家都知道它是谁。
而箭头函数则像是拥有坚定信念的战士,无论身处何地,始终记得自己的家乡是哪里。
更多推荐




所有评论(0)