什么是函数?
函数是这样一段js代码,它只定义一次,但可能被执行和调用任意次。你可能已经从诸如子例程或者过程这些名字对函数的概念有所了解。js函数是参数化,函数的定义会包括一个称为形参的标识列表,这些参数在函数体中像局部变量一样工作。函数调用会为形参提供实参的值。函数使用它们实参的值来计算返回值,成为该函数调用表达式的值,除了实参之外,每次调用还会拥有另外一个值——本次调用的上下文——这就是this关键字的值。
如果函数挂在一个对象上,作为对象的一个属性,就称它为对象的方法。当通过这个对象来调用函数时,该对象就是此次调用的上下文,也就是该函数的this值。用于初始化一个新创建的对象的函数,成为构造函数
在js里,函数即对象,程序可以随意操控它们。比如,js可以把函数赋值给变量,或者作为参数传递给其他函数。因为函数就是对象,所有可以给它们设置属性,甚至调用它们的方法
js的函数可以嵌套在其他函数中定义,这样它们就可以访问它们被定义时所处的作用域任何变量。这意味着js函数构成了一个闭包,它给js带来了非常强劲的编程能力
1.函数定义
函数使用 function 关键字来定义,它可以用在函数定义表达式或者函数声明语句里。在两种形式中,函数定义都从function关键字开始,其后跟随这些组成部分
● 函数名称标识符。函数名称是函数声明的语句必须的部分。它的用途就行变量的名字,新定义的函数对象会赋值给这个变量。对函数定义表达式来说,这个名字是可选的,如果存在,该名字只存在函数体中,并指代该函数对象本身
● 一对圆括号,其中包含0个或者多个逗号隔开的标识符组成的列表。这些标识符是函数的参数名称,它们就像函数体重的局部变量一样
● 一对花括号,其中包含0条或多条js语句。这些语句构成了函数体:一旦调用函数,就会执行这些语句
eg:
let o={x:33,y:44}
//输出o的每个属性的名称和值
function printprops(o){
for (var p in o){
console.log(p+':'+o[p]+"\n")
}
}
printprops(o);
//这个函数表达式定义了一个函数用来求传入参数的平方
//注意我们把它赋值给一个变量
let square=function(x){
return x*x;
}
square(2);
//计算阶乘的递归函数(调用自身的函数)
function factorial(x){
if(x<=1){
return 1;
}else{
return x*factorial(x-1);
}
}
//函数表达式可以包含名称,这在递归时很有用
var f=function fact(x){
if(x<=1){
return 1;
}else{
return x*fact(x-1)
}
}
//函数表达式也可以作为参数传递给其他函数
data.sort(function(a,b){
return a-b;
})
//函数表达式有时定义后立即调用
let tensquared=(function(x){
return x*x;
}(10))
注:以表达式方式定义的函数,函数名称是可选的。一条函数声明语句实际上声明了一个变量,并把一个函数对象赋值给它。相对而言,定义函数表达时并没有声明一个变量。函数是可以命名,就好像上面的阶乘函数,它需要一个名称指代自己。如果一个函数定义表达式包含名称,函数局部作用域将会包含一个绑定带函数对象的名称。实际上函数的名称将成为函数内部的一个局部变量。通常而言,以表达式方式定义函数时都不需要名称,这会让定义它们的代码更为紧凑。函数定义表达式特别适合用来定义那些只会用到一次的函数。
函数声明的语句‘被提前’到外部脚本或外部函数作用域的顶部,所以以这种方式声明的函数,可以被它定义之前出现的代码所调用。不过,以表达式定义的函数就另当别论了,为了调用一个函数,必须引用它,而要使用一个以表达式定义的函数之前,必须把它赋值给一个变量。变量的声明提前了,但是给变量赋值是不会提前的,所以表达式定义的函数在定义之前无法调用
注,大多数函数包含一条return。return语句导致函数停止执行,并返回它的表达式(如果有的话)的值给调用者。如果没有的话值是undefined.
嵌套函数,在js里函数可以嵌套函数里,如下
function hypotenuse(a,b){
function square(x){
return x*x;
}
return Math.sqrt(square(a)+square(b))
}
嵌套函数的有趣之处在于它的变量作用域规则:它们可以访问嵌套它们(或多层嵌套)的参数和变量
2.函数调用
构成函数主体的js代码在定义之时并不会执行,只有调用该函数时,它们才会执行。有四种方式可以调用它们
● 作为函数
● 作为方法
● 作为构造函数
● 通过它们的call()和apply()方法间接调用
2,1函数调用
使用调用表达式可以进行普通的函数调用也可进行方法调用。一个调用表达式由多个参数函数表达式组成,每个函数表达式都是由一个函数对象和左圆括号,参数列表和右圆括号组成,参数列表是由逗号分隔的0个或多个参数表达式组成。如果函数表达式是以属性访问的表达式,即函数是一个对象的属性或数组中的一个元素,那么它就是一个方法调用表达式。eg
let o={x:33,y:44}
//输出o的每个属性的名称和值
function printprops(o){
for (var p in o){
console.log(p+':'+o[p]+"\n")
}
}
printprops(o);
//这个函数表达式定义了一个函数用来求传入参数的平方
//注意我们把它赋值给一个变量
let square=function(x){
return x*x;
}
square(2);
在一个调用中,每个参数表达式(圆括号之间的部分)都会计算出一个值,计算的结果作为参数传递给另外一个函数。这些值作为实参传递给声明函数时定义的形参。在函数中存在一个形参的引用,指向当前传入的实参列表,通过它可以获得值。
对于普通的函数调用,函数的返回值作为调用表达式的值。如果函数返回的是因为解释器执行到一条return语句,返回值就是return之后的表达式的值,如果return语句没有值,则返回undefined
2.2 方法的调用
一个方法无非是保存在一个对象的属性的js函数。如果有一个函数f和一个对象o,则可以用下面的代码给o定义一个名为m()的方法
o.m=f;
给对象o定义了方法m(),调用它时就像这样
o.m();
或者,如果m()需要两个参数,调用起来则像这样
o.m(x,y);
上面的代码是一个调用表达式:它包括一个函数表达式o.m,以及两个参数表达式y,函数表达式本身就是一个属性访问表达式,这意味着该函数被当做一个方法,而不是作为一个普通函数来调用
对方法调用的参数和返回值的处理,和上面所描述的普通函数调用完全一致。但是,方法调用和函数调用有一个重要区别,即:调用上下文。属性访问表达式由两部分组成:一个对象(o)和属性名称(m)。就像这样的方法调用表达式里,对象o成为调用上下文,函数体可以使用关键字this引用该对象。下面是一个具体的例子
var calculator={
operand1:1,
operand1:2,
add:function(){
this.result=this.operand1+this.operand2
}
}
calculator.add();//这个方法调用计算1+1结果
calculator.result//2
大多数方法调用使用点符号来访问属性,使用方括号也可以进行方法访问,例如
calculator['m']();
注:this是一个关键字,不是变量,也不是属性名。js语法不允许给this赋值。和变量不同,关键字this没有作用域的限制,嵌套的函数不会从他函数照片那个继承this,如果嵌套函数作为方法调用,其this指向它调用的对象
let o={
x:1,
m:function(){
console.log(this);//对象o
f();
function f(){
console.log(this)//window
}
}
}
o.m()
2.3 构造函数
如果函数和方法直接带有关键字new,它就构成了构造函数调用。构造函数调用和普通的函数调用以及方法的调用在实参处理,调用上下文和返回值的方面各有不同。
如果构造函数调用在圆括号内包含一组实参的列表,先计算这些实参的表达式,然后传入函数内,这和函数调用和方法调用一致。但是构造函数如果没有形参,js构造函数调用的语法是允许省略实参列表和圆括号的。凡是没有形参的构造函数调用都可以省略圆括号,比如,下面这两行代码就是等价的
let o=new Object();
let o= new Object;
构造函数调用创建一个新的空对象,这个对象继承自构造函数的prototype属性。构造函数试图初始化这个新创建的对象,并将这个对象用做其调用上下文,因此构造函数以使用this关键字来引用这个新创建的对象。
注:构造函数看起来像一个方法调用,它依然会使用这个新对象作用调用上下文。也就是说,在表达式new o.m()中,调用上下文并不是o,eg:
let o={
x:1,
m:function(){
console.log(this);//this 是m的值
f();
function f(){
console.log(this)//window
}
}
}
new o.m();
构造函数通常不使用return关键字,它们通常初始化新对象,当构造函数的函数体执行完毕之后,它会显示返回。这种情况下,构造函数调用表达式的计算结果就是这个新对象的值。然而如果构造函数显示使用return语句返回一个对象,那么调用表达式的值就是这个对象。如果构造函数使用return语句没有指定返回值,或者返回一个原始值,那么这时将忽略返回值,同时使用这个新对象作为调用结果.
2.4间接调用
js中函数也是对象,和其他js对象没什么两样,函数对象也可以包含方法。其中两个方法call()和apply()可以间接的调用函数。这两个方法都可以显示指定所需的this值,也就是说任何函数可以作为任何对象的方法来调用,哪怕这个函数不是对象的方法。call方法使用它的实参列表作为函数的实参,apply方法则要求以数组的形式传入参数。
3.函数的实参和形参
js中的函数定义并未指定函数形参的类型,函数调用也未对传入的实参值做任何类型检查。实际上,js函数调用甚至不检查传入形参的个数。当调用函数时实参的个数和声明的形参个数不匹配时出现的状况,同样说明了如何显示测试函数实参的类型,以避免非法的实参传入函数
3.1可选形参
当调用函数的时候传入的形参比函数声明指定的形参个数要少,剩下的形参都将设置为undefined值。因此在函数调用时形参是否可选以及是否可以省略应当保持较好的适应性。为了做到这一点,应当给省略的参数赋一个合理的默认值,eg
注:当选用这种可选实参来实现函数时,需要将可选实参放在实参列表的最后
let o={x:33,y:44}
function getPropertyNames(obj,arr){
arr=arr || [ ];
//使用了||运算符,如果第一个实参是真值,则返回第一个,否则返回第二个
for(let p in obj){
//getOwnPropertyNames
if(Object.getOwnPropertyNames(obj[p])){
arr.push(p)
}
}
console.log(arr)
}
getPropertyNames(o);
3.2 可变长的实参列表:实参对象
调用函数的时候传入的实参个数超过函数定义时的参数,无法直接获取未命名的引用。参数对象解决了对各问题。在函数体内,标识符arguments是指向实参对象的引用,实参对象是一个类数组对象,这样可以通过数字下标就能访问传入函数的实参值,而不用非要用过名字来得到实参。
假设定义了函数f,它的实参只有一个x。如果调用这个函数时传入两个实参,第一个实参可以通过参数名x来获得,也可以通过arguments[0]来得到。第二个参数只能用arguments[1]来得到。此外,和真正的数组一样,arguments也包含一个length属性,用以标识其所包含的元素个数。因此,如果调用函数f()时传入两个参数,arguments的length值就是2,省略的实参是undefined,多出的参数将自动省略
function f(x,y,z){
console.log(arguments)
}
f(1);//[1, callee: ƒ, Symbol(Symbol.iterator): ƒ]
f(1,2);// [1, 2, callee: ƒ, Symbol(Symbol.iterator): ƒ]
实参对象有个重要用处,就是让函数可以操作任意实参。下面的函数就可以接收任意数量的实参,并返回传入实参最大值(类似内置函数Math.max()最大值)
function max(){
let sum = 0;
for(i=0;i
if(arguments[i]>sum){
sum=arguments[i]
}
}
return sum;
}
console.log(max(1,200,30,38,56))
不定参数函数的实参个数不能为0,arguments对象最适合的应用场景是在这样的一类函数中,这类函数包含固定个数的命名和必须参数,以及随后个数不定的可选参数。
arguments并不是真正的数组,它是一个实参对象。每个实参对象都包含数字为索引的一组元素以及length属性,但它毕竟不是真正的数组,可以这样理解,它是一个对象,只是碰巧具有以数字为索引的属性。
数组对象包含一个相同的特性。在非严格模式下,当一个函数包含若干形参,实参对象的数组元素是函数形参所对应实参的别名,实参对象中一数字索引为例,并且形参名称可以认为是相同变量的不同命名。通过实参名字来修改实参的值,通过arguments[]数组也可以获取更改后的值
function f(x){
console.log(x);//x
arguments[0]=null;
console.log(x);//null
}
f('x');
在严格模式中的函数无法使用arguments作为形参名和局部变量名,也不能给arguments赋值
function f(x){
'use strict'
console.log(x);//x
arguments[0]=null;
console.log(x);//x
}
f('x');
callee和caller属性
除了数组元素,实参对象还定义了callee和caller属性。在es5严格模式下,对这个两个属性的读写操作都会产生一个类型的错误。在非严格模式下,es标准规范规定callee属性指代当前正在执行的函数。caller是非标准的,但是大多数浏览器都识别了,它指代调用当前正字执行的函数的函数。通过caller属性可以访问调用栈。callee属性在某些说话非常有用,比如在匿名函数中通过callee来递归调用自身。
let factorial=function(x){
if(x<=1){
return 1
}else{
return x*arguments.callee(x-1);
}
}
3.3将对象属性用作实参
当一个函数包含3个形参时,对于程序员来说,要记住调用函数中实参的正确顺序实在让人头疼。每次调用这个函数时都要不厌其烦的查阅文档,为了不让程序员每次都翻阅手册那么麻烦,最后通过名/值对的形式传入参数,这样参数的顺序就无关紧要了。为了实现这种风格的方法调用,定义函数的时候,传入的实参都写入一个单独的对象之中,在调用的时候传入一个对象,对象中的名/值对是真正需要的实参数据。
eg:
let a=[1,2,4,5];
let b=[];
function arraycopy(from,to){
console.log(from)
for(let i=0;i
to[i]=from[i]
}
}
function easycopy(args){
console.log(args.from);
console.log(args.to)
arraycopy(args.from,args.to);
}
easycopy({from: a,to: b});
3.4实参类型
js方法的形参并未声明类型,在形参传入函数体之前也未做任何类型检查。可以采用语义化的单词来给函数实参命名。当一个方法可以接收任意实参时,可以用省略号。因为js在必要的时候会进行类型转换。因此如果函数期望接收一个字符串的实参,而调用函数时传入其他可续的值,所传入的值会在函数体内将其他用做字符串的地方转换为字符串类型。
4.作为值的函数
函数可以定义,也可以调用,这是函数最重要的特性,函数定义和调用是js的词法特性。然后在js中函数不仅仅是一个语法,也是值,也就是说,可以将函数赋值给变量,存储在对象的属性和数组的元素中,作为参数传入另外一个函数等。
为了便于理解js中的函数时如何用做数据的以及js语法,来看以下这样定义一个函数:
function square(x){
return x*x;
}
这个定义创建一个新的函数对象,并将其赋值给变量square。函数的名字实际上是看不见的,它square仅仅是变量的名字,这个变量指代函数对象。函数还可以赋值给其他变量,并且扔可以正常工作
var s=square;
square(4);//16
s(4);//16
除了可以将函数赋值给变量,同样可以将函数赋值给对象属性。当函数作为对象的属性调用时,函数称为方法
let o={
square:function(x){
return x*x;
}
}
let y=o.square(16);//256
函数甚至不需要带名字,当它们赋值给数组元素时,最后一句代码看起来很奇怪,但的确是合法的函数调用表达式
let a=[function(x){ return x*x },20];//数组直接量
a[0](a[1]);
下面是将函数做值的一些案例
//这里定义了一些简单的函数
function add(x,y){
return x+y;
}
function subtract(x,y){
return x-y;
}
function multiply(x,y){
return x*y;
}
function divide(x,y){
return x/y;
}
//这里的函数以上面的某个函数作为参数
//并给它传入两个操作数然后调用它
function operate(operator,operand1,operand2){
return operator(operand1,operand2);
}
//这行代码计算了(2+5)+(4*5)
let i=operate(add,operate(add,2,3),operate(multiply,4,5));
console.log(i);//25
案例简化
let operators={
add:function(x,y){
return x+y;
},
subtract:function(x,y){
return x-y;
},
multiply:function(x,y){
return x*y;
},
divide:function(x,y){
return x/y;
}
}
function operate2(operation,operand1,operand2){
if(typeof operators[operation] == 'function'){
return operators[operation](operand1,operand2)
}
}
//hello world
let j=operate2('add','hello',operate2('add','','world'))
自定义函数属性
js中的函数并不是原始值,而是一个特殊的对象,也就是说,函数可以拥有属性,而不是定义全部变量,显然定义全局变量会让命名空间变得更加的杂乱无章。比如,假设你想写一个返回一个唯一整数的函数,不管在哪里调用都会返回这个整数。而函数不能返回同一个值,为了做到这一点,函数必须能够跟踪它每次返回的值。而函数不能两次返回同一个值,为了做到这一点,函数必须能够跟踪他每次返回的值,而且这些值的信息需要在不同的函数过程中持久化。如果这个变量只是这个函数本身用的到,做好将信息到函数对象的一个属性中
eg:
//初始化函数对象的计数器
//由于函数声明被提前了,因此这里是可以在函数声明之前给它的属性赋值
uniqueInteger.counter=0;
//每次调用这个函数都会返回一个不同的整数
//它使用一个属性来记住下一次将要返回的值
function uniqueInteger(){
return uniqueInteger.counter++;
}
下面的案例使用了自身属性,将自身当做数组,来缓存上一次计算的结果
function factorial(n){
if(n<=1){
return 1;
}else{
factorial[n]=n*factorial(n-1);
return factorial[n];
}
}
factorial(3);
console.log(factorial[3]);//6
5.作为命名空间的函数
在函数中声明的变量在整个函数体内都是可见的,包括嵌套函数,在函数外部是不可见的。不在任何函数内声明的变量是全局变量,在整个js程序中都是可见的。在js中是无法声明只在一个代码内可见的变量,基于这个原因,我们常常简单地定义一个函数做临时的命名空间,在这个命名空间内定义的变量都不会污染到全局命名空间。
比如,假设你写了一段js模块代码,这段代码将要用在不同的js程序中,和大多数代码一样,假定这段代码定义了一个用以存储中间计算结果的变量。这样问题就来了,当模块代码放到不同程序运行时,你无法得知这个变量是否已经创建,如果已经存在这个变量,那么将会和代码发生冲突。解决方法当然是将代码放入一个函数内,然后调用这个函数,这样全局变量就变成了函数内的局部变量
function mymodule(){
//模块代码
//这个模块所使用的变量都是局部变量
//而不是污染全局命名空间
}
mymodule();
这段代码仅仅定义了一个单独的全局变量,名叫‘mymodule’的函数,这样还是太麻烦,并在单个表达式中调用它
((function()){
//模块代码
}())
这种定义匿名函数并立即在单个表达式中调用它的写法非常常见,已经成为一个惯用法了,主要上面代码的圆括号的用法,function之前的左圆括号是必需的,因为如果不写这个左圆括号,js解析器会试图将关键字function解析为函数声明语句。使用圆括号js解析器才会正确的将解析为函数定义表达式。使用圆括号是习惯用法,尽管有些时候没有必要也不应当省略。这里定义的函数会立即调用。
在IE浏览器中,以下代码在六年前的IE大部分版本中是不会弹出 1 的
for( var p in {string: null}) {
alert(1);
}
下面的案例定义一个扩展函数,用来将第二个及后续参数复制到第一个参数
6 闭包
和其他大多数现代编程语言一样,js也采用词法作用域,也就是说,函数的执行依赖于变量作用域,这个作用域是在函数定义时决定的,而不是函数调用时决定的。为了实现这种词法作用域,js函数对象的内部状态不仅包含函数的代码逻辑,还必须引进当前的作用域链。函数对象可以通过作用域链关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性在计算机科学文献中成为‘闭包’
从技术的角度讲,所有的js函数都是闭包。它们都是对象,它们都关联到作用域链。定义大多数函数时的作用域链在调用函数时依然有效,但这不影响闭包,当调用函数时闭包所指向的作用域链和定义函数时的作用域链不是同一个作用域链时,事情就变得非常微妙。当一个函数嵌套另一个函数,外部函数将嵌套的函数对象作为返回值返回的时候往往发生这种事。有很多强大的编程技术都利用到了这类嵌套函数的闭包,以至于这种编程模式在js中非常常见。当你第一次碰到闭包时可能会觉得非常让人费解,一旦掌握了闭包之后,就能非常自如地使用它,了解这一点至关重要。
eg:
let scope = 'global scope';
function checkscope(){
let scope='local scope';
function f(){
return scope;
}
return f();
}
console.log(checkscope());//local scope
checkscope()函数声明了一个局部变量,并定义了一个函数f(),函数f()返回这个变量值,最后将f()的执行结果返回。你应当非常清楚为什么调用 checkscope()会返回‘local scope’。现在我们对这段代码做一点变动。你知道这段代码返回的是什么
let scope = 'global scope';
function checkscope(){
let scope='local scope';
function f(){
return scope;
}
return f;
}
console.log(checkscope()());//local scope
在这段代码中,我们将函数内的一对圆括号移动到checkscope()之后。checkscope()现在仅仅返回函数内嵌的一个函数对象,而不是直接返回结果。在定义函数的作用域外面,调用这个嵌套的函数会发生什么事情呢?
回想以下词法规则:js函数的执行用到了作用域链,这个作用域链是函数定义的时候创建的。嵌套函数f()定义在这个作用域链里,其中的变量scope一定是局部变量,不管在何时何地执行函数f(),这种绑定在执行f()时依然有效。因此最后一行代码返回‘local scope’,而不是‘global scope’。简言之,闭包的这个特性强大到让人吃惊,它们可以捕捉到局部变量(和参数),并一直保存下来,看起来像这些变量绑定到了在其中定义的外部函数。
闭包可以捕捉到单个函数调用的局部变量,并将这些局部变量用作私有状态。eg
下面这个案例粗略的看,第一行代码看起来像函数赋值给一个变量uniqueInteger,实际上,这段代码定义了一个立即调用的函数,因此这个函数的返回值赋值uniqueInteger,现在,我们来看函数体,这个函数返回另外一个函数,这是一个嵌套函数,我们将它的值赋值给uniqueInteger,嵌套的函数时可以访问作用域内的变量的,而且可以访问外部函数中定义的counter变量。当外部函数返回之后,其他任何代码都无法访问counter变量,只有内部的函数才能访问到它。
var uniqueInteger=(function(){
let counter=0;
return function(){
return counter++;
}
}())
console.log(uniqueInteger())
像counter一样的私有变量不是只能用在一个单独的闭包内,在同一个外部函数内定义的多个嵌套函数也可以访问它,这多个嵌套函数都共享一个作用域链,eg
function counter(){
let n=0;
return{
count:function(){
return n++;
},
reset:function(){
n=0;
}
}
}
var c=counter(),d=counter();//创建两个计时器
c.count();//0
d.count();//0 它们互不干扰
c.reset();//0 reset和count共享状态
c.count();//0 因为我们重置了c
d.count();//1 而没有重置d
从技术角度看,其实可以将这个闭包合并为属性存取器getter和setter。不同的是,这里的私有状态的实现利用了闭包,而不是利用普通对象的属性来实现的。这个案例并没有声明局部变量,而只是使用参数n来保存私有状态,属性存取器方法可以访问n
function counter(n){
return{
get count(){
return n++;
},
set count(m){
if(m>=n){
n=m;
}else{
throw Error('count can only be set to a larger value')
}
}
}
}
let c=counter(1000);
c.count;//1000
c.count;//1001
c.count=2000;
c.count;
c.count=2000;
利用闭包实现的私有属性存取器方法
function addPrivateProperty(o, name, predicate) {
var value;//这是一个属性
o['get' + name] = function () {
return value;
}
//setter方法首先检测值是否合法,若不合法就抛出异常
//否则就将其存储起来
o['set' + name] = function (v) {
if (predicate && !predicate(v)) {
throw Error('set' + name + ':invalid value' + v)
} else {
value = v
}
}
}
var o={};
addPrivateProperty(o,'Name',function(x){ return typeof x=='String'});
o.setName('Frank');//设置属性值
console.log(o.getName());//得到属性值
o.setName(0);//试图设置一个错误类型值
同一个作用域链定义两个闭包,这两个闭包,这两个闭包共享私有变量或变量。这是一种非常重要的技术,但是特别要小心那些不希望共享的变量往往不经意间共享给了其他闭包,这一点很重要 eg
function constfunc(v){
return function(){
return v;
};
}
let funcs=[];
/*
下面这段代码创建了10个闭包,并将它们存储到一个数组中,这些闭包都是在同一个函数调用定义的,因此它们可以共享变量i。当 constfunc()返回时,变量i的值是10,所有的闭包都共享这一个值,因此数组中的函数的返回值都是一个值,这不是我们想要的结果。关联到闭包的作用域链都是活动的,记住这一点很重要。嵌套函数不会将作用域内的私有成员复制一份,也不会对所绑定的变量生成静态快照。闭包是在外部函数里无法定义this的
*/
for(var i=0;i<10;i++){
funcs[i]=constfunc(i);
}
console.log(funcs[5]());//5
console.log(funcs[0]());//10
console.log(funcs)
7.函数的属性,方法和构造函数
我们看到js程序中,函数是值,对函数执行typeof会返回字符串‘function’,但是函数时js中特殊的对象,因为函数也是对象,它们也可以拥有属性和方法,就像普通对象可以拥有属性和方法一样。甚至可以用Function()构造函数来创建新的函数对象。
7.1 length属性
在函数体内,arguments.length表示传入函数的实参的个数。而函数本身的length属性则有不同含义。函数的length属性是只读属性,它代表函数实参的数量,这里的参数指的是‘形参’而非‘实参’,也就是在函数定义时给出的实参的个数,实际上也是函数调用时期望传入函数的实参个数。
eg:
function check(args){
var actual=args.length;//实参的真实个数
var expected=args.callee.length;
if(actual!== expected){
throw Error('实参和形参个数不一致')
}
}
function f(x,y,z){
check(arguments);
return x+y+z;
}
7.2 prototype 属性
每一个函数都有一个prototype属性,这个属性指向一个对象的引用,这个对象称作原型对象。每一个函数都包含不同的原型对象。当将函数用做构造函数时,新创建的对象会从原型对象上继承属性。
7.3 call()方法和apply()方法
我们可以将call()和apply()看做是某个对象的方法,通过调用方法来间接调用函数。call()和apply()的第一个参数是调用函数的母对象,它调用上下文,在函数体内通过this来获得对它的引用。要想以对象o的方法调用函数f(),可以这样使用call()和apply()
eg:
f.call(o);
f.apply(o);
上面每行代码和下面代码功能类型(假设对象o中预先不存在名为m的属性)
o.m=f;//将f存储为o的临时方法
o.m();//调用它,不传入参数
delete o.m;//将临时方法删除
注:在严格模式中,call()和apply()的第一个实参都会变为this值,哪怕传入的参数是原始值甚至是null或undefined。在非严格模式中,传入的null或undefined都会被全局对象所代替,而其原始值则会被相应的包装对象所替代。
对于call()来说,第一个调用上下文实参之后的所有实参就是要传入待调用函数的值。比如对象o的方法的形式调用函数f(),并传入两个参数
f.call(o,1,2);
apply()方法和call()方法类似,但传入实参的形式和call()有所不同,它的实参都放入一个数组中
f.apply(o,[1,2]);
7.4 bind()方法
bind() 方法主要作用是将函数绑定到某个对象中。当在函数f()上调用bind()方法并传入一个对象o做参数,这个方法将返回一个新的函数。
function f(y){
return this.x + y;
}
let o={
x:1
}
let g=f.bind(o);
g(2);//3
bind不仅将函数绑定至一个对象,它还附带一些其他应用:除了第一个实参外,传入bind()的实参也会绑定到this。eg
let sum = function(x,y){
return x+y;
}
let succ=sum.bind(this,1);
console.log(succ(2));//3
function f(y,z){
return this.x+y+z;
}
let g=f.bind({x:1},2);
g(3);//6
自定义bind属性
Function.prototype.bind1 = function(o,/*,args*/) {
let that = this;
let argu=arguments;
console.log(arguments)
return function () {
let arg = [];
for (let i = 1; i < argu.length; i++) {
arg.push(argu[i])
}
for(let j=0;j
arg.push(arguments[j])
}
return that.apply(o, arg);
}
}
function f(y, z) {
return this.x + y + z;
}
let g = f.bind1({ x: 1 }, 2);
console.log(g(3));//6
7.5 toString()方法
和所有的js对象一样,函数也有toString()方法,es规范规定这个方法返回一个字符串,这个字符串和函数声明语句的语法相关。实际上,大多数的toString()方法的实现都返回函数的完整代码。内置函数往往返回一个类似于‘[ native code ]’ 的字符串作为函数体
7.6 构造函数
不管是通过函数定义语句还是函数直接量表达式,函数定义需要使用function关键字。但函数还可以通过Function()构造函数来定义 eg
var f =new Function('x','y',' return x*y');
这一行代码创建一个新的函数,这个函数和通过下面的代码定义的函数几乎等价
var f=function(x,y){
return x+y;
}
Function() 构造函数可以传入任意属性的字符串实参,最后一个实参所标识的文本就是函数体。它可以包含任意的js语句,没两条语句之间用分号分隔。传入的构造函数的其他所有的实参字符串是指定函数的形参名字的字符串。如果定义的函数不包含任何参数,只须给构造函数简单的传入一个字符串——函数体即可。
注:Function()构造函数并不需要传入实参以指定函数名。就行函数直接量一样,Function构造函数创建一个匿名函数。
关于Function()构造函数有几点需要特别注意
● Function()构造函数允许js在运行时动态的创建并编译函数
● 每次调用Function()构造函数都会解析函数体,并创建新的函数对象。如果是在一个循环或多次调用的函数中执行这个构造函数,执行效率会受影响。相比之下,循环中嵌套的函数和函数定义表达式则不会执行时都重新编译
● 最后一点,也是关于Function()构造函数非常重要的一点,就是它所创建的函数并不是使用词法作用域,相反,函数体代码的编译总会在顶层函数执行,
eg
let scope='global';
function constructFunction(){
let scope='local';
return new Function('return local')
}
constructFunction()();//global
7.7可调用对象
‘类数组对象’并不是真正的数组,但大部分场景下可以将其当做数组来对待。对于函数也存在类似的情况。‘可调用对象’是一个对象,可以在函数表达书中调用这个对象,所有的函数时可调用的,但并非所有的可调用对象都是函数。
截止目前,可调用对象在两个js实现中不能算作函数。首先,IE Web浏览器实现了客户端方法(诸如Window.alert()和Document.getElementsById()),使用可调用的宿主对象,而不是内置对象函数。IE中的这些方法在其他浏览器中也都存在,但它们本质上不是Function对象。IE9将它们实现为真正的函数,因此这类可调用的对象越来越罕见。
另外一个常见的可调用对象是RegExp对象,可以直接调用RegExp对象
如果想检测一个对象是否是真正的函数对象,eg
function isFunction(x){
return Object.prototype.toString.call(x) === '[object Function]'
}
8函数式编程
js并非函数式编程语言,但在js中可以像操作字符对象一样操控函数,也就是说可以在js中应用函数式编程技术。比如数组方法的map就非常适应于函数风格。
8.1 使用函数处理数组
假设有一个数组,数组元素都是数字,我们想要计算这些元素的平均值和标准差。若使用非函数风格的话,代码会是这样
let data=[1,1,3,5,5];
let total=0;
//计算平均数
for(let i=0;i
total+=data[i];
}
let mean=total/data.length;
//计算标准差
total=0;
for(let i=0;i
let deviation=data[i] - mean;
total+=deviation*deviation;
}
let stddev = Math.sqrt(total/(data.length -1));
可以使用数组方法map()和reduce()来实现同样的计算,这种实现极其简单
var sum=function(x,y){
return x+y;
}
var square=function(x){
return x*x;
}
let data=[1,1,3,5,5];
let mean=data.reduce(sum)/data.length;
let deviations=data.map(function(x){
return x-mean;
})
Math.sqrt(deviations.map(square).reduce(sum)/data.length-1);
自定义map方法
Array.prototype.mapdev=function(f){
let res=[];
for(i=0;i
if(i in this){
res[i]=f.call(this,this[i],i,this);
}
}
return res;
}
data=[ {
name:'zuojiaojiao',
age:23
}, {
name:'zuojiaojiao2',
age:23
}, {
name:'zuojiaojiao3',
age:23
}];
let res=data.mapdev(function(x,i,item){
console.log(x);
console.log(i);
console.log(item)
return x.name;
})
console.log(res)
自定义reduce方法
let data=[ {
name:'zuojiaojiao',
age:23
}, {
name:'zuojiaojiao2',
age:23
}, {
name:'zuojiaojiao3',
age:23
}];
Array.prototype.reducedev=function(f){
let value=0;
for(let i=0;i
value+=f.call(this,this[i],i,this);
}
return value;
}
let tatal=data.reducedev(function(x){
return x.age*x.age
})
console.log(tatal)
8.3高阶函数
所为高阶函数就是操作函数的函数,它接收一个或多个函数作为参数。并返回一个新函数。eg
function not(f){
return function(){
let result = f.apply(this,arguments);
return !result;
}
}
var even=function(x){
return x%2===0;
}
var odd=not(even);
[1,1,3,5,5].every(odd);//true
function compose(f,g){
return function(){
return f.call(this,g.apply(this,arguments));
}
}
let square=function(x){
return x*x;
}
let sum=function(x,y){
return x+y;
}
var sq=compose(square,sum);
sq(2,3);//25
8.3不完全函数
函数f()的bind()方法返回一个新函数,给新函数传入特定的上下文和一组指定的参数,然后调用函数f()。我们说他把函数‘绑定至’对象并传入一部分参数。bind()方法只是将实参放在(完整实例列表的)左侧,也就是说传入bind()的实参都是放在传入原始函数的实参列表开始的位置,但有时我们期望将传入bind()的实参放在右侧。
eg:
//获取数组的某个片段
function array(a,n){
return Array.prototype.slice.call(a,n || 0);
}
//这个函数实参传递至左侧
function partialLeft(fn){
var args=arguments;
return function(){
var a=array(args,1);
a= a.concat( array(arguments));//然后增加所有的内容实参
console.log(a)
return f.apply(this,a);
}
}
//这个函数实参传递至右侧
function partitalRight(f){
var args = arguments;
return function(){
var a=array(arguments);
a=a.concat(array(args,1));
return f.apply(this,a);
}
}
//实参列表中的undefined值都被填充
function partial(f){
var args=arguments;
return function(){
var a=array(args,1);
var i=0;j=0;
for(; i
if(a[i]==undefined){
a[i]=arguments[j++];
a=a.concat(array(arguments,j));
return f.apply(this,a);
}
}
}
}
var f=function(x,y,z){
return x*(y - z);
}
console.log(partialLeft(f,2)(3,4));//2*(3-1)
console.log(partitalRight(f,2)(3,4));//3*4-2
console.log(partial(f,undefined,2)(3,4));//3*(2-4)
8.4 记忆
在有的案例中国定义了一个阶乘函数,它可以将上次的计算结果缓存起来。在函数式编程当中,这种缓存技巧叫做‘记忆’。下面的代码展示了一个高阶函数,memorize()接收一个函数作为实参,并返回带有记忆能力的函数。
memorize()函数创建一个新对象,这个对象被当做缓存(的宿主)并赋值给一个局部变量,因此对于返回的函数来说它是私有(在闭包中)。所返回的函数将它的实参数组转换为字符串,并将这个字符串用做缓存对象的属性名,如果在缓存中存在这个值,则直接返回它。
eg:
function memorize(f){
var cache={};//将值保存在闭包中
return function(){
//将实参转为字符串形式,并将其用做缓存键
var key=arguments.length + Array.prototype.join.call(arguments,',');
if(key in cache){
return cache;
}else{
return cache[key]=f.apply(this,arguments);
}
}
}
var factorial=memorize(function(n){
return (n<1) ? 1:n*factorial(n-1)
})