JS函数与作用域

函数声明和函数表达式有什么区别

  • JavaScript 中需要创建函数的话,有两种方法:函数声明、函数表达式,各自写法如下:
    // 方法一:函数声明
    function foo() {}
    
    // 方法二:函数表达式
    var foo = function () {};
    
  • 另外还有一种自执行函数表达式,主要用于创建一个新的作用域,在此作用域内声明的变量不会和其它作用域内的变量冲突或混淆,大多是以匿名函数方式存在,且立即自动执行:
(function () {
    // var x = ...
})();

此种自执行函数表达式归类于以上两种方法的第二种,也算是函数表达式。

方法一和方法二都创建了一个函数,且命名为 foo,但是二者还是有区别的。JavaScript 解释器中存在一种变量声明被 提升(hoisting) 的机制,也就 是说变量(函数)的声明会被提升到作用域的最前面,即使写代码的时候是写在最后面,也还是会被 提升 至最前面。

** 例如以下代码段:**

alert(foo); // function foo() {}
alert(bar); // undefined
function foo() {}
var bar = function bar_fn() {};
alert(foo); // function foo() {}
alert(bar); // function bar_fn() {}

输出结果分别是function foo() {}undefinedfunction foo() {}function bar_fn() {}

可以看到 foo 的声明是写在 alert 之后,仍然可以被正确调用,因为 JavaScript 解释器会将其提升到 alert 前面,而以函数表达式创建的函数 bar则不享受此待遇。

那么bar 究竟有没有被提升呢,其实用 var声明的变量都会被提升,只不过是被先赋值为 undefined 罢了,所以第二个 alert 弹出了 undefined

所以,JavaScript 引擎执行以上代码的顺序可能是这样的:

  1. 创建变量 foobar,并将它们都赋值为 undefined
  2. 创建函数 foo 的函数体,并将其赋值给变量 foo
  3. 执行前面的两个 alert
  4. 创建函数 bar_fn,并将其赋值给 bar
  5. 执行后面的两个 alert

注:

严格地说,再 JavaScript 中创建函数的话,还有另外一种方法,称为“函数构造法”:

var foo = Function('alert("hi!");');
var foo = new Function('alert("hi!");'); // 等同于上面一行

new function是可以传参数的。
比如

var sum= new  Function("a", "b", "c", "return a+b+c");
sum(1,2,3)//6

什么是变量的声明前置?什么是函数的声明前置

先来看一个例子

fn1();   // 输出:我是函数声明
fn2();   //  报错
console.log(a); // 输出:undefined

function fn1() {
    console.log( "我是函数声明" );
}

var fn2 = function() {
    console.log( "我是函数表达式" );
}

var a = 20 

因为JS对使函数声明前置,所以fn1()在函数声明前执行仍然可以得到正确答案,而函数表达式fn2则报错,
为什么?我们先要弄清楚:

JS解释器如何找到我们定义的函数和变量?
通过 变量对象(Variable Object, VO)来获取。VO是一个抽象概念的“对象”,它用于存储执行上下文中的:1. 变量2. 声明3. 函数参数

函数的VO分为两个阶段——变量初始化代码执行。在变量初始化阶段,VO按照如下顺序填充:
1. 函数参数(若未传入,初始化该参数值为undefined)
2. 函数声明(若发生命名冲突,会覆盖)
3. 变量声明(初始化变量值为undefined,若发生命名冲突,则忽略)

注意:函数表达式与变量初始化无关。

在变量初始化阶段,需要先对arguments变量进行初始化(激活对象AO),再把函数体内的变量声明与函数声明存储在AO内,VO(functionContext) === AO

根据 VO数据 填充顺序看以下例子

var x = 10;
bar();

function foo() {
    console.log(x);
}

function bar() {
   var x = 30;
    foo();  //得到什么? 
}

/*
1. globalContext = {
    AO: {
          x: 10
          foo: function
          bar: function 
    }
    Scope: null
}

// 声明 foo 时 得到下面
foo.[ [scope] ] = globalContext.AO
// 声明 bar 时 得到下面
bar.[ [scope] ] = globalContext.AO

// 当调用bar(),进入bar 的执行上下文

2. barContext = {
    AO: {
         x: 30
    }
    Scope = bar.[ [scope] ]    // globalContext.AO
}

//当调用 foo() 时,先从 foo 执行上下文中的 AO里找,找不到再从 foo 的 [[scope]]里,找到后即调用
因为在foo执行上下文中找不到x,所以直接进入foo的scope里面找,即globalContext.AO

3. fooContext =  {
    AO: {
          
    }
    Scope = foo.[ [scope] ]  // globalContext.AO
}

*/

globalContext = {
    AO: {
          x: 10
          foo: function
          bar: function 
    }
    Scope: null

所以console.log(x) 是 10

arguments 是什么

  • arguments 是一个类似数组的对象, 对应于传递给函数的参数。
  • arguments.length表示的是实际上向函数传入了多少个参数。
  • arguments对象是所有函数中可用的局部变量。你可以使用arguments对象在函数中引用函数的参数。此对象包含传递给函数的每个参数的条目,第一个条目的索引从0开始。例如,如果一个函数传递了三个参数,你可以参考它们如下:

arguments[0]
arguments[1]
arguments[2]

  • 在函数内部,你可以使用arguments对象获取到该函数的所有传入参数
Paste_Image.png

函数的"重载"怎样实现

首先想声明下,什么是函数重载,javascript中不存在函数重载的概念,(其实是个伪命题)但一个函数通过不同参数列表(arguments)来实现各个功能,我们都叫函数重载,这就是牛逼闪闪的 JavaScript 函数重载

/*
    * 传统方法一
    * */
    var overrideMethod = function () {
        switch (arguments.length) {
            case 0 :
                console.log("class no body");
                break;
            case 1 :
                console.log("class has one student");
                break;
            default :
                console.log("class has more students");
        }
    }
    overrideMethod("test","blue"); //class has more students
    overrideMethod("test-blue"); //class has one student
    overrideMethod(); //class no body
    /*
    * 我们希望对象Company拥有一个find方法,当不传任何参数时,
    * 就会把Company.names里面的所有元素返回来;
    * 因为find方法是根据参数的个数不同而执行不同的操作的,
    * 所以,需要有一个overrideCompanyFind方法,能够如下的为Company添加find的重载:
    * */
    var company = {
        names : ["baron" , "Andy" ,"Lily" , "Blures"],
        find : function () {
            return this.names.length
        }
    };
    var overrideCompanyFind = function (object , method , cb) {
        var oldMethod = object[method];
        //给object 重新赋予新的方法
        object[method] = function () {
            if (cb.length == arguments.length) {
               return cb.apply(this,arguments)
            }else if(typeof oldMethod== 'function'){
               return oldMethod.apply(this,arguments)
            }
        };
    };
    overrideCompanyFind(company,'find',function (name , name2) {
        return this.names
    });
    overrideCompanyFind(company,'find',function (name) {
        return name + '的位置是' + this.names.indexOf(name) + '排'
    });
    console.log(company.find()); //4
    console.log(company.find('Lily')); // Lily的位置是2排
    console.log(company.find('Lily','baron')); //["baron", "Andy", "Lily", "Blures"]

立即执行函数表达式是什么?有什么作用

**立即调用函数表达式可以令其函数中声明的变量绕过JavaScript的变量置顶声明规则,还可以避免新的变量被解释成全域变量或函数名占用全域变量名的情况。与此同时它能在禁止访问函数内声明变量的情况下允许外部对函数的调用。有时,这种编程方法也被叫做“自执行(匿名)函数”,但“立即调用函数表达式”是语义上最准确的术语。 **

// 下面2个括弧()都会立即执行

(function () { /* code */ } ()); // 推荐使用这个
(function () { /* code */ })(); // 但是这个也是可以用的

// 由于括弧()和JS的&&,异或,逗号等操作符是在函数表达式和函数声明上消除歧义的
// 所以一旦解析器知道其中一个已经是表达式了,其它的也都默认为表达式了
// 不过,请注意下一章节的内容解释

var i = function () { return 10; } ();
true && function () { /* code */ } ();
0, function () { /* code */ } ();

// 如果你不在意返回值,或者不怕难以阅读
// 你甚至可以在function前面加一元操作符号

!function () { /* code */ } ();
~function () { /* code */ } ();
-function () { /* code */ } ();
+function () { /* code */ } ();

// 还有一个情况,使用new关键字,也可以用,但我不确定它的效率
// http://twitter.com/kuvos/status/18209252090847232

new function () { /* code */ }
new function () { /* code */ } () // 如果需要传递参数,只需要加上括弧()

有什么用?

javascript中没用私有作用域的概念,如果在多人开发的项目上,你在全局或局部作用域中声明了一些变量,可能会被其他人不小心用同名的变量给覆盖掉,根据javascript函数作用域链的特性,可以使用这种技术可以模仿一个私有作用域,用匿名函数作为一个“容器”,“容器”内部可以访问外部的变量,而外部环境不能访问“容器”内部的变量,所以( function(){…} )()内部定义的变量不会和外部的变量发生冲突,俗称“匿名包裹器”或“命名空间”。


求n!,用递归来实现

function fn(n){
    if(n===1){
         return 1;
    }
   alert( n * fn(n-1));
}
fn(5);

以下代码输出什么?

function getInfo(name, age, sex){
        console.log('name:',name);
        console.log('age:', age);
        console.log('sex:', sex);
        console.log(arguments);
        arguments[0] = 'valley';
        console.log('name', name);
    }

getInfo('饥人谷', 2, '男');  //  name: 饥人谷    age:2    sex:男    ["饥人谷", 2, "男"]    name valley
getInfo('小谷', 3);   // name: 小谷    age:3    sex:undefined    ["小谷", 3]    name valley
getInfo('男');   // name: 男    age:undefined    sex:undefined    ["男"]    name valley

写一个函数,返回参数的平方和?

   function sumOfSquares(){
        var sum = 0;
       for (var i=0; i<arguments.length; i++) {
              sum +=  arguments[i] * arguments[i];
       }
       console.log(sum);
   }
   var result = sumOfSquares(2,3,4)
   var result2 = sumOfSquares(1,3)
   console.log(result)  //29
   console.log(result)  //10

如下代码的输出?为什么

    console.log(a);    //  undefined
    var a = 1;       
    console.log(b);     // 报错

JS变量声明会被 提升 所以console.log(a) var a 会提前,但是还没被赋值,所以输出undefined
console.log(b) 因为变量b没有被声明,所以报错

如下代码的输出?为什么

    sayName('world');     // hello world
    sayAge(10);          // 报错
    function sayName(name){
        console.log('hello ', name);
    }
    var sayAge = function(age){
        console.log(age);
    };

sayName('world');根据函数声明会被提升,所以输出hello world。sayAge(10),报错是因为var sayAge = function(age){}是一个函数表达式,声明var sayAge时,还不是一个函数, sayAge(10) 调用在声明前,所以报错。

如下代码输出什么? 写出作用域链查找过程伪代码

var x = 10
bar() 
function foo() {
  console.log(x)
}
function bar(){
  var x = 30
  foo()
}           //  输出 10

1. globalContext = {
    AO: {
       x: 10
       foo: funciton
       bar: funciton
    }
    Scope: null
}

//声明 foo() 时 得到
foo.[ [scope] ] = globalContext.AO
//声明 bar() 时 得到
bar.[ [scope] ] = globalContext.AO
// 当调用bar(),进入bar 的执行上下文
2. barContext = {
    AO: {
      x: 30
    }
   Scope: bar.[ [scope] ]
}
//当调用 foo() 时,先从 bar 执行上下文中的 AO里找,找不到再从 bar 的 [[scope]]里,找到后即调用
  fooContext = {
    AO: {
       
    } 
   Scope: foo.[ [scope] ]
}

所以console.log(x) 得到10

如下代码输出什么? 写出作用域链查找过程伪代码

var x = 10;
bar() 
function bar(){
  var x = 30;
  function foo(){
    console.log(x) 
  }
  foo();
}

以下代码输出什么? 写出作用域链的查找过程伪代码

var x = 10;
bar() 
function bar(){
  var x = 30;
  (function (){
    console.log(x)
  })()
}

1. globalContext = {
   AO: {
      x: 10
      bar: funciton
   }
   Scope: null
}

bar.[ [scope] ] = globalContext.AO

2. barContext = {
    AO: {
      x: 30
    }
   Scope: bar.[ [scope] ]
 }

声明x,x赋值10 > 执行bar() > 声明x,x赋值30 > 立即执行匿名函数 > console.log(x) > 到bar上下文AO中找到x = 30 ,所以console.log(x) 得到30
 
// 



以下代码输出什么? 写出作用域链查找过程伪代码

var a = 1;

function fn(){
  console.log(a)
  var a = 5
  console.log(a)
  a++
  var a
  fn3()
  fn2()
  console.log(a)

  function fn2(){
    console.log(a)
    a = 20
  }
}

function fn3(){
  console.log(a)
  a = 200
}

fn()
console.log(a)

1. globalContext = {
    AO: {
       a: 1
       fn: funciton
       fn3: funciton
    }
   Scope:null
}
// fn.[ [scope] ] = globalContext.AO   
// fn3.[ [scope] ] = globalContext.AO

2. fnContext = {
    AO: {
       a: undefine
       fn3: funciton
       fn2: funciton
    }
   Scope: fn.[ [scope] ]    // fn.[ [scope] ] = globalContext,AO
}
}

fn2Context {
     AO: {
        
     } 
    Scope: fn2.[ [scope] ]    // fn2.[ [scope] ] = fnContext,AO
}

fn3Context {
     AO: {
     }
    Scope: fn3.[ [scope] ]    // fn3.[ [scope] ] = globalContext,AO
}

执行fn() > console.log(a) 此时 a = undefined;  > console.log(a) 此时 a = 5 > a++ 此时 a = 6 > 执行fn3() , console.log(a) 找到globalContext.AO里 a = 1;  a = 200 此时globalContext.AO里 a = 200   >  执行fn2() , console.log(a) 找到fnContext.AO里 a = 6, 然后a = 20 此时 fnContext.AO里 a = 20;
> 执行 console.log(a) 此时 a = 20    >   最后 执行 console.log(a)  得到 a = 200

结果  undefined > 5 > 1 > 6 > 20 > 200

推荐阅读更多精彩内容

  • 函数声明和函数表达式有什么区别? 函数声明和函数表达式是EMACScript规定的两种不同的声明函数的方法。1.函...
    LeeoZz阅读 70评论 0 1
  • JavaScript中的函数运行在它们被定义的作用域里,而不是它们被执行的作用域里。 函数声明和函数表达式有什么区...
    毕子歌阅读 37评论 0 0
  • 1.函数声明和函数表达式有什么区别 函数声明可以看作是函数的初始化,我们将给函数传参并建立函数体的表达式,当我门建...
    高进哥哥阅读 23评论 0 0
  • 1.函数声明和函数表达式有什么区别 函数声明 代码执行时函数声明会被提升到最前执行,所以函数的调用与函数声明的顺序...
    Feiyu_有猫病阅读 70评论 0 0
  • 去感觉而不是想象,去描述而不是评价。 很多时候,我们所看到的事情,只能看到我们想看到的部分,因为在观察时加入了太多...
    无间行者lee阅读 22评论 0 1