概述
函数是这样一段代码,它只定义一次,但可能被执行或调用多次。
函数定义会包括一个形参的标识符列表,这些参数在函数体中像局部变量一样工作。函数调用会为形参提供实参的值。函数使用它们实参的值来计算返回值,成为该函数调用表达式的值。
除了实参之外,每次调用还会拥有另一个值,本次调用的上下文,这就是this关键字的值。
如果函数挂载在一个对象上,作为对象的一个属性,就称它为对象的方法。当通过这个对象来调用函数时,该对象就是此次调用的上下文,也就是该函数的this的值。
用于初始化一个新创建的对象的函数称为构造函数。
在JS里,函数即对象。JS可以把函数赋值给变量,或者作为参数传递给其它函数。
JS函数可以嵌套在其他函数中定义,这样它们就可以访问他们被定义时所处的作用域中的任何变量。这意味着JS函数构成了一个闭包。
1函数定义
1.1函数组成:
- 名称标识符(用途就像变量的名字一样,新定义的函数对象会赋值给这个变量。)
- 一对圆括号:参数列表
- 一对花括号:语句块
1.2.1函数声明语句:
- 一条函数声明语句实际上声明了一个变量,并把一个函数对象赋值给它。
- 函数声明语句“被提前”到外部脚本或外部函数作用域的顶部,这种方式声明的函数,可以被在它定义之前出现的代码所调用。
- 函数声明语句并非真正的语句,ES规范只是允许它们作为顶级语句,它们可以出现在全局代码里,或者嵌套其他函数。但不能出现在循环条件判断语句中。
function funcName(){}
1.2.2函数定义表达式:
相对而言定义函数表达式时并没有声明一个变量。
函数表达式定义的函数,函数名是可选的。
- 如果一个函数定义表达式包含名称,函数的名称将成为函数内部的一个局部变量。
- 通常而言,以表达式方式定义函数时不需要名称,适合用来定义只会使用到一次的函数。
- 以表达式方式定义的函数在定义之前无法调用。为了调用一个函数,必须要能引用它,而要使一个以表达式方式定义的函数之前,必须把它赋值给一个变量。变量的声明提前了,但是给变量赋值是不会提前的。
- 函数定义表达式可以出现在任何地方。
var a = function (){}
1.3函数命名:
- 函数名通常是动词或动词词组
- 通常函数名第一个字符小写,这是一种编程约定
- 当包含多个单词时,一种约定是下划线,一种是小驼峰
- 有一些函数是内部函数或私有函数,函数名前加下划线
1.4返回值:
- return语句导致函数停止执行
- 返回它的表达式的值给调用者
- 如果没有一个相关的表达式,返回undefined
- 如果函数中没有return语句,则执行函数中每条语句,返回undefined
- 没有返回值的函数有时候称作过程
1.5嵌套函数
- 嵌套函数变量作用域规则:
- 他们可以访问嵌套它们的函数的参数和变量。
2函数调用
构成函数主体的JS代码在定义之时并不会执行,只有调用该函数时,它们才会执行。
2.1调用方式:
- 作为函数的函数调用
//在一个调用中,每个参数表达式会计算出一个值,
//计算结果作为参数传递给另外一个函数。
//这些值作为实参传递给声明函数时定义的形参。
//在函数体中存在一个形参的引用,指向当前传入的实参列表,
//通过它可以获得参数的值。
//函数的返回值成为调用表达式的值。
//如果函数所有语句执行完返回值就是undefined,
//如果函数返回是因为解释器执行到一条return语句,
//返回值就是return语句之后的表达式的值。
//如果return语句没有值,也返回undefined。
printprops({x:1});
var total = distance(0,0,2,1) + (2,1,3,5);
var probability = factorial(5)/factorial(13);
根据ES3和非严格ES5对函数调用的规定。调用上下文(this的值)是全局对象。然而在严格模式下,调用上下文是undefined。
以函数形式调用的函数通常不使用this关键字。不过,this可以用来判断当前是否是严格模式。
// 定义并调用一个函数来确定当前脚本运行时是否为严格模式
var strict = (function(){return !this;}());
// 翻译成人类语言就是,当没有this的时候返回true
- 作为方法的方法调用
一个方法就是保存在一个对象的属性里的JS函数。
o.m(x,y);
// 函数表达式本身就是一个属性访问表达式
// 方法调用和函数调用的重要区别:调用上下文。
// 属性表达式由两部分组成:一个对象和属性名称。[o.m]
// 在像这样的方法调用表达式里,对象o成为调用期上下文,
// 函数体可以使用关键字this引用该对象。
关键字this引用对象:
var calculator = {
operand1:1,
operand2:1,
add:function(){
//注意this关键字的用法,this指代当前对象。
this.result = this.operand1 + this.operand2;
}
};
calculator.add();
console.log(calculator.result);
任何函数只要作为方法调用实际上都会传入一个隠式的实参,这个实参是一个对象,方法调用的母体就是这个对象。方法调用的语法已经很清晰地表明了函数将基于一个对象进行操作。
rect.setSize(width,height); // 函数执行的载体是rect对象。
setRectSize(rect,width,height);
方法链:
当方法的返回值是一个对象,这个对象还可以再调用它的方法。
链式调用中,当方法不需要返回值时,最好直接返回this。
this:
this是一个关键字,不是变量也不是属性名。
this没有作用域的限制,嵌套的函数不会从调用它的函数中继承this。
如果嵌套函数作为方法调用,其this值指向调用它的对象。
如果嵌套函数作为函数调用,其this值不是全局对象(非严格模式下)就是undefined。
很多人误以为调用嵌套函数时this会指向调用外层函数的上下文。如果你想访问这个外部函数的this值,需要将this的值保存在一个变量里,这个变量和内部函数都同在一个作用域内。通常使用变量self来保存this:
var o = { // 对象o
n: 7,
m: function(){ // 对象中的方法m
var self = this; // 将this的值保存到一个变量
console.log(this === o); // true this就是这个对象o
f();
function f(){ // 定义一个嵌套函数
console.log(this === o);
// false:this的值是全局对象或undefined
console.log(self === o);
// true:self指外部函数的this值
console.log(self.n);
}
}
};
o.m();
- 作为构造函数的调用
如果函数和方法前带有关键字new,它就构成 构造函数调用。
如果构造函数没有形参可以省略括号。
var o = new Object();
var o = new Object;
构造函数调用会创建一个新的空对象,这个对象继承自构造函数的prototype属性。
构造函数可以使用this关键字来引用这个新创建的对象,它会使用这个新对象作为调用上下文。也就是说,在表达式 new o.m() 中,调用上下文不是o。
- 通过call() & apply() 的 间接调用
3函数的实参和形参
3.1.1 可选形参
- 当实参比形参少时,剩下的形参会设置成undefined。
- 因此在调用函数时形参是否可选以及是否可以省略应当保持一个较好的适应性。可以给省略的参数赋一个合理的默认值。
// 将对象o中可枚举的属性名追加到数组a中,并返回这个数组a
// 如果省略a,则创建一个新数组并返回这个新数组
function getPropertyNames(o,/*optional*/a){
if(a === undefined) a = [];
// 此处可以不使用if语句,使用||运算符
a = a || [];
// 使用||运算符代替if语句的前提是a必须预先声明。在这个例子中a是作为形参传入的,相当于var a。
for(var property in o) a.push(property);
return a;
}
// 这个函数调用可以传入1个或2个实参
var a = getPropertyName(o); // 将o的属性存储到一个新数组中
getPropertyName(p,a); // 将p的属性追加到数组a中
3.1.2 实参对象
当传入参数大于形参个数时,参数对象解决了问题。在函数体内,标识符arguments是指向实参对象的引用,实参对象是一个类数组对象,这样可以通过数字下标访问传入函数的实参值。
JS默认行为:省略的实参都是undefined,多出的参数会自动省略。
实参对象可以让函数操作任意数量的实参。
function max(){
var max = Number.NEGATIVE_INFINITY;
// 遍历实参,查找并记住最大值。
for(var i = 0;i < arguments.length; i++){
if(arguments[i] > max) max = arguments[i];
// 返回最大值
return max;
}
}
var largest = max(1,10,100,2,3,1000,4,5,10000,6);
类似这种函数可以接收任意个数的实参,称作 不定实参函数。
function sum(){
console.log(arguments);
var num = 0;
for (var i = 0; i < arguments.length; i++){
console.log(arguments[i]);
num = num + arguments[i];
}
}
sum(1,3,443,5,32,546,3);
//实参列表:当传入参数个数不定时使用
function sum(a,b){
arguments[0]; // 10
arguments[1]; // 20
a // 10
b // 20
arguments[0] = 40;
a // 40
}
sum(10,20);
// 在满足一定条件下(传入实参个数与对应个数的实参列表数量相同时)
// 实参列表与形参具有一一映射关系
function sum (a,b,c) {
arguments.length; // 实参长度
sum.length; // 形参长度
}
sum(10,20);
4 作为值的函数
function square(x){return x * x};
定义创建一个新的函数对象,并将其赋值给变量square。函数的名字实际上是看不见的,(square)仅仅是变量的名字,这个变量指代函数对象。
5 作为命名空间的函数
- 在函数声明中的变量在整个函数体内都是可见的(包括嵌套函数)
在函数外部是不可见的。 - 不在任何函数内声明的变量是全局变量,在整个程序中都是可见的。
- 可以定义一个函数用作临时的命名空间,这个命名空间内定义变量不会污染到全局命名空间。
6 闭包
- 当内部函数被保存到外部时,将会生成闭包。闭包会导致原有作用域链不释放,造成内存泄露。
- 函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性在计算机科学文献中称为“闭包”。
- 函数变量可以被隐藏于作用域链之内,因此看起来是函数将变量包裹了起来。
- 所有的JS函数都是闭包:它们都是对象,它们都关联到作用域链。
- 定义大多数函数时的作用域链在调用函数时依然有效,但这并不影响闭包。
嵌套函数的词法作用域规则:
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){return scope;}
return f();
}
checkscope()
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){return scope;}
return f;
}
checkscope()()
JS函数的执行用到了作用域链,这个作用域链是函数定义时创建的。嵌套的函数f()定义在这个作用域链里,其中的变量scope一定是局部变量,不管任何时候执行函数f(),这种绑定在执行f()时依然有效。因此最后一行代码返回“local scope”。
- 函数定义时的作用域链到函数执行时依然有效。
- 每次调用JS函数的时候,都会为之创建一个新的对象用来保存局部变量,把这个对象添加至作用域链中。当函数返回的时候,就从作用域链中将这个绑定变量的对象删除。
- 如果不存在嵌套的函数,也没有其他引用指向这个绑定对象,它就会被当做垃圾回收掉。
- 如果定义了嵌套的函数,每个嵌套的函数都各自对应一个作用域链,并且这个作用域链指向一个绑定对象。
- 但如果这些嵌套的函数对象在外部函数中保存下来,那么他们也会和指向的变量绑定对象一样当做垃圾回收。
- 但是如果这个函数定义了嵌套的函数,并将它们作为返回值返回或者存储在某处的属性里,这时就会有一个外部引用指向这个嵌套的函数。它就不会被当做垃圾回收,并且它所指向的变量绑定对象也不会当做垃圾回收。
书写闭包的时候还需要注意一个事情,this是JS的关键字,而不是变量。每个函数调用都包含一个this值,如果闭包在外部函数里时无法访问this的,除非外部函数将this转存为一个变量:
var self = this;
绑定arguments的问题与之类似,arguments并不是一个关键字,但在调用每个函数时都会自动声明它,由于闭包具有自己所绑定的arguments,因此闭包内无法直接访问外部函数的参数数组,除非外部函数将参数数组保存到另外一个变量中:
var outerArguments = arguments;
7 函数属性,方法和构造函数
- length属性
在函数体里,arguments.length表示传入函数的实参个数。而函数本身的length属性则有着不同含义。
函数的length属性是只读属性,表示函数实参的数量。
- prototype属性
指向一个对象的引用,这个对象称作“原型对象”。
- call() & apply()
- bind()
- toString()