Javascript-8函数

JavaScript-8函数

概况

  • 形参实参
  • this
  • 对象的方法
  • 构造函数
  • 函数即对象
  • 闭包
    • 函数可以嵌套在其他函数中定义,这样他们就可以访问他们被定义时所处的作用域中的任何变量,这意味着Javascript函数构成了一个闭包

8.1函数定义

  • 两种方式
    • 函数定义表达式
    • 函数声明语句
  • 函数定义的必须部分
    • 函数名称标识符
    • 一对圆括号。包含由0个或者多个用逗号隔开的标识符组成的列表,这些标识符是函数的参数名称,他们就像函数体中的局部变量一样
    • 一对花括号(包含0或多条JavaScript语句,这些语句构成了函数体)
1
2
3
4
5
6
7
var square = function(x){ return x*x ;}
//函数表达式可以包含名称,这在递归时很有用
var f = function fact(x){if(x<1)return 1; else return x*fact(x-1)};
//函数表达式也可以作为参数传给其他函数
data.sort(function(a,b){ return a-b; });
//函数表达式有时定义后立即调用
var tensquared = (function(x){return x*x;}(10));
  • 函数声明语句的例子
1
2
3
4
function printprops(o){
for(var p in o)
console.log(p+ ":" +o[p]+ "\n");
}

示例

示例

示例

箭头函数

​ 为什么叫Arrow Function?因为它的定义用的就是一个箭头

1
2
3
4
5
x=>x*x;
===
function(x){
return x*x;
}
  • 箭头函数相当于匿名函数,有两种格式
    • 只包含一个表达式, 省略了{…}和return,就像上面
    • 包含多条语句,这时不能省略
    • 如果参数不是一个,就需要用括号()括起来
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//一个参数
x=>{
if(x>0){return x*x ;}
else {return -x*x ;}
}
//两个参数
(x,y)=>x*x+y*y
//无参数 ()=>3.14;
//可变函数
(x,y,...rest)=>{
var i,sum = x+y;
for(i=0;i<rest.length;i++)
{
sum+=rest[i];
}
return sum;
}
  • 如果要返回一个对象,就要注意,如果是单表达式,这么写会报错:

    • x=>{foo:x}
  • 因为和函数体的{…}有语法冲突,所以要改为:

    • x=>({foo:x})
  • 箭头函数和匿名函数有个明显的区别:箭头函数内部的this是词法作用域,由上下文确定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var obj={
birth: 1997,
getAge: function(){
var b = this.birth;
var fn = function(){
return new Date().getFullYear()-this.birth;
//this指向undefined而不是obj
};
return fn();
}
};
//这样逻辑是有问题的
obj.getAge();//NaN

var obj ={
birth:1997,
getAge:function(){
var b = this.birth;
var fn = ()=>new Date().getFullYear()-this.birth;
return fn();
}
};
obj.getAge();//23
//这样箭头函数完全修复了this的指向,this总是指向词法作用域,也就是最外层调用者obj

8.2 函数调用

  • 调用JavaScript函数的四种方式
    • 作为函数
    • 作为方法
    • 作为构造函数
    • 通过他们的call()和aply()方法间接调用

函数调用

1
2
3
printprops({x:1});
var total = distance(0,0,2,1)+distance(2,3,1,5);
var probability = factorial(5)/factorial(13);

方法调用

  • 方法调用和汉时调用的重要区别:
    • 调用上下文
  • 方法调用通过属性访问表达式
  • 属性访问表达式由两部分组成
    • 一个对象(o): 调用上下文,函数体可以用this引用
    • 属性名称(m)
  • 方法是个保存在一个对象的属性里的JavaScript函数
1
o.m = f;

给对象o定义了方法m(),调用时候她就像这样

1
2
3
o.m();
o.m(x,y);
o["m"](x,y); //o.m(x,y);的另一种写法
  • 例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var calculator = {
operand1:1,
operand2:1,
add: function(){
this.result = this.operand1+this.operand2;
}
};
calculator.add(); //这个方法调用计算1+1的结果
calculator.result ;// =>2
/*
这个例子和上面的例子是不一样的,上面的例子中,this外面还有一层函数,在那个函数中用匿名函数
会不指向obj,而是指向第二层函数。
var obj={
birth: 1997,
getAge: function(){
var b = this.birth;
var fn = function(){
return new Date().getFullYear()-this.birth;
//this指向undefined而不是obj
};
return fn();
}
};
但是这个例子中,只有一层,this直接指向obj

*/
方法链
  • 当方法的返回值是一个对象,这个对象还可以再调用他的方法。形成链,每次调用的结果都是另外一个表达式的组成部分
1
2
3
4
customer.surname.toUpperCase();	//调用customer.surname的方法
f().m(); //在f()调用结束后继续调用返回值的方法m()
//下面这个表达式是说:找到所有的header,取得他们id的映射,转换成数组并对它们进行排序
//$(": header").map(function(){return this.id}).get().sort();
  • 如果在设计的API中一直采用这种方式(每个方法都返回this)就可以进行链式调用
1
shape.setX(100).setY(100).setSize(50).setOutline("red").setFill("blue").draw();
  • 注意

    • 不要把方法的链式调用和构造函数的链式调用混为一谈

    • 方法和this关键词是JS面向对象编程的核心

    • 方法调用实际上都会传入一个隐式实参(一个对象),方法调用的母体就是这个对象

    • ```javascript
      rect.setSize(width,height);
      setRectSize(rect,width,height);

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18

      + 我们假设这两行代码的功能完全一样,他们都作用于一个假定的对象rect。可以看出第一行的方法调用语法非常清晰的表明这个函数执行的载体是rect对象,函数中的所有操作都将基于这个对象

      + this是一个关键字,不是变量也不是属性名,不允许给this赋值

      ```javascript
      var o ={
      m:function(){
      var self = this; //讲this的值保存到一个变量中去
      console.log(this===0); //输出true,this就是这个对象o
      f(); //调用辅助函数f()

      function f(){
      console.log(this===o);//false:this的值是全局对象或者undefined
      console.log(self===o);//true:self指外部函数的this值,也就是o
      }
      }
      }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function getAge(){
var y = new Date().getFullYear();
return y-this.birth();
}
var xiaoming ={
name: '小明',
birth: 1990,
age: getAge
};
xiaoming.age();//30
getAge(); //NaN 因为this指向全局去了,全局里面并没有birth()

var fn = xiaoming.age;
fn(); //这样也是NaN

这样是不对的!!!

示例

这样就对了

示例

创建对象时构造

  • 创建过程
    • 首先创建一个新的空对象,然后传入指定的参数并将这个新对象当作this的值来调用与i个指定的函数。这个函数使用this来初始化新创建对象的属性
    • 构造函数一般不会返回值,这个新创建并被初始化后的对象就是整个对象创建表达式的值
    • 如果构造函数确实会返回一个对象值,则此时对象作为整个对象创建表达式的值,新创建的对象就废弃了
    • 如果构造函数使用return单没有指定返回值,或者返回一个原始值,这时将忽略返回值,而使用这个新对象作为调用结果

间接调用

示例

8.3函数的实参和形参

存在的问题

  • 函数定义未指定函数形参的类型
  • 函数调用没有对传入的实参值做类型检查
  • 不检查传入形参的个数

可选形参

  • 若传入的实参比函数声明式指定的形参个数少,剩下的形参都将是只给undefined值
    • 应该给省略的参数赋一个合理的默认值

示例

  • 注意
    • 用这种可选形参来实现函数时,需要将可选形参放在参数列表的最后,否则必须传入null或者undefined作为第一个形参
      • 在函数定义中使用注释/optional/来强调形参式可选的

可变长的实参列表:实参对象

  • 若传入的实参个数超过函数定义时的形参个数,使用参数对象解决
    函数体内,标识符arguments式指向实参对象的引用
    • 通过数字下表就能访问传入函数的实参值
    • 省略的实参都是undefined,多出的参数自动省略
    • arguments并不是真正的数组,他是一个实参对象(它是一个对象,只是碰巧具有以数字为索引的属性),实参对象式一个类数组对象

示例

示例

  • 在非严格模式下,当一个函数包含若干形参,实参对象的数组元素是函数形参所对应实参的别名,并且形参名称可以认为是相同变量的不同命名,单严格模式下不行
剩余参数

剩余参数语法允许我们将一个补丁数量的参数表示为一个数组

  • 语法
  • 示例
  • 例子
  • 示例
  • 示例
1
2
3
4
5
6
7
function sum(...Args){
return theArgs.reduce((previous,current)
{
return previous+current;
});
}
console.log(sum(1,2,3)); //6
剩余参数和arguments对象的区别
  • 剩余参数只包含哪些没有对应形参的实参,arguments对象包含了传给函数的所有实参
  • arguments对象不是一个真正的数组,而剩余函数是真正的array实例,也就是你能够在它上面直接使用所有的数组方法,比如sort,map,forEach和pop()

将对象属性用作实参

  • 通过名/值 对的形式传入参数,是参数顺序无关紧要
  • 例子

示例

13

实参类型

  • JavaScript方法的形参并未声明类型,在形参传入函数体之前也没有做类型检查
  • 应当在程序中添加实参类型检查逻辑

示例

默认参数值

  • 函数默认参数允许在没有值或undefined被传入时使用默认形参
1
2
3
4
5
function multiply(a,b=1){
return a*b;
}
console(multiply(5,2)); //10
console(multiply(5)) //5

8.4作为值的函数

  • 函数不仅是一种语法,也是值
    • 可以将函数赋值给变量,存储在对象的属性或数组的元素中,作为参数传入另外一个函数等

      赋值给变量

1
2
3
4
function square(x){ return x*x ;}
var s = square; //s 和 square指代同一个函数
square(4); //16
s(4); //16

赋值给对象的属性

1
2
var o = {square: function(x){return x*x ;}} //对象直接量
var y = o.squre(16); //y=256

赋值给数组元素

1
2
var a = [function(x){return x*x ;},20];	//作为数组的一个成员
a[0](a[1]); //400

作为参数传入另一个函数

1
2
3
4
5
6
7
8
9
10
11
function add(x,y){ return x+y ;}
function substract(x,y){return x-y;}
function multiply(x,y){return x*y ;}
function multiply(x,y){return x/y;}
//这里的函数以上面的某个函数作为参数
//并给他传入两个操作数然后调用它
function operate(operator,operad1,operand2){
return operaor(operand1,operand2);
}
//这行代码所示的函数调用实际上计算了(2+3)+(4*5)的值
var i = operate(add,operate(add,2,3),operate(multiply,4,5));

自定义函数属性

  • 函数可以拥有属性
1
2
3
4
5
6
7
8
//初始化函数对象的计数器属性
// 有序函数声明被提前了,因此这里是可以在函数声明之前给他成员赋值的
uniqueInteger.counter = 0;
//每次调用这个函数都会返回一个不同的整数
//它使用一个属性来记住下一次将要返回的值
function uniqueInteger(){
return uniqueInteget.counter++;//先返回计数器的值,然后计数器自增
}
  • 例子
1
2
3
4
5
6
7
8
9
10
11
//计算阶乘,并将结果缓存到函数的属性中
function factorial(n){
if(isFinite(n)&&n>0&&n==Math.round(n)){
//有限的正整数,round是四舍五入的函数,这里来判断n是不是整数。isFinite()判断是否无限
if(!(n in factorial))//如果这个位置没有缓存结果
factorial[n]=n*factorial(n-1);
return factorial[n]; //返回缓存结果
}
else return NaN;
}
factorial[1]=1

8.5 作为命名空间的函数

  • 不在任何函数内声明的变量是全局变量,在整个JavaScript程序中都是可见的
  • 我们常常就简单的定义一个函数用作临时的命名空间,在这个命名空间内定义的变量都不会污染到全局命名空间
1
2
3
4
5
6
function mymodule(){
//模块代码
//这个模块所使用的所有变量都是局部变量
//而不是污染全局命名空间
}
module();
1
2
3
(function(){
//模块代码
}());//结束函数定义并立即调用它

8.6 闭包

  • 闭包:函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在函数的作用域内
1
2
3
4
5
6
7
var scope ="global scope";
function checkscope(){
var scope = "local scope";
function f(){return scope;}
return f();
}
checkscope(); => "local scope"
1
2
3
4
5
6
7
var scope ="global scope";
function checkscope(){
var scope = "local scope";
function f(){return scope;}
return f;
}
checkscope()(); => 返回值是什么? 也是"local scope"
  • 闭包可以捕捉到局部变量(和参数),并一直保存下来,看起来想这些变量绑定到了在其中定义他们的外部函数
1
2
3
4
var uniqueInteger = (function(){//定义函数并立即调用
var counter = 0; //函数的私有状态,外面访问不到counter属性
return function(){return counter++;};
}());
  • 例子
1
2
3
4
5
6
7
8
9
10
11
12
13
function counter(){
var n = 0;
return {
count : function(){return n++;}
reset : function(){n=0;}
};
}
var c= counter().d= counter();
c.count() //0
d.count() //0 c d 互不干扰
c.reset() //reset()和count()方法共享状态
c.count() //0:因为我们把c重置了
d.count() //1:没有重置d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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");
}
}
}
var c= counter(1000);
c.count; //1000
c.count; //1001
c.count = 2000;
c.count; //2000
c.count = 2000; //Error
//也不是很好
1
2
3
4
5
6
7
8
9
10
11
12
function addPrivateProperty(o,name,predicate){
var value;
o["get"+name] = function(){ return value;}
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";});
  • 例子
1
2
3
4
5
6
7
//这个函数返回一个总是返回v的函数
function constfunc(v){ return function(){return v;};}
//创建一个数组用来存储常数函数
var funcs = [];
for(var i=0;i<10;i++) funcs[i] = constfunc(i);
//在第五个位置的元素所表示的函数返回值为5
funcs[5]() //=> 5
  • 例子
1
2
3
4
5
6
7
8
9
10
11
//返回一个 函数 组成的数组,它们的返回值是0-9
function constfuncs(){
var funcs = [];
for(var i=0;i<10;i++)
//这时候创建了10个闭包,都共享了i,当i变化,所有的闭包都会返回相同的值
funcs[i] = function(){return i;}
return funcs;
}

var funcs = constfuncs();//变量i的值是10
funcs[5]()//返回值是什么,是10
  • 注意
    • 每个函数调用都包含一个this值,闭包在外部函数里是无法访问this的
      • 除非外部函数将this转存为一个变量
1
var self = this;//将this保存至一个变量中,以便嵌套的函数能够访问它
  • 闭包具有自己所绑定的arguments,因此闭包内无法直接访问外部函数的参数数组
    • 除非外部函数将参数数组保存到另外一个变量中:
1
var outerArguments = arguments;	//保存起来以便嵌套的函数能使用它

8.7函数属性、方法和构造函数

  • 函数也是对象,他们也可以拥有属性和方法
    • 函数属性和方法
    • Function()构造函数

length属性

  • arguments.length 表示传入函数的实参的个数
  • 函数本身的length属性值得是形参的数量
1
2
3
4
5
6
7
8
9
10
function check(args){
var actual = args.length; //实参的真是个数
var excepted = args.callee.length; //期望的实参个数
if(actual!== expected) //如果不同则抛出异常
throw Error("Expected"+ expected + "args;got "+ actual);
}
function f(x,y,z){
check(arguments); // 检查实参个数和期望的实参个数是否一致
return x+y+z; // 再执行函数的后续逻辑
}
1
2
3
4
5
function foo (a,b,...rest){
console.log('a='+a);
console.log('b='+b);
}
foo.length; //2 不会管..rest

prototype属性,在第九章进一步讨论

call() 和 apply()

  • call()和apply()的第一个实参都变为this的值,即使传入的实参是原始值或者null或者undefined
  • call(),第一个参数之后的所有实参是要传入带调用函数的值;
1
2
f.call(o,1,2);
//对于这个对象来说调用函数,传入参数1,2
  • apply()实参都放到一个数组
1
f.apply(o,[1,2]);
例子
1
2
3
4
5
6
7
8
9
10
11
12
function getAge(){
var y = new Date().getFullYear();
return y = this.birth;
}
var xm = {
name:"小明",
birth:1990;
age: getAge;
}
xm.age();
getAge.apply(xm,[])//25,this指向xm这个对象
//apply是应用的意思,可以理解为把函数引用到这个对象上

bind()方法

  • 作用是把函数绑定到某个对象
1
2
3
4
function f(y){return this.x+y;}	//这是个待绑定的函数
var o = {x:1}; //将要绑定的对象
var g = f.bind(o); //通过调用g(x)来调用o.f(x)
g(2)//3
  • 除了第一个实参之外,传入bind()的实参也会绑定到this
  • 示例

toString()方法

示例

Function()构造函数

  • FUnction()构造函数可以传入任意数量的字符串实参,最后一个实参所表示的文本就是函数体
  • 除了最后一个实参,其他实参字符串是指定函数的形参名字的字符串
1
2
3
var f = new Function("x","y","return x*y");
var f = function(x,y){return x*y;}
//这两个是一样的
  • 需要特别注意
    • 在运行时动态的创建并编译函数
    • 每次调用Function()构造函数都会解析函数体,并创建新的函数对象
    • 所创建的函数代码的编译总是会在顶层函数(全局作用域)执行

8.8函数式编程

  • 在JavaScript中可以像操控对象一样操控函数,也就是说可以在Javascript中应用函数式编程技术、

使用函数处理数组

1
2
3
4
5
6
7
8
9
//首先定义两个简单的函数
var sum = function(x,y){return x+y;}
var square = function(x){return x*x;}
//然后将这些函数和数组的方法配合使用计算出平均数和标准差
var data = [1,1,3,5,5];
var mean = data.reduce(sum)/data.length;
var deviation = data.map(function(x){return x-mean;});
//计算标准差
var stddev = Math.sqrt(deviation.map(square).reduce(sum)/(data.length -1));

高阶函数

  • 操作函数的函数,它姐手一个或者多个函数作为参数,并返回一个新函数
1
2
3
4
5
6
7
8
9
10
11
12
13
//这个高阶函数会返回一个新的函数,这个新函数将他的实参传入f()
//并且返回f的返回值的逻辑非
function not(f){
return function(){
var 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 每个元素都是奇数
1
2
3
4
5
6
7
8
9
//所返回的函数的参数应当是一个实参数组,并对每个数组元素执行函数f()
//并返回所有计算结果组成的数组
//可以对比一下这个函数和上文提到的map()函数
function mapper(f){
return function(a){ return map(a,f)};
}
var increment = function(x){return x+1;};
var incrementer = mapper(increment);
incrementer([1,2,3]) ; // [2,3,4]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//返回一个新的可以计算f(g(...))的函数
//返回的函数h()将它所有的实参传入g(),然后将g()的返回值传入f()
//调用f()和g()时的this值和调用h()时的this是同一个this
function compose(f,g){
return function(){
//需要给f()传入一个参数,所以使用f()的call方法
//需要给g()传入很多参数。所以用g()的apply()方法
return f.call(this,g.apply(this,arguments));
};
}
var sum = function(x,y){return x+y;}
var square = function(x){return x*x;}
var squareofsum = compose(square,sum);
squareofsum(2,3) ;//25

不完全函数

  • 这个函数可以接受一些参数,这些参数中有一些参数可以被绑定成其他函数,然后返回一个新的函数,这个新的函数接收剩下的未绑定的的参数

记忆

  • 将上次的计算结果缓存起来,在函数式编程当中,这种缓存技巧叫做记忆
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//返回f()的带有记忆功能的版本
//只有当f()的实参的字符串表示都不相同时它才会工作
function memorize(f){
var cache = {};//将值保存在闭包内
return function(){
//将实参转换为字符串形式,并将其用作缓存的键
var key = arguments.length + Array.prototype.join.call(arguments,",");
if(key in cache) return cache[key];//找到的话直接返回
else return cache[key] = f.apply(this,arguments);//否则把新的再调用
}
}
//返回两个整数的最大公约数
function gcd(a,b) {
var t;
if(a<b) {
t=b,b=a&t , a=t;
}
while (b!=0){
t=b,b=a%b , a=t;
}
return a;
}
var gcdmemo = memorize(gcd);
gcdmemo(85,187)//17



//注意,当我们写一个递归函数是,往往需要实现记忆功能
// 我们更希望调用实现了记忆功能的递归函数,而不是原递归函数
var factorial = memorize(function (n) {
return (n<=1)? 1: n*factorial(n-1);
});
factorial(5); //120
//先计算了factorial(1)。。。(4)并且有缓存,不会重复计算
-------------本文结束,感谢您的阅读-------------