本文深入解析 JavaScript 中常用的高级函数模式,包括作用域安全的构造函数、惰性载入函数、函数绑定、柯里化、防抖与单次执行函数。通过具体代码示例,阐述如何提升代码性能、简化逻辑并确保全局对象安全,适用于前端开发与框架设计。
在 JavaScript 中,函数扮演着核心角色,它们被视为第一类对象,既可作为其他对象的方法存在,也能作为参数传递给其他函数,甚至可以作为返回值被函数返回。这种灵活性使得函数在 JS 中无处不在,掌握其精髓能让代码更加优雅简洁,反之则可能导致逻辑混乱。
除了基础语法,掌握并应用高级函数模式不仅能精简代码,还能显著提升运行性能。本文整理了一些常用且关键的高级函数技巧,并融入了个人理解。对于初学者,文中穿插了原型和 this 指向等基础知识,有助于降低理解门槛;对于资深开发者,这也是一份查漏补缺的参考资料。
正文
作用域安全的构造函数
作用域安全的构造函数
function Person(name,age){
this.name = name;
this.age = age;
}
var p1 = new Person("Claiyre",80);
对于上述构造函数,大家可能并不陌生。然而,如果调用者粗心大意,忘记使用 new 关键字来调用构造函数,会发生什么情况呢?
var p3 = Person("Tom",30);
console.log(p3); //undefined
console.log(window.name); //Tom
由于构造函数未进行安全检查,上述代码意外地修改了 window 对象的 name 属性。这是因为 this 对象的绑定发生在运行时:使用 new 调用时,this 指向新创建的对象;而不使用 new 直接调用时,this 则指向全局对象 window。
window 对象的 name 属性通常用于标识链接目标和 frame,因此对该属性的意外覆盖可能会引发其他难以排查的错误。
作用域安全的构造函数通过首先确认 this 对象是否为正确类型的实例来避免此类问题,随后才执行属性赋值,具体实现如下:
function Person(name,age){
if(this instanceof Person){
this.name = name;
this.age = age;
} else {
return new Person(name,age);
}
}
这种机制有效避免了在全局对象上意外更改或设置属性的风险。
实现这种安全模式相当于锁定了调用构造函数的环境。因此,单纯借用构造函数继承模式可能会遇到挑战,解决方案是结合使用原型链和构造函数模式,即采用组合继承方式。
如果您是 JavaScript 库或框架的开发者,作用域安全的构造函数将极具价值。在多人协作的项目中,为了防止团队成员意外修改全局对象,推荐使用这种构造函数模式。
惰性载入函数
惰性载入函数
由于不同浏览器之间存在行为差异,代码中往往包含大量检测浏览器特性的 if 语句。然而,一旦用户的浏览器支持某项特性,该特性将长期有效。因此,这些检测逻辑只需执行一次,即便只有一个 if 语句,其执行效率也优于未优化的代码。
惰性载入的核心思想是:函数执行的分支仅执行一次。第一种实现方式是在函数首次被调用时进行处理,并用检测到的结果重写原函数本身。
function detection(){
if(//支持某特性){
detection = function(){
//直接用支持的特性
}
} else if(//支持第二种特性){
detection = function(){
//用第二种特性
}
} else {
detection = function(){
//用其他解决方案
}
}
}
第二种实现惰性载入的方式是在声明函数时直接指定合适的函数逻辑。
var detection = (function(){
if(//支持某特性){
return function(){
//直接用支持的特性
}
} else if(//支持第二种特性){
return function(){
//用第二种特性
}
} else {
return function(){
//用其他解决方案
}
}
})();
惰性载入函数的优势在于仅在初次执行时牺牲少量性能,后续调用将不再产生额外的检测开销。
函数绑定作用域
函数绑定作用域
在 JavaScript 中,函数的作用域是在调用时动态绑定的,这意味着函数内部 this 的指向是不确定的。但在某些场景下,我们需要固定函数的执行作用域,使其始终指向特定对象。此时该如何处理?
这时可以使用函数绑定作用域的技术来实现。
function bind(fn,context){
return function(){
return fn.apply(context,arguments);
}
}
具体用法如下:
var person1 = {
name: "claiyre",
sayName: function(){
alert(this.name);
}
}
var sayPerson1Name = bind(person1.sayName,person1);
sayPerson1Name(); //claiyre
call 和 apply 方法可以临时改变函数的作用域,而 bind 函数则能返回一个绑定了特定作用域的新函数。
函数柯里化 (curry)
函数柯里化 (curry)
柯里化的概念相对简单:通过传递部分参数调用函数,函数返回另一个处理剩余参数的函数。这可以理解为赋予了函数“逐步加载”参数的能力。
许多 JavaScript 库都封装了柯里化函数,具体使用方式如下。
var match = curry(function(what,str){
return str.match(what)
});
var hasNumber = match(/[0-9]+/g);
var hasSpace = match(/\s+/g)
hasNumber("123asd"); //['123']
hasNumber("hello world!"); //null
hasSpace("hello world!"); //[' '];
hasSpace("hello"); //null
console.log(match(/\s+/g,'i am Claiyre')); //直接全部传参也可: [' ',' ']
一旦函数经过柯里化处理,我们可以先传递部分参数进行调用,从而获得一个更具体的函数。这个新函数通过闭包记住了首次传递的参数,最终我们可以利用这个具体函数完成后续操作。
一种较为简单的柯里化实现方式如下:
function curry(fn){
var i = 0;
var outer = Array.prototype.slice.call(arguments,1);
var len = fn.length;
return function(){
var inner = outer.concat(Array.prototype.slice.call(arguments));
return inner.length === len?fn.apply(null,inner):function (){
var finalArgs = inner.concat(Array.prototype.slice.call(arguments));
return fn.apply(null,finalArgs);
}
}
}
debounce 函数
debounce 函数
debounce 函数,也称为“去抖函数”。其功能直观:防止某个函数被连续高频调用,从而避免浏览器卡顿或崩溃。具体用法如下:
var myFunc = debounce(function(){
//繁重、耗性能的操作
},250);
window.addEventListener('resize',myFunc);
对于窗口 resize 等高频触发的事件,非常适合使用去抖函数(有时也称为“函数节流”),以降低对浏览器的性能负担。
具体实现逻辑是:当函数被调用时,不立即执行相关语句,而是等待固定的时间窗口 w。若在等待期间函数再次被调用,则重置计时器并继续等待 w 时间。重复此过程,直到最后一次调用后的 w 时间内函数未被再次调用,才执行相应的代码。
具体实现代码如下:
function debounce(fn,wait){
var td;
return function(){
clearTimeout(td);
td= setTimeout(fn,wait);
}
}
once 函数
once 函数
顾名思义,once 函数确保被执行的逻辑仅运行一次。具体实现如下:
function once(fn){
var result;
return function(){
if(fn){
result = fn(arguments);
fn = null;
}
return result;
}
}
var init = once(function(){
//初始化操作
})
在执行过一次后,参数 fn 被赋值为 null。因此,在后续调用时,条件判断不再进入 if 语句块,即该函数在首次调用后永远不会再次执行。
还可以对上述 once 函数进行改进,不仅传入函数,还可以同时指定作用域 u,从而同时实现 bind 和 once 的功能。
function once(fn,context){
var result;
return function(){
if(fn){
result = fn.apply(context,arguments);
fn = null;
}
return result;
}
}
结语
通过上述阅读,不难发现许多“高级函数”的实现其实并不复杂,通常数十行代码即可搞定。但关键在于真正理解其原理,并在实际开发中适时应用,以此提升性能,使代码更加简洁、逻辑更加清晰。