js你不是的那些基础问题-函数

1 概述

1.1 函数的声明

  JavaScript 有三种声明函数的方法。

  (1)function 命令

function命令声明的代码区块,就是一个函数。function命令后面是函数名,

函数名后面是一对圆括号,里面是传入函数的参数。函数体放在大括号里面。

function print(s) {
  console.log(s);
}

  上面的代码命名了一个print函数,以后使用print()这种形式,

  就可以调用相应的代码。这叫做函数的声明(Function Declaration)。

  (2)函数表达式

  除了用function命令声明函数,还可以采用变量赋值的写法。

var print = function(s) {
  console.log(s);
};

  这种写法将一个匿名函数赋值给变量。这时,这个匿名函数又称函数表达式(Function Expression),

  因为赋值语句的等号右侧只能放表达式。

  采用函数表达式声明函数时,function命令后面不带有函数名。

  如果加上函数名,该函数名只在函数体内部有效,在函数体外部无效。

var print = function x(){
  console.log(typeof x);
};

x
// ReferenceError: x is not defined

print()
// function

  上面代码在函数表达式中,加入了函数名x。这个x只在函数体内部可用,指代函数表达式本身,

  其他地方都不可用。这种写法的用处有两个,一是可以在函数体内部调用自身,

  二是方便除错(除错工具显示函数调用栈时,将显示函数名,而不再显示这里是一个匿名函数)。

  因此,下面的形式声明函数也非常常见。

var f = function f() {};

  需要注意的是,函数的表达式需要在语句的结尾加上分号,表示语句结束。

  而函数的声明在结尾的大括号后面不用加分号。

  总的来说,这两种声明函数的方式,差别很细微,可以近似认为是等价的。

 

  (3)Function 构造函数

  第三种声明函数的方式是Function构造函数。

var add = new Function(
  'x',
  'y',
  'return x + y'
);

// 等同于
function add(x, y) {
  return x + y;
}

  上面代码中,Function构造函数接受三个参数,除了最后一个参数是add函数的“函数体”,

  其他参数都是add函数的参数。

  你可以传递任意数量的参数给Function构造函数,只有最后一个参数会被当做函数体,

  如果只有一个参数,该参数就是函数体。

var foo = new Function(
  'return "hello world";'
);

// 等同于
function foo() {
  return 'hello world';
} 

  Function构造函数可以不使用new命令,返回结果完全一样。

  总的来说,这种声明函数的方式非常不直观,几乎无人使用。

 1.2 第一等公民

JavaScript 语言将函数看作一种值,与其它值(数值、字符串、布尔值等等)地位相同。

凡是可以使用值的地方,就能使用函数。比如,可以把函数赋值给变量和对象的属性,

也可以当作参数传入其他函数,或者作为函数的结果返回。

函数只是一个可以执行的值,此外并无特殊之处。

1.3 函数名的提升

   JavaScript 引擎将函数名视同变量名,所以采用function命令声明函数时,

  整个函数会像变量声明一样,被提升到代码头部。所以,下面的代码不会报错。

f();

function f() {}

  表面上,上面代码好像在声明之前就调用了函数f。但是实际上,由于“变量提升”,

  函数f被提升到了代码头部,也就是在调用之前已经声明了。

  但是,如果采用赋值语句定义函数,JavaScript 就会报错。

f();
var f = function (){};
// TypeError: undefined is not a function

  上面的代码等同于下面的形式。

var f;
f();
f = function () {};

  上面代码第二行,调用f的时候,f只是被声明了,还没有被赋值,

  等于undefined,所以会报错。因此,如果同时采用function命令和赋值语句声明同一个函数,

  最后总是采用赋值语句的定义

var f = function () {
  console.log('1');
}

function f() {
  console.log('2');
}

f() // 1

2 函数的属性和方法

2.1 name 属性

   函数的name属性返回函数的名字。

function f1() {}
f1.name // "f1"

  如果是通过变量赋值定义的函数,那么name属性返回变量名。

var f2 = function () {};
f2.name // "f2"

  但是,上面这种情况,只有在变量的值是一个匿名函数时才是如此。

  如果变量的值是一个具名函数,那么name属性返回function关键字之后的那个函数名。

var f3 = function myName() {};
f3.name // 'myName'

  上面代码中,f3.name返回函数表达式的名字。注意,真正的函数名还是f3

  而myName这个名字只在函数体内部可用。

  name属性的一个用处,就是获取参数函数的名字。

var myFunc = function () {};

function test(f) {
  console.log(f.name);
}

test(myFunc) // myFunc

  上面代码中,函数test内部通过name属性,就可以知道传入的参数是什么函数。

2.2 length 属性

  函数的length属性返回函数预期传入的参数个数,即函数定义之中的参数个数。

function f(a, b) {}
f.length // 2

  上面代码定义了空函数f,它的length属性就是定义时的参数个数。

  不管调用时输入了多少个参数,length属性始终等于2。

  length属性提供了一种机制,判断定义时和调用时参数的差异,

  以便实现面向对象编程的“方法重载”(overload)。

2.3 toString()

  函数的toString方法返回一个字符串,内容是函数的源码。

function f() {
  a();
  b();
  c();
}

f.toString()
// function f() {
//  a();
//  b();
//  c();
// }

  对于那些原生的函数,toString()方法返回function (){[native code]}

Math.sqrt.toString()
// "function sqrt() { [native code] }"

  上面代码中,Math.sqrt是 JavaScript 引擎提供的原生函数,toString()方法就返回原生代码的提示。

  函数内部的注释也可以返回。

function f() {/*
  这是一个
  多行注释
*/}

f.toString()
// "function f(){/*
//   这是一个
//   多行注释
// */}"

  利用这一点,可以变相实现多行字符串。

var multiline = function (fn) {
  var arr = fn.toString().split('\n');
  return arr.slice(1, arr.length - 1).join('\n');
};

function f() {/*
  这是一个
  多行注释
*/}

multiline(f);
// " 这是一个
//   多行注释"

3 函数作用域

3.1 函数本身的作用域

  函数本身也是一个值,也有自己的作用域。它的作用域与变量一样,

  就是其声明时所在的作用域,与其运行时所在的作用域无关。

 

var a = 1;
var x = function () {
  console.log(a);
};

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

f() // 1

 

  上面代码中,函数x是在函数f的外部声明的,所以它的作用域绑定外层,

  内部变量a不会到函数f体内取值,所以输出1,而不是2

  总之,函数执行时所在的作用域,是定义时的作用域,而不是调用时所在的作用域。

  很容易犯错的一点是,如果函数A调用函数B,却没考虑到函数B不会引用函数A的内部变量。

var x = function () {
  console.log(a);
};

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

y(x)
// ReferenceError: a is not defined

  同样的,函数体内部声明的函数,作用域绑定函数体内部。

function foo() {
  var x = 1;
  function bar() {
    console.log(x);
  }
  return bar;
}

var x = 2;
var f = foo();
f() // 1

  上面代码中,函数foo内部声明了一个函数barbar的作用域绑定foo

  当我们在foo外部取出bar执行时,变量x指向的是foo内部的x,而不是foo外部的x

  正是这种机制,构成了下文要讲解的“闭包”现象。

4. 参数

 

4.1 传递方式

函数参数如果是原始类型的值(数值、字符串、布尔值),传递方式是传值传递(passes by value)。

这意味着,在函数体内修改参数值,不会影响到函数外部。

var p = 2;

function f(p) {
  p = 3;
}
f(p);

p // 2

  但是,如果函数参数是复合类型的值(数组、对象、其他函数),

  传递方式是传址传递(pass by reference)。

  也就是说,传入函数的原始值的地址,因此在函数内部修改参数,将会影响到原始值。

var obj = { p: 1 };

function f(o) {
  o.p = 2;
}
f(obj);

obj.p // 2

  注意,如果函数内部修改的,不是参数对象的某个属性,而是替换掉整个参数,这时不会影响到原始值。

var obj = [1, 2, 3];

function f(o) {
  o = [2, 3, 4];
}
f(obj);

obj // [1, 2, 3]

4.2 arguments 对象

  由于 JavaScript 允许函数有不定数目的参数,所以需要一种机制,

  可以在函数体内部读取所有参数。这就是arguments对象的由来。

  正常模式下,arguments对象可以在运行时修改。

var f = function(a, b) {
  arguments[0] = 3;
  arguments[1] = 2;
  return a + b;
}

f(1, 1) // 5

  严格模式下,arguments对象与函数参数不具有联动关系。

  也就是说,修改arguments对象不会影响到实际的函数参数。

var f = function(a, b) {
  'use strict'; // 开启严格模式
  arguments[0] = 3;
  arguments[1] = 2;
  return a + b;
}

f(1, 1) // 2

  callee 属性

  arguments对象带有一个callee属性,返回它所对应的原函数。

var f = function () {
  console.log(arguments.callee === f);
}

f() // true

  可以通过arguments.callee,达到调用函数自身的目的。这个属性在严格模式里面是禁用的,因此不建议使用。

函数的其他知识点

5.1 闭包

function f1() {
  var n = 999;
  function f2() {
  console.log(n); // 999
  }
}

  上面代码中,函数f2就在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。

  但是反过来就不行,f2内部的局部变量,对f1就是不可见的。

  这就是 JavaScript 语言特有的"链式作用域"结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。

  所以,父对象的所有变量,对子对象都是可见的,反之则不成立。

  既然f2可以读取f1的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了吗!

function f1() {
  var n = 999;
  function f2() {
    console.log(n);
  }
  return f2;
}

var result = f1();
result(); // 999

  闭包就是函数f2,即能够读取其他函数内部变量的函数。由于在 JavaScript 语言中,

  只有函数内部的子函数才能读取内部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。

  闭包最大的特点,就是它可以“记住”诞生的环境,比如f2记住了它诞生的环境f1

  所以从f2可以得到f1的内部变量。在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁

 

  闭包的最大用处有两个,一个是可以读取函数内部的变量,

  另一个就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。

  请看下面的例子,闭包使得内部变量记住上一次调用时的运算结果。

function createIncrementor(start) {
  return function () {
    return start++;
  };
}

var inc = createIncrementor(5);

inc() // 5
inc() // 6
inc() // 7

  上面代码中,start是函数createIncrementor的内部变量。

  通过闭包,start的状态被保留了,每一次调用都是在上一次调用的基础上进行计算。

  从中可以看到,闭包inc使得函数createIncrementor的内部环境,一直存在。

  所以,闭包可以看作是函数内部作用域的一个接口。

 

  为什么会这样呢?原因就在于inc始终在内存中,而inc的存在依赖于createIncrementor

  因此也始终在内存中,不会在调用结束后,被垃圾回收机制回收。

 

  闭包的另一个用处,是封装对象的私有属性和私有方法。

function Person(name) {
  var _age;
  function setAge(n) {
    _age = n;
  }
  function getAge() {
    return _age;
  }

  return {
    name: name,
    getAge: getAge,
    setAge: setAge
  };
}

var p1 = Person('张三');
p1.setAge(25);
p1.getAge() // 25

  注意,外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,

  所以内存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题。

5.2 立即调用的函数表达式(IIFE)

  我们需要在定义函数之后,立即调用该函数。这时,

  你不能在函数的定义之后加上圆括号,这会产生语法错误。

function(){ /* code */ }();
// SyntaxError: Unexpected token (

  产生这个错误的原因是,function这个关键字即可以当作语句,也可以当作表达式。

   为了避免解析上的歧义,JavaScript 引擎规定,如果function关键字出现在行首,一律解释成语句。

  因此,JavaScript 引擎看到行首是function关键字之后,认为这一段都是函数的定义,

  不应该以圆括号结尾,所以就报错了

  解决方法就是不要让function出现在行首,让引擎将其理解成一个表达式。

  最简单的处理,就是将其放在一个圆括号里面。

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

  上面两种写法都是以圆括号开头,引擎就会认为后面跟的是一个表示式,

  而不是函数定义语句,所以就避免了错误。

  这就叫做“立即调用的函数表达式”(Immediately-Invoked Function Expression),简称 IIFE。

  注意,上面两种写法最后的分号都是必须的。如果省略分号,遇到连着两个 IIFE,可能就会报错。

 

  通常情况下,只对匿名函数使用这种“立即执行的函数表达式”。

  它的目的有两个:一是不必为函数命名,避免了污染全局变量;

  二是 IIFE 内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量。

// 写法一
var tmp = newData;
processData(tmp);
storeData(tmp);

// 写法二
(function () {
  var tmp = newData;
  processData(tmp);
  storeData(tmp);
}());

  上面代码中,写法二比写法一更好,因为完全避免了污染全局变量。

6 eval 命令

  eval命令接受一个字符串作为参数,并将这个字符串当作语句执行。

eval('var a = 1;');
a // 1

  如果参数字符串无法当作语句运行,那么就会报错。

eval('3x') // Uncaught SyntaxError: Invalid or unexpected token

  放在eval中的字符串,应该有独自存在的意义,不能用来与eval以外的命令配合使用。

  举例来说,下面的代码将会报错。

eval('return;'); // Uncaught SyntaxError: Illegal return statement

  如果eval的参数不是字符串,那么会原样返回。

eval(123) // 123

  eval没有自己的作用域,都在当前作用域内执行,

  因此可能会修改当前作用域的变量的值,造成安全问题。

var a = 1;
eval('a = 2');

a // 2

  上面代码中,eval命令修改了外部变量a的值。由于这个原因,eval有安全风险。

  为了防止这种风险,JavaScript 规定,如果使用严格模式,

  eval内部声明的变量,不会影响到外部作用域。

(function f() {
  'use strict';
  eval('var foo = 123');
  console.log(foo);  // ReferenceError: foo is not defined
})()

  总之,eval的本质是在当前作用域之中,注入代码。

  由于安全风险和不利于 JavaScript 引擎优化执行速度,所以一般不推荐使用。

  通常情况下,eval最常见的场合是解析 JSON 数据的字符串,

  不过正确的做法应该是使用原生的JSON.parse方法。

 

 

文章内容转自 阮一峰老师 JavaScript教程 https://wangdoc.com/javascript/index.html

posted @ 2019-08-13 13:02  WernerWu  阅读(168)  评论(0编辑  收藏  举报