函数与作用域

函数声明与函数表达式

参考 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
*/
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • 1.函数声明和函数表达式有什么区别 函数就是一段可以反复调用的代码块。函数还能接受输入的参数,不同的参数会返回不同...
    徐国军_plus阅读 472评论 0 0
  • 函数声明和函数表达式有什么区别 函数声明语法:function functionName(arg0,arg1,ar...
    _Dot912阅读 564评论 0 3
  • 一、函数声明和函数表达式有什么区别 区别:用函数声明创建的函数可以在定义之前就进行调用;而用函数表达式创建的函数不...
    任少鹏阅读 188评论 1 2
  • 1,函数声明和函数表达式有什么区别 1、背景介绍 定义函数的方法主要有三种: 1:函数声明(Function De...
    进击的前端_风笑影阅读 426评论 0 0
  • 人性之恶 有些人 慷慨仁慈 心胸坦荡 忠厚老实 处处为人民着想 最后却一败涂地 有些人 狡猾敏锐 贪得无厌 自私自...
    欧阳小川阅读 258评论 9 14