共计 6800 个字符,预计需要花费 17 分钟才能阅读完成。
函数
函数是一段可以反复调用的代码块。函数还能接受输入的参数,不同的参数会返回不同的值。
1. 概述
1.1 函数的声明
1.2 函数的重复声明
1.3 圆括号运算符,return 语句和递归
1.4 第一等公民
1.5 函数名的提升
2 函数的属性和方法
2.1name 属性
2.2length 属性
2.3toString()
3 函数作用域
3.1 定义
3.2 函数内部的变量提升
3.3 函数本身的作用域
4 参数
4.1 概述
4.2 参数的省略
4.3 传递方式
4.4 同名参数
4.5arguments 对象
5. 函数的其他知识点
5.1 闭包
5.2 立即调用的函数表达式(IIFE)
6eval 命令
6.1 基本用法
6.2eval 的别名调用
函数
函数是一段可以反复调用的代码块。函数还能接受输入的参数,不同的参数会返回不同的值。
1. 概述
1.1 函数的声明
(1)function 命令
(2)函数表达式 变量赋值
var print = function(s) {}
function 命令后面不带有函数名。如果加上函数名,该函数名只在函数体内部有效,在函数体外部无效。
var print = function x(){
console.log(typeof x);
};
x
// ReferenceError: x is not defined
print()
// function
用处有两个,一是可以在函数体内部调用自身,二是方便除错(除错工具显示函数调用栈时,将显示函数名,而不再显示这里是一个匿名函数)。
(3)Function 构造函数
var add = new Function(
‘x’,
‘y’,
‘return x + y’
); 除了最后一个参数是 add 函数的“函数体”,其他参数都是 add 函数的参数
// 等同于
function add(x, y) {
return x + y;
}
1.2 函数的重复声明
由于函数名的提升
后面的声明就会覆盖前面的声明
1.3 圆括号运算符,return 语句和递归
圆括号用于调用函数 a()
遇到 return 后面句子都不执行,没有就返回 undefined
函数可以调用自身,这就是递归(recursion)。下面就是通过递归,计算斐波那契数列的代码。
function fib(num) {
if (num === 0) return 0;
if (num === 1) return 1;
return fib(num – 2) + fib(num – 1);
}
fib(6) // 8
1.4 第一等公民
函数只是一个可以执行的值
函数只是一个可以执行的值
function add(x, y) {
return x + y;
}
// 将函数赋值给一个变量
var operator = add;
// 将函数作为参数和返回值
function a(op){
return op;
}
a(add)(1, 1)
// 2
1.5 函数名的提升
视为变量名,所以采用 function 命令声明函数时,整个函数会像变量声明一样
表达式形式是匿名函数
f();
var f = function (){};
// TypeError: undefined is not a function
上面的代码等同于下面的形式。
var f;
f();
f = function () {};
上面代码第二行,调用 f 的时候,f 只是被声明了,还没有被赋值,等于 undefined,所以会报错。因此,如果同时采用 function 命令和赋值语句声明同一个函数,最后总是采用赋值语句的定义。因为两个都是声明,然后赋值 function 形式是声明 var 声明 最后赋值
var f = function () {
console.log(‘1’);
}
function f() {
console.log(‘2’);
}
f() // 1
2 函数的属性和方法
2.1name 属性
2.1.1function f1() {}
f1.name // “f1”
如果是通过变量赋值定义的函数,那么 name 属性返回变量名。
2.2.2var f2 = function () {};
f2.name // “f2”
如果变量的值是一个具名函数,那么 name 属性返回 function 关键字之后的那个函数名。
2.2.3var f3 = function myName() {};
f3.name // ‘myName’
f3.name 返回函数表达式的名字。注意,真正的函数名还是 f3,而 myName 这个名字只在函数体内部可用
2.2.4name 属性的一个用处,就是获取参数函数的名字。
var myFunc = function () {};
function test(f) {
console.log(f.name);
}
test(myFunc) // myFunc
2.2length 属性
函数定义之中的参数个数。
2.3toString()
返回一个字符串,内容是函数的源码。
Math.sqrt.toString()
// “function sqrt() { [native code] }
利用这一点,可以变相实现多行字符串。
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 定义
3.1.1 全局 一直存在在内存 到处可读取
3.1.2 函数作用域 变量只在函数种存在
对于 var 命令来说,局部变量只能在函数内部声明,在其他区块中声明,一律都是全局变量。
3.2 函数内部的变量提升
函数作用域内部也会产生“变量提升”现象。var 命令声明的变量,不管在什么位置,变量声明都会被提升到函数体的头部
3.3 函数本身的作用域
作用域只与声明的地方有关
函数本身也是一个值,也有自己的作用域。它的作用域与变量一样,就是其声明时所在的作用域,与其运行时所在的作用域无关
var a = 1;
var x = function () {
console.log(a);
};
function f() {
var a = 2;
x();
}
f() // 1
函数 x 是在函数 f 的外部声明的,所以它的作用域绑定外层,内部变量 a 不会到函数 f 体内取值,所以输出 1,而不是 2。
3.3.1 同样的,函数体内部声明的函数,作用域绑定函数体内部。
function foo() {
var x = 1;
function bar() {
console.log(x);
}
return bar;
}
var x = 2;
var f = foo();
f() // 1
上面代码中,函数 foo 内部声明了一个函数 bar,bar 的作用域绑定 foo。当我们在 foo 外部取出 bar 执行时,变量 x 指向的是 foo 内部的 x,而不是 foo 外部的 x。正是这种机制,构成了下文要讲解的“闭包”现象。
4 参数
4.1 概述
4.2 参数的省略
4.1.1 无论提供多少个参数(或者不提供参数),JavaScript 都不会报错。省略的参数的值就变为 undefined。需要注意的是,函数的 length 属性与实际传入的参数个数无关
4.1.2 没有办法只省略靠前的参数,而保留靠后的参数。如果一定要省略靠前的参数,只有显式传入 undefined。
function f(a, b) {
return a;
}
f(, 1) // SyntaxError: Unexpected token ,(…)
f(undefined, 1) // undefined
上面代码中,如果省略第一个参数,就会报错。
4.3 传递方式
4.3.1 函数参数如果是原始类型的值(数值、字符串、布尔值),传递方式是传值传递。在函数里面的是复制的另外一个值了
在函数体内修改参数值,不会影响到函数外部
var p = 2;
function f(p) {
p = 3;
}
f(p);
p // 2
上面代码中,变量 p 是一个原始类型的值,传入函数 f 的方式是传值传递。因此,在函数内部,p 的值是原始值的拷贝,无论怎么修改,都不会影响到原始值。
4.3.2 如果函数参数是复合类型的值(数组、对象、其他函数),传递方式是传址传递
传入函数的原始值的地址,因此在函数内部修改参数,将会影响到原始值。
var obj = {p: 1};
function f(o) {
o.p = 2;
}
f(obj);
obj.p // 2
上面代码中,传入函数 f 的是参数对象 obj 的地址。因此,在函数内部修改 obj 的属性 p,会影响到原始值。(外面和里面指向同一个地址)
4.3.3 注意,如果函数内部修改的,不是参数对象的某个属性,而是替换掉整个参数,这时不会影响到原始值。
var obj = [1, 2, 3];
function f(o) {
o = [2, 3, 4];
}
f(obj);
obj // [1, 2, 3]
形式参数(o)的值实际是参数 obj 的地址,重新对 o 赋值导致 o 指向另一个地址,保存在原地址上的值当然不受影响。(外面和里面指向不同地址了)
4.4 同名参数
如果有同名的参数,则取最后出现的那个值
function f(a, a) {
console.log(a);
}
f(1, 2) // 2
取值的时候,以后面的 a 为准,即使后面的 a 没有值或被省略,也是以其为准。
function f(a, a) {
console.log(a);
}
f(1) // undefined
function f(a, a) {
console.log(arguments[0]);
}
f(1) // 1
4.5arguments 对象
4.5.1 只能在函数体内用
正常可以修改 严格无效
有 length 属性
var f = function(a, b) {
arguments[0] = 3;
arguments[1] = 2;
return a + b;
}
f(1, 1) // 5
4.5.2 与数组的关系
数组专有的方法(比如 slice 和 forEach),不能在 arguments 对象上直接使用
两种常用的转换方法:slice 方法和逐一填入新数组。
var args = Array.prototype.slice.call(arguments);
// 或者
var args = [];
for (var i = 0; i < arguments.length; i++) {
args.push(arguments[i]);
}
4.5.3callee 属性
arguments 对象带有一个 callee 属性,返回它所对应的原函数。
var f = function () {
console.log(arguments.callee === f);
}
f() // true
可以通过 arguments.callee,达到调用函数自身的目的。这个属性在严格模式里面是禁用的,因此不建议使用。
5. 函数的其他知识点
5.1 闭包
定义在函数内的函数
用处 1. 读取函数内的变量
2. 让这些变量始终保存在内存中
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,因此也始终在内存中,不会在调用结束后,被垃圾回收机制回收。
因为 inc 是全局的,使得闭包可以一直存在同时访问内部变量
3 封装私有变量
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
上面代码中,函数 Person 的内部变量_age,通过闭包 getAge 和 setAge,变成了返回对象 p1 的私有变量
注意,外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题
5.2 立即调用的函数表达式(IIFE)
()是运算符 表达式
function(){ / code / }();
// SyntaxError: Unexpected token (
产生这个错误的原因是,function 这个关键字即可以当作语句,也可以当作表达式。
如果 function 关键字出现在行首,一律解释成语句。因此,JavaScript 引擎看到行首是 function 关键字之后,认为这一段都是函数的定义,不应该以圆括号结尾
// 语句
function f() {}
// 表达式
var f = function f() {}
5.2.1 最简单的处理,就是将其放在一个圆括号里面。
(function(){/ code / }());
// 或者
(function(){/ code / })();
上面两种写法都是以圆括号开头,引擎就会认为后面跟的是一个表示式,而不是函数定义语句,所以就避免了错误。这就叫做“立即调用的函数表达式
任何让解释器以表达式来处理函数定义的方法,都能产生同样的效果,比如下面三种写法。
var i = function(){ return 10;}();
true && function(){ / code / }();
0, function(){ / code / }();
甚至像下面这样写,也是可以的。
!function () { / code / }();
~function () { / code / }();
-function () { / code / }();
+function () { / code / }();
5.2.2 目的
1. 不必为函数命名,避免污染全局变量
2. 封装私有变量
// 写法一
var tmp = newData;
processData(tmp);
storeData(tmp);
// 写法二
(function () {
var tmp = newData;
processData(tmp);
storeData(tmp);
}());
上面代码中,写法二比写法一更好,因为完全避免了污染全局变量
6eval()命令
6.1 接受一个字符串当参数并当语录执行 否则报错
eval(‘var a = 1;’);
a // 1
eval(‘3x’) // Uncaught SyntaxError: Invalid or unexpected token
6.2
放在 eval 中的字符串,应该有独自存在的意义,不能用来与 eval 以外的命令配合使用。举例来说,下面的代码将会报错。
eval(‘return;’); // Uncaught SyntaxError: Illegal return statement
6.3 如果 eval 的参数不是字符串,那么会原样返回。
eval(123) // 123
6.4 作用域
var a = 1;
eval(‘a = 2’);
a // 2
上面代码中,eval 命令修改了外部变量 a 的值。由于这个原因,eval 有安全风险
如果使用严格模式,(自己声明的不会影响,上面的问题依然有)eval 内部声明的变量,不会影响到外部作用域。
(function f() {
‘use strict’;
eval(‘var foo = 123’);
console.log(foo); // ReferenceError: foo is not defined
})()
(function f() {
‘use strict’;
var foo = 1;
eval(‘foo = 2’);
console.log(foo); // 2
})()
eval 最常见的场合是解析 JSON 数据的字符串,不过正确的做法应该是使用原生的 JSON.parse 方法
6.1 基本用法
6.2eval 的别名调用
1.eval 不利于引擎优化执行速度
2. 引擎在静态代码分析的阶段,根本无法分辨执行
的是 eval。
var m = eval;
m(‘var x = 1’);
x // 1
上面代码中,变量 m 是 eval 的别名。静态代码分析阶段,引擎分辨不出 m(‘var x = 1’)执行的是 eval 命令
为了保证 eval 的别名不影响代码优化改进:用别名的话,里面都是全局变量
var a = 1;
function f() {
var a = 2;
var e = eval;
e(‘console.log(a)’);
}
f() // 1
上面代码中,eval 是别名调用,所以即使它是在函数中,它的作用域还是全局作用域,因此输出的 a 为全局变量。这样的话,引擎就能确认 e()不会对当前的函数作用域产生影响,优化的时候就可以把这一行排除掉。
eval 的别名调用的形式五花八门,只要不是直接调用,都属于别名调用,因为引擎只能分辨 eval()这一种形式是直接调用。
eval.call(null, ‘…’)
window.eval(‘…’)
(1, eval)(‘…’)
(eval, eval)(‘…’)
上面这些形式都是 eval 的别名调用,作用域都是全局作用域。