本文详细解析 JavaScript 函数的核心用法,涵盖三种定义方式及其解析差异。深入探讨参数处理、this 指向规则及 call/apply/bind 方法。同时讲解执行环境、作用域链、垃圾回收机制以及闭包和柯理化函数的原理与应用。
本文详细介绍 JavaScript 中函数的具体用法,旨在为读者提供实用的参考与学习素材。
初始函数
在 JavaScript 中,Function 类型代表了函数的基本类型。
一个标准的 JavaScript 函数通常包含以下结构:
function 函数名称(参数表){
//函数执行部分
return ;
}
//注意:参数列表直接写形参名即可
return 语句用于返回函数的计算结果并终止函数的执行。此外,函数本身也可以被视为一种数据对象进行传递。参数列表构成了函数的输入接口,而 return 语句则构成了输出接口。
函数可以作为参数传递给其他函数使用。
function test ( a ) {
a();
}
test(function () {
alert(1);
});
函数支持嵌套定义,即在一个函数内部可以定义另一个函数。
function test2(){
function test3(){
alert(1);
}
test3();
}
test2();
定义函数
JavaScript 提供了三种主要的函数定义方式:
包括 function 语句形式、函数直接量形式以及通过 Function 构造函数形式来定义函数。
//1 function 语句式
function test1 () {
alert(1);
}
//2 函数直接量 (ECMAScript 推荐)
var test2 = function () {
alert(2);
}
//3 function 构造函数式
var test3 = new Function('a','b','return a+b;'); //最后一个参数是函数结构体
test3(10,20);
| function语句 | Function构造函数 | 函数直接量 | |
|---|---|---|---|
| 兼容 | 完全 | js1.1以上 | js1.2以上版本 |
| 形式 | 句子 | 表达式 | 表达式 |
| 名称 | 有名 | 匿名 | 匿名 |
| 性质 | 静态 | 动态 | 静态 |
| 解析时机 | 优先解析 | 顺序解析 | 顺序解析 |
| 作用域 | 具有函数的作用域 | 顶级函数(顶级作用域) | 具有函数作用域 |
静态与动态的区别
var d1 = new Date();
var t1 = d1.getTime();
for ( var i=0; i<10000000; i++ ) {
// function test1 () {} // 342ms //在函数体只会被声明一次 ,其它地方并不再解析,就可以调用 。 //静态 //只会编译一次,放入内存中。
var test2 = function () {} //354ms
// var test3 = new Function(); //8400ms //每次执行,都是重新new一个 函数。 //new 完之后, 就销毁了。不占用内存。动态创建一次。
}
var d2 = new Date();
var t2 = d2.getTime();
console.log( t2 - d1 );
不同定义方式的解析顺序存在差异。
function f () {
return 1;
} // 函数1
alert( f() ); //返回值为4 说明第1个函数被第4个函数覆盖
var f = new Function("return 2;"); // 函数2
alert( f() ); //返回值为2 说明第4个函数被第2个函数覆盖
var f = function () {
return 3;
} // 函数3
alert( f() ); //返回值为3 说明第2个函数被第3个函数覆盖
function f () {
return 4;
} // 函数4
alert(f()); //返回值为3 说明第4个函数被第3个函数覆盖
var f = new Function("return 5"); // 函数5
alert(f()); //返回值为5 说明第3个函数被第5个函数覆盖
var f = function(){
return 6;
} // 函数6
alert(f()); //返回值为6 说明第5个函数被第6个函数覆盖
函数作用域决定了变量的可见性和生命周期。
var k = 1 ;
function t1(){
var k = 2 ;
// function test(){return k ;} //2
// var test = function(){ return k}; //2
var test = new Function('return k;'); //1 //new Function(); 是会有顶级作用域
alert(test());
}
t1();
函数的参数 arguments
arguments 对象是实际传入参数的副本,允许在函数内部访问所有实参。
//js 中 函数分为 : 形参,实参
function test ( a,b,c,d ) {
// console.log( test.length ); //获取形参的个数 4
//函数的实际参数 内部就是使用一个数组去接收实际参数。 类数组对象
//arguments 对象,只能在函数内部使用。
//arguments 对象, 可以访问函数的实际参数 (实参的副本)
// console.log( arguments.length ); //2
// console.log( arguments[0] ); //10
// // 第一种方式:
// if ( test.length === arguments.length ) {
//
// return a + b;
//
// }
//使用第二种 方式:
if ( arguments.callee.length === arguments.length ) {
return a + b;
}
//arguments对象, 使用得最多的还是使用递归操作
// arguments.callee; // 指向函数本身,函数体
}
test(10,20);
this 对象的简单理解
this 对象是在函数运行时根据具体的执行环境动态绑定的。
在全局函数中,this 指向 window 对象;当函数作为某个对象的方法被调用时,this 指向该对象。
这意味着 this 关键字始终指向调用当前函数的对象(即谁调用了我,它就指向谁)。
//this: this对象是指在运行时期基于执行环境所绑定的
var k = 10;
function test () {
this.k = 20;
}
test();
console.log( test.k ); //undefined
call 和 apply 方法
每个函数都包含两个非继承的方法:call 和 apply。这两个方法的主要用途是在特定的作用域中调用函数,实际上就是设置函数体内 this 对象的值。
call 和 apply 除了传递参数外,更强大的功能在于能够扩充函数赖以运行的作用域,使得作用域可以动态变化。
使用 call() 或 apply() 来扩充作用域的最大优势在于,目标对象无需与该方法存在任何耦合关系。
通过 fn.call(obj) 执行,可以让 fn 以 obj 的身份运行。
这可以将一个函数绑定到特定的作用域中,并传递该作用域下的参数。
//call, apply 简单 用法: 绑定一些函数 用于传递参数 调用
function sum ( x,y ) {
return x + y;
}
function call1 ( num1,num2 ) {
return sum.call(this,num1,num2);
}
function apply1 ( num1,num2 ) {
return sum.apply(this,[num1,num2])
}
//将一个函数绑定到一个特定的作用域中,然后传递特定作用域中的参数。
console.log( call1(10,10) );
console.log( apply1(20,10) );
//扩充作用域,底层也经常使用这两个方法,用于绑定不同的作用域。
//把一个函数赋给一个对象, 赋完之后,还可以重用,赋给另外一个对象。
window.color = 'pink';
var obj = {
color: 'tan'
}
function showColor () {
console.log( this.color );
}
showColor.call(window);
// showColor.call(this);
showColor.apply(obj);
call 方法简单的实现
function test1 ( a,b ) {
return a + b;
}
//自定义对象
function Obj ( x,y ) {
return x * y;
}
var obj = new Obj();
//挂载到对象上
obj.method = test1;
//执行该函数
obj.method(10,20);
//执行完后删除
delete obj.method;
bind
ES5 引入了 bind() 方法,用于为函数绑定执行时的作用域。该方法被添加到 Function 的原型上,因此所有函数都具备此方法。bind() 在添加作用域时并不立即执行方法,而是在方法实际执行时,将作用域切换为绑定的对象作用域。
function b () {
console.log(this.title);
}
function Book ( title ) {
this.title = title;
}
var book = new Book('javascript');
// apply , call 特点:调用就执行
// b.call(book);
// b.apply(book);
// 当执行一个函数的时候更改作用域
var bBindfn = b.bind(book);
// 执行更改作用域
bBindfn();
执行环境和作用域链概念
执行环境(execution context)是 JavaScript 中至关重要的概念。它定义了变量或函数有权访问的其他数据,并决定了它们各自的行为。每个执行环境都关联一个变量对象,其中保存了环境中定义的所有变量和函数。虽然代码无法直接访问这个对象,但解析器在处理数据时会在后台执行它。

全局执行环境是最外围的执行环境。根据 ECMAScript 实现所在的宿主环境不同,表示执行环境的对象也会有所区别。
每个函数都拥有自己的执行环境。当执行流进入一个函数时,该函数的环境会被推入一个环境栈中;函数执行结束后,栈将其环境弹出,并将控制权返还给之前的执行环境。当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。作用域链的作用是保证对执行环境有权访问的所有变量和函数进行有序访问,从而控制代码的访问权限。
var color1 = "blue";
function changeColor () {
var color2 = "red";
function swapColor () {
var color3 = color2; //color3 = 'red'
color2 = color1; //color2 = 'blue'
color1 = color3; //color1 = 'red'
console.log( color1,color2,color3 );
}
swapColor();
}
changeColor();
//环境变量 可以一层一层的向上进行追溯 可以访问它的上级 环境(变量和函数)
// 作用域链 具有层级关系
//在大型程序中,全局变量,尽量少使用,因为全局变量总是最后一次搜索。 防止全局变量污染。//很少去定义全局变量,效率比较慢。
垃圾收集和块级作用域的概念
垃圾收集
JavaScript 是一门具备自动垃圾收集机制的编程语言,开发人员无需手动关心内存的分配和回收问题。
垃圾回收器会定期执行回收操作。
离开作用域的值会被自动标记为可回收,从而在垃圾收集期间被删除。标记清除是目前主流的垃圾收集算法,其核心思想是给当前不使用的值加上标记,然后回收其内存。
//垃圾收集 ,标记清除 (模拟)
function test () {
var a = 10; //mark - 被使用
var b = 20; //mark - 被使用
}
test(); //执行完毕 之后 ,a,b又被标记使用。 mark - 没有被使用
//在间隔时间中 回收。 如果mark 没有被使用, 则回收。
//引用计数(模拟)
//如果变量被引用 , count = 1;
function test2 () {
var a = 10; //count = 1;
var b = 20;
var c;
c = a; //count++ = 2; //a 被 c 所使用 ,引用。
a = 50; //count--; //重新被赋值 count-- //等待 count 为 0 的时候, 垃圾回收机制 就回收
}
块级作用域
JavaScript 传统上没有块级作用域的概念,因此在使用 if 或 for 语句时需要格外小心。
JavaScript 模拟块级作用域(块级作用域相当于内部的执行体,即一个执行环境)可以利用 IIEF(立即调用函数表达式)的特性来实现。
//当函数执行之后, 变量就被回收
function test () {
(function () { //函数有一个单独的作用域,外面无法访问到 i
for ( var i=0; i<=5; i++ ) {
console.log( i );
}
})();
console.log( i ); //报错,无法找到
}
test();
闭包 Closure
闭包与函数有着紧密的关系,它是函数代码在运行过程中的一个动态环境,是一个运行期的、动态的概念。
所谓闭包,是指词法表示包括不必计算的变量的函数。也就是说,该函数能够使用函数外定义的变量。
在程序语言中,闭包是指语法域位于某个特定区域,具有持续参照(读写)位于该区域内自身范围之外的执行域上的非持久型变量值能力的段落。这些外部执行域的非持久型变量会神奇地保留它们在闭包最初定义(或创建)时的值。
理解闭包,必须对作用域链的概念有非常清晰的认知。
var name = "xiao A";
var obj = {
name : "xiao B",
getName: function(){
return function(){
return this.name;
}
}
};
console.log(obj.getName()()); //xiao A
//类似:
//var zf = obj.getName();//全局作用域
//zf();
var name = "xiao A";
var obj = {
name : "xiao B",
getName: function(){
var self = this;
return function(){
return self.name;
}
}
};
//console.log( obj.getName().call(obj) );
console.log( obj.getName()() );
//闭包: 一个函数, 可以访问另外一个作用域中的变量
//封闭性,(类似食品包装袋一样,封闭起来,保质期延长,变量的访问范围的延长) //private 起到一个保护变量的作用
//1 level
function f(x){ //2 level
var temp = x; //局部变量 //temp 标记 已经没有被使用
return function(x){ //3 level (function 有一个执行域)
temp += x; //temp 下一级作用域仍然被引用 , 标记为 使用
alert(temp);
}
}
//js 垃圾回收机制,当函数执行完毕后,内部所有的局部变量都集体的被回收。
var a = f(50);
a(5); //55
a(10); //65
a(20); //85
回调函数
-
回调函数执行
-
回调函数中的 this
-
回调函数的返回值
forEach
// forEach:用来遍历数组中的每一项
// 1. 数组中有几项,那么传递进去的匿名回调函数就需要执行几次。
// 2. 每一次执行匿名函数的时候,还传递了三个参数值:数组中的当前项item,当前项的索引index,原始的数组input
// forEach方法中的this是arr,匿名函数回调函数的this默认是window
var arr = [10, 234, 23, 76, 7666, 34];
arr.forEach(function(item, index, input) {
input[index] = item * 10; // 操作之后,修改了原数组
console.log(arguments);
});
var obj = {name: 'zf'};
// arr.forEach(function(item, index) {
// console.log(this);
// }.call(obj)); // 给forEach赋值的是时候,首先把匿名函数执行,把匿名函数中的this变为obj,把匿名函数执行的返回结果undefined赋值给foreach
arr.forEach(function(item, index) {
console.log(this, '---');
}.bind(obj)); // bind 只是预先处理,先把this转为参数的对象,到后续该执行的时候才执行。
// 不管是forEach,还是map都支持第二个参数,第二个参数表示:把匿名函数中的this进行修改。
arr.forEach(function() {
console.log(this, 'this');
}, obj);
forEach 兼容处理
// 兼容处理
Array.prototype._forEach = function(callback, context) {
content = content || window;
if ('forEach' in Array.prototype) {
this.forEach(callback, context);
return;
}
// IE6-8,执行回调逻辑
for (var i=0; i<this.length; i++) {
callback || callback.call(context, this[i], i, this);
}
}
map
var arr = [10, 234, 23, 76, 7666, 34];
arr.map(function(item, index, input) { // 原有数组不变
console.log(arguments);
return item * 10;
});
// map和forEach非常相似,都是用来遍历数组中的每一项的值
// 区别:map的回调函数中支持return返回值,return的是什么,相当于把数组中的这一项改变为什么(但是并不影响原来的数组,只是相当于把原数组克隆一份,把克隆的这一份的数组中的对应项改变)
map 兼容处理
Array.prototype._map = function(callback, context) {
context = context || window;
if ('map' in Array.prototype) {
this.map(callback, context);
return;
}
// IE6-8,执行回调逻辑
var resArr = [];
for (var i=0; i<this.length; i++) {
if (typeof callback === 'function') {
resArr[resArr.length] = callback.call(context, this[i], i, this);
}
}
return resArr;
}
var arr = [10, 234, 23, 76, 7666, 34];
arrMap = arr._map(function(item, index, input) { // 原有数组不变
// console.log(arguments, '_map');
return item * 100;
});
console.log(arrMap);
柯理化函数思想
柯理化函数思想是一种 JavaScript 预处理思想。
核心原理是利用函数执行可以形成一个不销毁的私有作用域,将需要预先处理的内容存储在这个不销毁的作用域中,并返回一个匿名函数。执行该匿名函数时,对之前预先存储的值进行相关操作处理即可。
var obj = {name: 'zf'};
function fn(num1, num2) {
console.log(this, num1, num2)
}
// 把传递进来的callback这个方法中的this预先处理为context
function bind(callback, context) {
context = context || window;
var outArg = Array.prototype.slice.call(arguments, 2);
return function(ev) {
var innerArg = Array.prototype.slice.call(arguments, 0);
callback.apply(context, outArg.concat(innerArg));
}
}
// document.body.onclick = fn; // fn 中的 this是document.body. num1是 MouseEven对象
document.body.onclick = fn.bind(obj, 100, 200); // 除了预先处理了this和需要手动传递的参数值以外,把浏览器默认给传递的鼠标事件对象也进行预先处理,传递到最后一个参数。
document.body.onclick = bind(obj, 100, 200)
// document.body.onclick = function() {
// console.log(this); // this --> document.body
// }
// window.setTimeout(fn.bind(obj), 0);
// window.setTimeout(bind(fn, obj, 100, 20), 0); // 给定时器绑定方法,然后定时器到达时间的时候,让fn执行,并且让fn中的this变为obj
感兴趣的朋友可以使用在线HTML/CSS/JavaScript代码运行工具:http://tools.jb51.net/code/HtmlJsRun测试上述代码运行效果。
更多关于 JavaScript 相关内容可查看本站专题:《JavaScript 常用函数技巧汇总》、《javascript 面向对象入门教程》、《JavaScript 错误与调试技巧总结》、《JavaScript 数据结构与算法技巧总结》及《JavaScript 数学运算用法总结》
希望本文所述对大家 JavaScript 程序设计有所帮助。