Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

函数 #11

Open
cy0707 opened this issue Oct 8, 2016 · 0 comments
Open

函数 #11

cy0707 opened this issue Oct 8, 2016 · 0 comments

Comments

@cy0707
Copy link
Owner

cy0707 commented Oct 8, 2016

函数的定义

函数定义有两种方式:一是函数的声明,二是函数的表达式。

//函数的声明
function functionName(arg0, arg1, arg2) {
    //函数体
}
//函数的表达式
var functionName = function() {
 //函数体
}

对于函数的声明:有一个重要的特征那就是函数声明提升,其意思是在执行代码之前会读取函数的声明,这就意味着可以把函数声明放在调用它的语句的后面。

//函数的调用
   sayHi();
//输出的是hello
function sayHi(){
 console.log("hello");
}

函数的表达式:这种形式看起来好像是变量赋值语句,即创建一个函数并将它赋值给变量functionName,在这种情况下创建的函数,叫匿名函数。因为function关键字后面没有标识符。函数表达式跟其他表达式一样,必须在使用它之前赋值。不然会报错。

//函数的调用,会报错,提示函数未定义
sayHi();
var sayHi = function(){
   console.log("hello");
} 

函数的参数

  1. ECMAScript函数不介意传递多少个参数以及参数的类型。
  2. 假如你定义了的函数只接受两个参数,在调用该函数也未必要传递两个参数,可以传递一个,二个,三个,或者不传参数。
  3. 在ECMAScript中函数的参数是用一个数组来表示的。函数接受到的始终都是这个数组,而不关心数组有哪些参数。
  4. 在函数体内可以通过arguments对象来访问这个参数数组,从而获得传递给函数的每一个参数。

arguments对象

  1. arguments对象只是与数组类似,但是它并不是Array()的实例。
  2. 可以使用方括号语法来访问它的每一个元素。例如arguments[0], arguments[1]。
  3. 使用length属性来确定传递进来多少个参数
function sayHi(){
 console.log("hello" + arguments[0] + "," + arguments[1] );
}

所以在js中函数的命名参数只是提供了便利,但是并不是必须的。

命名参数可以与arguments对象一起使用

function doAdd(num1, num2) {
   if(arguments.length ==1) {
       console.log(num1+10);
   }else if(arguments.length == 2){
      console.log(arguments[0]+num2);
   }
}
//调用函数
doAdd(1, 2);

关于arguments行为,有一点要注意的那就是他的值永远与对应的命名参数的值保持同步。

function doAdd(num1, num2) {
  arguments[1] = 10;
 console.log(arguments[1]+num2);
}
//调用函数
doAdd(1, 2);
//返回的是20,

我们对上面这个分析一下:

  1. 当我们每次执行这个doAdd()函数的时候,都会重写第二个参数,将第二个参数的值变为10。
  2. 这是因为arguments的值会自动反映到对应的命名参数,所以修改arguments[1]也就修改了num2。
  3. 不过需要注意的是,这并不是说读取这两个值会访问相同的内存空间,内存空间是独立的。但是值是同步的。
  4. 这种影响是单向的,修改命名参数的值不会改变arguments中对应的值。
  5. 还要记住如果只传入一个参数,那么arguments[1]设置的值不会反映到命名参数。这是因为arguments的长度有传入的参数决定,并不是定义函数时的命名参数的个数决定的。
  6. 对于没有传递值的命名参数会赋值为undefined。这就跟定义了变量未初始化一样的。

对于严格模式中,对arguments对象做了一些限制

  1. 像前面那样赋值会变得无效,也就是说把arguments[1] =10,num2的值依然是undefined的
  2. 重写arguments会导致语法错误

函数中所有参数都是按值传递

函数中所有参数都是按值传递:这句话意思是说把函数外部的值复制给函数内部参数。就是把值从一个变量复制到另一个变量一样。

基本类型值的传递如同基本类型变量复制一样,而引用类型值的传递,则如果引用类型变量的复制一样。访问变量有按值和按引用两种方式,而参数只能按值传递。

看一个基本类型例子:

//基本类型
var addTen(num){
   num+=10;
   return num;
}
var count =20;
var result = addTen(count);
console.log(result);
//返回的是30
console.log(count);
//返回的是20,没有改变

再看一个引用类型例子

//引用类型
function setName(obj){
  obj.name = "jerry";
  obj = new Object();
   obj.name = "gery";
}
var person = new Object();
setName(person);
console.log(person.name);
//返回的是jerry

基本类型很好理解,但是对用引用类型大家会疑惑,总是弄不清楚。我们可以对其分析一下

  1. 我们创建一个对象person,我们把它作为参数传入setName()函数中,
  2. 我们此时参数是按照值传递,相当于把person复制参数obj,此时obj与person都是指向一个对象。
  3. 我们给obj的name赋值为jerry,那么person的name属性值也是jerry
  4. 然后,我们又对ob重新赋值为一个新对象,导致ob不在指向person的那个对象,所以他的name属性值为gery,根本没有影响到person的值。
  5. 如果是按引用传递的话,结果就是gery而不是jerry.

函数没有没有重载

function addSomeNumber(num) {
   return num+100;
}
function addSomeNumber(num) {
   return num+200;
}
var result = addSomeNumber(100)
//结果是300

第二个函数覆盖第二个函数。

函数返回值

在定义函数的时候,不必指定是否有返回值,实际上,任何函数在任何时候都可以通过return语句后跟要返回的值来实现返回值。

function sum(num1, num2) {
    return num1+num2;
}
  1. 除了return语句,没有任何声明表示该函数会返回一个值。
  2. 函数会在执行return语句之后停止并立即退出,因此位于return语句之后的任何代码永远都不会执行。
  3. return语句也可以不带任何返回值,函数在停止执行后,会返回undefined值。这种用法一般用在需要提前停止函数,又不要返回值的情况下使用。

在严格模式对函数有一些限制

  1. 不能把函数名命名为eval或者arguments
  2. 不能把参数命名为eval或者arguments
  3. 不能出现两个命名参数同名的情况。不然会报错

递归函数

递归函数是一个函数通过名字调用自身的情况下构成。

function factorial(num) {
   if(num <=1) {
    return 1;
  }else {
     return  num*factorial(num-1);
  }
}
//这上面一个是一个经典的递归函数,看上去并没有问题,但是如果这样的话
var factorial1 = factorial;
factorial =null;
console.log(factorial1(4));
//会报错

我们对上面这个进行分析一下,为什么会出现这样,是因为函数保存在变量factorial1中,再对这个函数赋值为null,因为这个是引用类型,所以这个是函数也是null,调用这个函数已经不存在,所以会报错。

对于上面这种情况,我们可以采用下面两种方式来解决

第一种:利用arguments.callee来实现,arguments.callee是代表一个正在执行的函数指针。因此可以用来实现对函数的递归调用---这个在严格模式下面会出错,因为严格模式下,不能通过脚本访问arguments.callee。

function factorial(num) {
   if(num <=1) {
    return 1;
  }else {
   return  num*arguments.callee(num-1);
  }
}
var factorial1 = factorial;
factorial =null;
console.log(factorial1(4));
//这样就不会报错了

第二种:利用函数的表达式

function factorial = (function f(num){
  if(num <=1) {
    return 1;
  }else {
   return  num*f(num-1);
  }
});
var factorial1 = factorial;
factorial = null;
console.log(factorial1(4));
//这样就不会报错了

模仿块级作用域

在JavaScript中并没有块级作用域的概念,这就意味着在块语句中定义的变量,实际上是在包含函数中而非语句中创建的。

function outputNumbers(count){
  for(var i=0; i<count; i++) {
      console.log(i);
   }
   console.log(i);
}

我们分析一下这个:

  1. 这个函数中我们定义了一个for循环,而变量i的初始值为0,。
  2. JAVA与C++语言中,变量i只会在for循环的语句块中有定义,循环一旦结束,变量i就会销毁。
  3. 可是在JavaScript中,变量i是定义在outputNumbers()的活动对象。
  4. 因此从它有定义开始,就可以在函数内部随处访问它

即使错误的重新声明一个变量同一个变量,也不会改变它的值。

function outputNumbers(count){
  for(var i=0; i<count; i++) {
      console.log(i);
   }
  var i; //重新声明同一个变量
   console.log(i);
}

JavaScript从来不会告诉你是否多次声明了同一个变量,遇到这种情况,它只会对后续的声明视而不见(不过,它会执行后续声明中的变量初始化)。对于这个问题,可以用匿名函数来模范块级作用域。

闭包

闭包:是有权访问另一个函数作用域中的变量的函数。
常见创建闭包的方式有:在一个函数内部创建另一个函数
(闭包的相关知识在作用域的那一节有了详细的介绍,不在介绍)

闭包与变量

作用域的这种配置机制引出一个值得注意的副作用,即闭包只能取得包含函数中任何变量的最后一个值,因为闭包所保存是整个变量对象,而不是某个特殊的变量。下面我们看个例子:

//创建了一个函数
function createFunction() {
//创建了一个数组
 var result = new Array();
//一个for循环
 for(var i=0; i<10; i++) {
//一个闭包
    result[i] = function(){
//访问的是其包含函数的变量
           return i;
      }
  }
}

返回的结果是一个数组,但是这个数组并不是[0,1..9] ,而是[10, 10..10],在每一个匿名函数的作用域链中都保存着createFunction()函数的活动对象,所以他们引用的是同一个变量i,所以当createFunction()函数返回后,变量i变成10,此时每一个匿名函数都引用着保存变量i的同一个变量对象。所以每个函数内部i的值都是10。

但是我们可以通过创建另一个匿名函数强制让闭包的行为符合预期

function createFunction() {
 var result = new Array();
 for(var i=0; i<10; i++) {
    result[i] = function(num){
           return function(){
               return num;
          }
      }(i);
  }
return result;
}

在这里,我们并没有直接把闭包赋值给数组,而是定义了一个匿名函数。并将立即执行该匿名函数的结果赋值给数组。

这里的匿名函数有一个参数num。也就是最终的函数要返回的值,在调用每个匿名函数时,我们传入了变量i,由于函数的参数是按值传递,所以就会将变量i的当前值复制给参数num,而在这个匿名函数内部,又创建并返回一个访问num的闭包,这样以来,result数组中的每一个函数都有自己num变量的一个副本,因此就可以返回各自不同的数值。

块级作用域(通常为私有作用域)

//块级作用域语法
 (function(){
    //这里是块级作用域
 })();

将函数声明包含在一对圆括号中,表示它实际上是一个函数表达式。而紧随其后的另一对圆括号会立即调用这个函数。可能这种语法有点不好理解。我们可以这样理解:

var count=5;
outputNumber(count);
//为了让代码更简洁
outputNumber(5);
//我们这样写,是因为变量只不过值的另一种表现形式。因此可以用实际值代替变量

var someFuncton = function(){
  //这里是块级作用域
};
someFuncton();
//根据上面的思路,可以这样写
//实际值代替变量
function(){
   //这里是块级作用域
}();

//但是,function是关键字是当作一个函数的声明的开始,函数声明后面不能跟圆括号。
//而函数的表达式后面可以跟圆括号,把函数声明转为函数表达式,只要像下面这样

(function(){
   //这里是块级作用域
})();

无论在什么地方,只要临时需要一些变量,就可以使用私有作用域。例如:

function outputNumber(count) {
 (function(){
    for(var i=0; i<count; i++){
          console.log(i);
     }
})();
console.log(i)
//会出错的。
}

因为在for循环外部插入了一个私有作用域,在匿名函数中定义的任何变量,都会在执行结束时被销毁。因此,变量i只能在循环中使用,使用后即被销毁。

在大型应用程序中,我们都应该尽量少向全局作用域添加变量和函数,因为过多的全局变量和函数,很容易导致命名冲突。通过创建私有作用域,每个开发人员可以使用自己的变量,又不必担心搞乱全局。
且这种做法可以减少闭包占用的内存问题。因为没有匿名函数的引用。只要函数执行完毕,就可以立即销毁其作用域链了。

私有变量

严格来说,在javascript中没有私有成员的概念,所有对象的属性都是共有的,但是有一个私有变量的概念:任何函数中定义的变量,都可以认为是私有变量,因此不能在函数的外部访问这些变量。

私有变量:包括函数的参数,局部变量和函数内部定义的其他函数。

function add(num1, num2) {
   var sum = num1 + num2;
   return sum;
}

在函数的内部,我们定义了三个变量 sum, num1, num2。 在函数内部可以访问这几个变量,但是在函数的外部则不能访问他们。如果在函数内部建一个闭包,闭包通过自己的作用域链也可以访问到这些变量。我们可以利用这种,创建用于访问私有变量的共有方法。

特权方法

我们把有权访问私有变量和私有函数的公有方法称为特权方法。有两种在对象上创建特权方法的方法。

第一种:在构造函数中定义特权方法

function MyObject(){
//私有变量和私有函数
    var privateVariable = 10;
    function privateFunction(){
       return false;
   }
//特权方法
   this.publicMethod = function(){
       privateVariable++;
       return  privateFunction();
   }
}

对这个例子而言, privateVariable和privateFunction()只能通过特权方法publicMethod来访问。在创建
MyObject的实例后,除了使用publicMethod()方法这一个途径外,没有任何方法可以直接访问privateVariable和privateFunction()

利用私有和特有成员,我们可以隐藏那些不应该被直接修改的数据。例如:

function Person(name) {
   this.getName = function(){
      return name;
   };
   this.setName = function(value){
          name = value;
   };
}

var person = new Person("jerry");
console.log(person.getName());
//返回jerry
person.setName("gery");
console.log(person.getName());
//返回gery

以上代码定义了两个特权方法,getName()和setName(),这两个函数在构造函数外部也可以访问,而且有权访问私有变量name.但在构造函数的外部,没有任何办法访问name。

这两个方法是在构造函数内部定义的,他们作为闭包能够通过作用域链访问name,私有变量name在Person的每一个实例都是不同的,每次调用这个构造函数,都会重新创建这两个方法。

利用构造函数定义特权方法,有一个缺点,那就是必须使用构造函数,而构造函数模式针对每一组实例,都会创建同样一组新方法。

静态私有变量

通过在私有作用域中定义私有变量和函数,同样可以创建特权方法。

(function(){
//私有变量和私有函数
    var privateVariable = 10;
    function privateFunction(){
       return false;
   }
//构造函数(注意这个是全局变量)
MyObject = function(){
};
//特权方法
   MyObject.prototype.publicMethod = function(){
       privateVariable++;
       return  privateFunction();
   }
})();

这个模式创建一个私有作用域,并在其中封装了一个构造函数及相应的方法。我们把公有方法定义在原型上面,这里体现了原型模式,我们创建了函数的表达式,并没有创建函数声明,因为函数声明只能创建局部变量,,而且出于同样的原因,我们也没有在声明MyObject时,使用var关键字。

记住:初始化未经声明的变量,总会创建一个全局变量,所以MyObject是一个全局变量,但是,我们在严格模式下,给未经声明的变量赋值会报错的。

这个方法与刚才在构造函数中定义特权方法的区别在于:私有变量个函数是共享的。由于特权方法在原型上定义的,因此所有实例都使用同一个函数,而这个特权方法,作为一个闭包,总是保存着对包含作用域的引用。

模块模式

模块模式:为单例创建私有变量和特权方法。
单例:指的是只有一个实例的对象。按照惯例,javascript中以对象字面量的方式来创建单例对象。

var singleton = {
    name :  value,
    method : function(){
     //这里是方法
    }
};
//模块模式通过为单例添加私有变量和特权方法,能够使其得到增强。
var singleton =function(){
//添加私有变量和函数
var privateVariable = 10;
function privateFunction() {
    return false;
} 
//特权方法
return  {
   publicProperty : true,
   publicMethod : function(){
          privateVariable;
         privateFunction();
     }
};
}();

这个模块模式使用了一个返回对象的匿名函数,在这个匿名函数,首先定义了私有变量和函数,然后将一个对象字面量作为函数返回。返回的对象字面量只包含可以公开的属性和方法,由于这个对象是在匿名函数内部定义的,因此它的公有方法有权访问私有变量和函数。

从本质上来讲:这个对象字面量定义的是单例的公共接口。这种模式在需要对单例初始化,同时又维护其私有变量时,是非常有用的。

如果必须创建一个对象并以某些数据对其进行初始化,同时还要公开一些能够访问这些私有数据的方法,就可以使用模块模式。以这种模式创建的每一个单例都是Object的实例。因为最终要通过一个对象字面的方法来表示它。

增强模块模式

这种增强的模块模式适合那些单例必须是某种类型的实例。同时还必须添加某些属性或方法对其加以增强的情况。

var singleton =function(){
//添加私有变量和函数
var privateVariable = 10;
function privateFunction() {
    return false;
} 
//创建对象
var object = new CustomType();
//特权方法
   objectProperty : true,
   objectMethod : function(){
          privateVariable;
         privateFunction();
//返回这个对象
return object;
}();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant