函数与作用域

函数声明与函数表达式

参考 ECMA 标准规范

  • 函数声明

      function a(){
        return 1;
      }
    

    一条函数声明语句实际上声明了一个变量,并把一个函数对象赋值给它。

  • 函数表达式

      var a = function(){
        return 1;
      }
    

    定义函数表达式时并没有声明一个变量。同时,函数可以命名,如果一个函数表达式包含名称,函数的局部作用域将会包含一个绑定到函数对象的名称。实际上,函数的名称将成为函数内部的一个局部变量。

    尽管函数声明语句和函数定义表达式包含相同的函数名,但二者仍然不同。两种方式都创建了新的函数对象,但函数声明语句中的函数名是一个变量名,变量指向函数对象。

    • 函数定义表达式 使用var,只有变量声明提前了——变量的初始化代码(将函数赋值给变量的操作)仍然在原来的位置
    • 函数声明语句 函数名称和函数体均提前:脚本中的所有函数和函数中所有嵌套的函数都会在当前上下文中其他代码之前声明,因此,可以在声明一个JavaScript函数之前调用它
    函数声明 函数表达式
    声明不必放在调用前 声明必须放在调用前

    tip: 和 var 语句一样,函数声明语句创建的变量也是无法删除的,但这些变量不是只读的,变量值可以重写


声明前置

变量在声明前是可用的,这一特性被非正式的称为声明提前(hoisting),即在一个作用域下,var 声明的变量和 function 声明的函数会前置,声明都会提升到当前作用域的顶部
需要注意的是

  • 函数定义表达式 基本同 var 声明变量,变量会提升,函数赋值给变量不会提升
  • 函数声明语句,函数名和函数体均会提升至当前作用域的顶部
  • 函数里声明的所有变量(但不涉及赋值)都被“提升”至函数体的顶部

arguments

在函数体内,标识符 arguments 是指向实参对象的引用,实参对象是一个类数组对象,可以像操作数组一样操作 arguments

实参对象有一个重要的用处,让函数可以操作任意数量的实参

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); // => 10000

重载

重载是指不同的函数使用相同的函数名,但是函数的参数个数或类型不同。调用的时候根据函数的参数来区别不同的函数。

JS中函数没有重载!同名函数会覆盖。但可以在函数体针对不同的参数调用执行相应的逻辑

function printInfo(name, age, sex){
  if(name){
    console.log(name);
  }
  if(age){
    console.log(age);
  }
  if(sex){
    console.log(sex)
  }
}
printInfo('tom', 23);
// tom
// 23
printInfo('Luk', 34, 'male');
// Luk
// 34
// male

tips: 犀牛书P229 构造函数的重载和工厂方法,本质上重写构造函数,使得函数能够根据传入参数的不同来执行不同的初始化方法


立即执行函数表达式

翻译参考
原文链接
无论是函数声明语句声明的具名函数还是函数定义表达式,调用函数只需在函数名接括号(表达式则是在变量名后接括号),但如果是匿名函数呢?

(function(){/* code */}());
(function(){})();

[function fn() {}];

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

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

以上写法目的都在于将声明的语句“变为”表达式,再执行
立即执行函数表达式还有个作用:隔离作用域


递归实现阶乘

function factorial(n){
  if (typeof n !== "number") throw new TypeError("请输入数字!");
  if (n == 0) {
    return 0;
  }
  if (n == 1) {
    return 1;
  }
  if (n > 1) {
    return n*factorial(n-1);
  }
}
factorial(0); // => 0
factorial(1); // => 1
factorial(5); // => 120

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, "男", callee: ƒ, Symbol(Symbol.iterator): ƒ]
// => name valley

getInfo('小谷', 3);
// => name: 小谷
// => age: 3
// => sex: undefined
// => ["小谷", 3, callee: ƒ, Symbol(Symbol.iterator): ƒ]
// => name valley

getInfo('男');
// => name: 男
// => age: undefined
// => sex: undefined
// => ["男", callee: ƒ, Symbol(Symbol.iterator): ƒ]
// => name valley

arguments 并不是真正的数组,它是一个实参对象。每个实参对象都包含以数字为索引的一组元素以及 length 属性,但它毕竟不是真正的数组。它是一个对象,具有以数字为索引的属性


返回参数的平方和

function sumOfSquares(){
  var _argu = Array.prototype.slice.call(arguments).map(function(x){
    return x*x;
  });
  var s=0;
  for(var i =  0;i  <  _argu.length;  i++){
    s+=_argu[i];
  }
  return s;
}
var result = sumOfSquares(2, 3, 4);
var result2 = sumOfSquares(1, 3);
console.log(result); // => 29
console.log(result2); // => 10

console.log(a);
var a = 1;
console.log(b);
// 相当于
var a;
console.log(a); // => undefined
a = 1;
console.log(b); // => Uncaught ReferenceError: b is not defined 因为 b 未声明

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

// 结果为
sayName('world'); // => hello world   函数声明语句声明提升
sayAge(10); // => Uncaught TypeError: sayAge is not a function  函数定义表达式,变量会提升,赋值操作不提升

拓展1

fn();
var i = 10;
var fn = 20;
console.log(i);
function fn(){
    console.log(i);
    var i = 99;
    fn2();
    console.log(i);
    function fn2(){
        i = 100;
    }
}

分析如下

// var i =100;
function fn(){
    function fn2(){
        i = 100; // => fn2执行时,会声明了一个全局变量
    }
    var i;
    console.log(i); // undefined
    i = 99;
    fn2(); // 执行时,声明了一个全局变量i,此时,覆盖掉之前的 99
    console.log(i) // 100
}
var i,fn;
fn(); // 此时fn执行

i = 10;
fn = 20;
console.log(i); // 10

所以控制台会打印如下结果

undefined
100
10

拓展2

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

分析如下

// 依据声明提升的规则,将代码重新放置后
function bar(){
    var x = 30;
        function foo(){
        console.log(x); // 打印 30
    } 
    foo();
}
var x;
x = 10;
bar(); // bar 执行

所以控制台会打印结果

30

作用域链查找过程伪代码


训练一

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

分析如下

/*
globalContext = {
    AO: {
        x: 10
        foo: function(){}
        bar: function(){}
    },
    Scope: null
}

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

// 调用 bar() 时,进入 bar 的执行上下文
barContext = {
    AO: {
        x: 30
    },
    Scope: bar.[[scope]] // globalContext.AO
}

// 调用 foo 时,先从 bar 执行上下文中的 AO 里找,
找不到再从 bar 的 [[scope]] 里找,找到后即调用

*/

训练二

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

分析

/*

globalContext = {
    AO: {
        x: 10
        bar: function(){}
    },
    Scope: null
}

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

// 调用 bar 时 进入 bar 的上下文
barContext = {
    AO: {
        x: 30
        foo: function(){}
    },
    Scope: bar.[[scope]] // globalContext.AO
}

// 声明 foo 时 得到
foo.[[scope]] = barContext.AO

// 调用 foo 时 进入 foo 的上下文
fooContext = {
    AO: {},
    Scope: foo.[[scope]] // barContext.AO
}

// 调用 foo 时,先从 bar 执行上下文的 AO 里找,
// 找不到再从 bar 的 [[scope]] 里(即全局作用域)找,找到后即调用

// 因此 会打印 30

*/

训练三

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

分析

/*

globalContext = {
    AO: {
        x: 30
        bar: function(){}
    },
    Scope: null
}

// 定义 bar
bar.[[scope]] = globalContext.AO 

// 调用 bar 进入 bar 上下文
barContext = {
    AO: {
        x: 30
        IIFE: function(){}
    }
    Scope: bar.[[scope]]
}
// IIFE 定义时
IIFE.[[scope]] = barContext.AO

// IIFE 自执行时 进入 IIFE 上下文
IIFEContext = {
    AO:{}
    Scope: IIFE.[[scope]] // barContext.AO
}

//---↓↓↓↓↓这是错的↓↓↓↓↓↓---------//
//------------------------------//
// 由于立即执行函数表达式会隔绝作用域 //
// 匿名函数定义时未定义 变量x       //
// 匿名函数自执行时未传入参数       //
// 所以 x 为 未定义               //
// 即 x is not define           //
//------------------------------//
//-----↑↑↑↑↑这是错的↑↑↑↑↑↑-------//
//--以上为第一次分析时错误之处,留存以供复习查阅----//

// 正式分析
// IIFE 自执行时,会向 barContext.AO 查找 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)

分析如下

/*

globalContext = {
    AO: {
        a: 1 // 1 200
        fn: function(){}
        fn3: function(){}
    },
    Scope: null
}

fn.[[scope]] = globalContext.AO
fn3.[[scope]] = globalContext.AO

// 调用 fn 时 进入 fn 的上下文
fnContext = {
    AO: {
        a: 6 // undefined 5 6 20
        fn2: function(){}
    },
    Scope: fn.[[scope]]
}

// 声明 fn2
fn2.[[scope]] = fnContext.AO


// 调用 fn3 进入 fn3 上下文
fn3Context = {
    AO: {
        
    },
    Scope: fn3.[[scope]] // globalContext
}

// 调用 fn2 进入 fn2 上下文
fn2Context = {
    AO: {
        
    },
    Scope: fn2.[[scope]] // fnContext.AO
}


// 所以打印顺序是
// undefined
// 5
// 1
// 6
// 20
// 200
*/

你可能感兴趣的:(函数与作用域)