JS笔记《数据类型—函数》

97 阅读7分钟

定义函数的方式

  • 函数是一个引用值,堆存函数体,栈存内存地址
  • 如果同一个函数被多次声明,后面的声明会覆盖前面的声明。

函数声明

function test(){    
    // 函数体  
}

test  // 调用

函数表达式

// 匿名函数表达式
var test = function (){      
    // 函数体  
}  

// 命名函数表达式
var test = function abc(){      
    // 函数体  
    console.log(typeof abc);  // 'function'
}  
abc();    // Uncaught ReferenceError: abc is not defined

命名函数表达式,函数名只在该函数体内有效,外部无法访问。

Function 构造函数(了解)

var test = new Function(
  'a', 
  'b', 
  'return a + b'
);
test(1, 2) // 3 调用且传参

// 等同于
function test(a, b){
  return a + b;
}

参数

function x(a, b){  // a b 形参
 return a + b;
}

x(1, 2)  // 3   // 1 2 实参

x(1)     // NaN  参数b没传,默认为undefined。  1 + undefined == NaN

传递方式

  • 函数参数如果是原始值则传递方式是值传递,这表示在函数体内修改参数值不会影响到调用时传入的原始数据。
var a = 1;
function x(a){ 
  a = 2;
}
x(a);
a // 1 函数体内部修改不会影响外部
  • 函数参数如果是引用值(数组、对象、函数),则传递方式是引用传递,这表示在函数体内修改参数将会影响调用时传入的原始数据。
var o = {a: 1};

function x(obj){
  obj.a = 2;
}

x(o);

o  // {a: 2}  函数体内部修改影响了外部原始数据
  • 接上,如果函数内部修改的不是引用值参数的某个属性,而是整个参数,则不会影响外部的原始数据。
var o = {a: 1};

function x(obj){
  obj = {a: 2};
}

x(o);

o  // {a: 1}  因为形参obj的值是o的引用地址,重新给obj赋值就导致obj指向了另一个引用地址,
   //         所以原始数据的o当然不受影响

arguments对象

  • arguments对象是实参类数组,包含了函数运行时的所有参数,只有在函数体内部才可以使用。
function f(a, b){
  console.log(arguments)
}
f(1, 2)    // 类数组:[1, 2, callee: ƒ, Symbol(Symbol.iterator): ƒ]
f(1, 2, 3) // 类数组:[1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ]

与形参的映射关系

  • arguments会与形参一 一形成映射关系,一改全改。未形成映射关系的修改其中一个不会影响另一个
function test(a, b, c) {  
    arguments[0] = 10;       
    console.log(a);    // 10   
    // 修改 arguments[i]的数值,则对应的形参也会跟着变化(前提是实参与形参形成了映射)  
      
    b = 20;  
    console.log(arguments[1]);    // 20   
    // 修改形参数值,对应的arguments[i]也变(前提是实参与形参形成了映射)  
      
    c = 30;  
    console.log(arguments[2]);    // undefined  
    // 由于实参是两个,形参3个,第三个形参没有与实参形成映射关系,所以为 undefined  
}  
test(12);
  • 但是在严格模式下arguments与形参不会形成映射关系,修改其中一个不会影响另一个。
function test(a, b, c) {  
  'use strict';
  arguments[0] = 10;       
  console.log(a);    // 1   
}  
test(1, 2);

callee

  • 返回它所对应的原函数,适用于调用没有函数名的函数。严格模式禁用
function test() {  
    console.log(arguments.callee);          // ƒ test() {console.log(arguments.callee);}  
    console.log(test == arguments.callee);  // true 指向拥有自己的函数  
}  
test();  
  
// 适用场景:立即执行函数无法再次调用,使用 arguments.callee  
var num = (function(n) {  
    if (n == 1) {  
        return 1;  
    }  
    return n * arguments.callee(n - 1);  
}(5));  
console.log(num);   // 120

返回值 return

  • return既能返回值,又能终止代码执行。
  • 函数中如果未指定返回值,则默认返回undefined
function test(){
    return "abc";   
}
var str = test();   // 调用函数,接收返回值
str                 // "abc"

函数名的提升

  • JS将函数名视为变量名,所以采用函数声明定义的函数会像变量声明一样,被提升到代码头部。
f(); // 不会报错
function f() {}  // 函数声明定义的函数

a();  // Uncaught TypeError: a is not a function
var a = function(){}
// 该函数是以函数表达式定义的,所以整个函数不会被提升。
// 但是变量 a 的声明会被提升,值为 undefined,所以以函数的方式调用会报错。
// 等价于:
var a;
a();
a = function(){}

函数属性及方法

name

  • 返回函数的名字。
function f(){}
f.name // 'f'

var f1 = function(){} 
f1.name // 'f1'  匿名函数表达式返回的是变量名(也可以叫做函数名)

var f2 = function ff(){}
f2.name  // 'ff' 命名函数表达式返回的是 function 后定义的函数名,但 'ff' 只在函数体中才可以使用

caller

  • 获得调用当前函数的函数(window或全局调用指向 null)。
function a(){
  console.log(a.caller)
} 

function b(fn){
  fn()
}
b(a)  // b(fn){fn()}  打印的是b函数,因为函数a是在b函数中调用的
a()   // null  a函数是全局调用的

length

  • 返回函数形参个数。
function test(a,b,c){

}
test.length // 3 无论调用时的实参是几个,永远打印形参的个数

toString()

  • 返回函数源码字符串。
  • 对于原生函数返回function (){[native code]}
function f() {
  console.log(1);
}
f.toString();  // 'function f() {\n  console.log(1);\n}'

函数作用域

  • 作用域指的是变量存在的范围,在ES5中只有全局作用域函数作用域,ES6新增了块级作用域。
  • 对于顶层函数来讲,函数外声明的变量就是全局变量,函数内可以读取。但在函数内声明的变量是局部变量。外部无法读取。

函数内部变量声明提升

  • 与全局作用域一样,函数作用域内部也会产生变量提升现象,使用var命令的变量声明会被提升到函数体的头部。
function foo(x) {
  if (x > 100) {
    var tmp = x - 100;
  }
}

// 等同于:
function foo(x){
  var tmp;   // tmp的变量声明被提升到了函数体的头部
  if(x > 100){
    tmp = x - 100;
  }
}

函数本身作用域

  • 函数执行时的作用域是定义时的作用域,而不是执行时所在的作用域。
var a = 1;   // 全局变量a
var x = function(){
    console.log(a);
}

function f(){
    var a = 2; // 局部变量a
    x();
}
f();  // 1  打印的是全局变量a,即使x是在f中被调用。
      // 因为x函数是在全局定义的,作用域绑定是全局,所以输出a就是全局变量的a


// 改动版:
var x = function () {  
  console.log(a);  
};

function y(f) {
  var a = 2;
  f();
}

y(x)  // ReferenceError: a is not defined
      // x是在全局作用域定义的,没有全局变量a,所以报错
  • 函数体内部定义的函数,该函数的作用域是整个函数体内部(闭包)
function x(){
  var a = 1;  // 局部变量a

  function y(){
    console.log(a);  
  }
  return y;
}

var a = 2;  // 全局变量a
var f = x();
f();  // 1  y 函数的作用域在定义时绑定的是 x 函数体,
      // 即使保存到全局被调用依然指向的是局部变量 a

立即执行函数 IIFE

  • 普通的函数声明如果不执行会一直占用内存空间,等待被调用(执行上下文里存在)。而立即执行函数没有声明,在一次执行过后立即释放。适合做初始化工作、插件。

  • 立即执行函数另一大用处就是避免全局污染,写在立即执行函数中的变量与函数无法被外部访问。

语法

  • JS中,() 跟在函数后表示调用该函数,但是函数声明后不能加上 (),这会产生语法错误。
function test(){}();  // 报错 Uncaught SyntaxError: Unexpected token ')'
// 报错的原因是 function 关键字被当成了语句,而语句是不可以被执行的

function f(){}  // 语句

var f = function f(){}  // 表达式

var f = function x(){   // 1 表达式可以被执行
    console.log(1);
}()  

只有表达式可以被 () 执行。

  • JS规定,如果function关键字出现在行首一律被解释为语句,JS引擎会认为这一段都是函数的定义,不应该以 ()结尾,所以报错。解决办法是不要让function出现在行首,让JS引擎将其理解成为一个表达式。
// 标准写法:
var num = (function(a, b){    // 形参  
    return a + b;             // 可以有返回值,定义变量 num接收。  
}(12));                     // 结尾的 ; 必备,如果省略连续遇到两个IIFE可能会报错
num;  // 3

// 其他写法:
true && function test(){  
    console.log(123);  
}();  // 可以执行
test  // test is not defined   函数表达式会自动忽略函数名

用正号(+)、负号(-)、()、!、||、&&等,只要可以将函数声明转为函数表达式的符号都可以执行。