function
命令后面是函数名,函数名后面是一对圆括号,里面是传入函数的参数。函数体放在大括号里面。function print(s) { console.log(s); }
function
命令后面通常不带有函数名。若是加上函数名,该函数名只在函数体内部有效,在函数体外部无效。var print = function x(){ console.log(typeof x); }; x // ReferenceError: x is not defined print() // function
可是,咱们在函数表达式中,每每会加入这个函数名x。虽然这个x
只在函数体内部可用,指代函数表达式自己,其余地方都不可用。可是这种写法有两个好处:(1)能够在函数体内部调用自身,方便递归;(2)方便除错(除错工具显示函数调用栈时,将显示函数名,而再也不显示这里是一个匿名函数)编程
若一个函数被屡次声明,后边的声明会覆盖前边的声明(这里要注意的是,因为函数名的提高,前一次声明在任什么时候候都是无效的,并非在第二次声明前还有效,而是所有覆盖)数组
function f() { console.log(1); } f() // 2 function f() { console.log(2); } f() // 2
因为函数与其它数据类型地位平等,因此js中又称函数为第一等公民。安全
function add(x, y) { return x + y; } // 将函数赋值给一个变量 var operator = add; // 将函数做为参数和返回值 function a(op){ return op; } a(add)(1, 1) // 2
因为js引擎将函数名视同变量名,因此也遵循变量声明的原则,会被提高到代码头部闭包
所以,此代码不报错函数
f(); function f() {}
可是,若采用赋值语句定义函数,就会发生错误。这与变量名声明提早相似,只是将函数声明提早,而没有将函数的赋值提早工具
f(); var f = function (){}; // TypeError: undefined is not a function
var f; f(); f = function () {};
再看下这个例子性能
var f = function () { console.log('1'); } function f() { console.log('2'); } f() // 1
先var f,而后第二个函数声明覆盖了第一个函数声明,接着第一个函数的赋值覆盖了第二个函数的声明,最后仍是第一个函数的赋值优化
当变量赋值定义函数时,若为匿名函数,返回变量名;如有具体函数名,返回function后的函数名prototype
var f3 = function myName() {}; f3.name // 'myName'
name属性的重要做用,就是获取传入的函数参数的函数名code
var myFunc = function () {}; function test(f) { console.log(f.name); } test(myFunc) // myFunc
function (){[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); // " 这是一个 // 多行注释"
js只有两种做用域:(1)全局做用域:在函数外部声明;(2)函数做用域:在函数内部定义,外部没法读取
值得注意的是,函数内部定义的变量,会在该做用域覆盖掉同名全局变量
另外,只有在函数内部声明的才是局部变量,在其余区块中声明,尽管和函数同样都有区块,一概是全局变量
与全局做用域同样,函数做用域内部也会产生“变量提高”现象。var
命令声明的变量,无论在什么位置,变量声明都会被提高到函数体的头部。
function foo(x) { if (x > 100) { var tmp = x - 100; } } // 等同于 function foo(x) { var tmp; if (x > 100) { tmp = x - 100; }; }
在js中,函数是一等公民,与值相同,所以也有本身的做用域,其做用域与变量同样,就是其声明时所在的做用域,与运行时所在的做用域无关。
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
上面代码将函数x
做为参数,传入函数y
。可是,函数x
是在函数y
体外声明的,做用域绑定外层,所以找不到函数y
的内部变量a
,致使报错。
一样的,函数体内部声明的函数,做用域绑定函数体内部。
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
。正是这种机制,构成了下文要讲解的“闭包”现象。
首先注意,参数是能够省略的。不管函数定义时定义了多少参数,可是运行时提供多少参数甚至不提供参数,js都不会报错。省略的参数的值就变为undefined
。须要注意的是,函数的length
属性与实际传入的参数个数无关,只反映函数预期传入的参数个数。
function f(a, b) { return a; } f(1, 2, 3) // 1 f(1) // 1 f() // undefined f.length // 2
可是,没有办法只省略靠前的参数,而保留靠后的参数。若是必定要省略靠前的参数,只有显式传入undefined
。
function f(a, b) { return a; } f( , 1) // SyntaxError: Unexpected token ,(…) f(undefined, 1) // undefined
上面代码中,若是省略第一个参数,就会报错
var p = 2; function f(p) { p = 3; } f(p); p // 2
var obj = { p: 1 }; function f(o) { o.p = 2; } f(obj); obj.p // 2
如有同名的参数,则取最后出现的那个值,即便后边的参数没有值或者被省略,也是以其为准
function f(a, a) { console.log(a); } f(1) // undefined
这时,若要得到第一个a的值,可使用arguments对象
function f(a, a) { console.log(arguments[0]); } f(1) // 1
arguments对象包含了函数运行时的全部参数,arguments[0]
就是第一个参数,arguments[1]
就是第二个参数,以此类推。这个对象只有在函数体内部,才可使用。
var f = function (one) { console.log(arguments[0]); console.log(arguments[1]); console.log(arguments[2]); } f(1, 2, 3) // 1 // 2 // 3
通常状况下,arguments对象能够在运行时修改,例如,函数f()
调用时传入的参数,在函数内部被修改为3
和2
。
var f = function(a, b) { arguments[0] = 3; arguments[1] = 2; return a + b; } f(1, 1) // 5
可是若使用严格模式,修改arguments
对象就不会影响到实际的函数参数。
var f = function(a, b) { 'use strict'; // 开启严格模式 arguments[0] = 3; arguments[1] = 2; return a + b; } f(1, 1) // 2
经过arguments
对象的length
属性,能够判断函数调用时到底带几个参数。
function f() { return arguments.length; } f(1, 2, 3) // 3 f(1) // 1 f() // 0
值得注意的是,虽然arguments
很像数组,但它是一个对象。数组专有的方法(好比slice
和forEach
),不能在arguments
对象上直接使用。
若是要让arguments
对象使用数组方法,真正的解决方法是将arguments
转为真正的数组。下面是两种经常使用的转换方法:slice
方法和逐一填入新数组。
var args = Array.prototype.slice.call(arguments); // 或者 var args = []; for (var i = 0; i < arguments.length; i++) { args.push(arguments[i]); }
js语言特有链式做用域结构,子对象会一级一级的向上寻找全部父对象的变量。即全部父对象的变量对子对象都是可见的,可是子对象的变量对父对象不可见(老父亲了)
例如,函数内部能够直接读取全局变量,函数外部却没法读取函数内部的变量;F2能够读取F1的变量,F1却不能读取F2的变量
function f1() { var n = 999; function f2() { console.log(n); // 999 } }
下边咱们思考一个问题,原本f1外部是读取不到f1的变量的,可是f2能够读取到父对象f1的变量,若咱们把f2做为返回值,不就能够在f1外部读取f1的内部变量了吗!
function f1() { var n = 999; function f2() { console.log(n); } return f2; } var result = f1(); result(); // 999
这就是闭包的概念,简单来讲,闭包就是定义在函数内部的函数,可以读取父函数内部变量的f2.本质上讲,闭包可以记住诞生的环境,是将函数内部和函数外部连接起来的一座桥梁
start
是函数createIncrementor
的内部变量。经过闭包,start
的状态被保留了,每一次调用都是在上一次调用的基础上进行计算。从中能够看到,闭包inc
使得函数createIncrementor
的内部环境,一直存在。因此,闭包能够看做是函数内部做用域的一个接口。
为何会这样呢?缘由就在于inc
始终在内存中,而inc
的存在依赖于createIncrementor
,所以也始终在内存中,不会在调用结束后,被垃圾回收机制回收。
function createIncrementor(start) { return function () { return start++; }; } var inc = createIncrementor(5); inc() // 5 inc() // 6 inc() // 7
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
的私有变量。
在js 中,圆括号()
是一种运算符,跟在函数名以后,表示调用该函数。好比,print()
就表示调用print
函数。
可是,当咱们在定义函数后当即调用函数时,不能定义后加括号,不然会产生语法错误。这是由于function便可以当作语句,也能够当作表达式,而js引擎规定,若function出如今句首,一概解释为语句。因此,JavaScript 引擎看到行首是function
关键字以后,认为这一段都是函数的定义,不该该以圆括号结尾,因此就报错了。
// 语句 function f() {} // 表达式 var f = function f() {}
解决方法也很简单,就是将函数放到一个圆括号里。这就是IIFE(当即调用的函数表达式)
值得注意的是,最后的分号是必须的,不然会报错
(function(){ /* code */ }()); // 或者 (function(){ /* code */ })();
一般状况下,只对匿名函数使用这种“当即执行的函数表达式”。它的目的有两个:一是没必要为函数命名,避免了污染全局变量;二是 IIFE 内部造成了一个单独的做用域,能够封装一些外部没法读取的私有变量。
// 写法一 var tmp = newData; processData(tmp); storeData(tmp); // 写法二 (function () { var tmp = newData; processData(tmp); storeData(tmp); }());
上面代码中,写法二比写法一更好,由于彻底避免了污染全局变量。
eval
命令接受一个字符串做为参数,并将这个字符串看成语句执行。
eval('var a = 1;'); a // 1
若是参数字符串没法看成语句运行,那么就会报错。
eval('3x') // Uncaught SyntaxError: Invalid or unexpected token
放在eval
中的字符串,应该有独自存在的意义,不能用来与eval
之外的命令配合使用。举例来讲,下面的代码将会报错。
eval('return;'); // Uncaught SyntaxError: Illegal return statement
上面代码会报错,由于return
不能单独使用,必须在函数中使用。
若是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 })()
上面代码中,函数f
内部是严格模式,这时eval
内部声明的foo
变量,就不会影响到外部。
不过,即便在严格模式下,eval
依然能够读写当前做用域的变量。
(function f() { 'use strict'; var foo = 1; eval('foo = 2'); console.log(foo); // 2 })()
上面代码中,严格模式下,eval
内部仍是改写了外部变量,可见安全风险依然存在。
总之,eval
的本质是在当前做用域之中,注入代码。因为安全风险和不利于 JavaScript 引擎优化执行速度,因此通常不推荐使用。一般状况下,eval
最多见的场合是解析 JSON 数据的字符串,不过正确的作法应该是使用原生的JSON.parse
方法。
前面说过eval
不利于引擎优化执行速度。更麻烦的是,还有下面这种状况,引擎在静态代码分析的阶段,根本没法分辨执行的是eval
。
var m = eval; m('var x = 1'); x // 1
上面代码中,变量m
是eval
的别名。静态代码分析阶段,引擎分辨不出m('var x = 1')
执行的是eval
命令。
为了保证eval
的别名不影响代码优化,JavaScript 的标准规定,凡是使用别名执行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
的别名调用,做用域都是全局做用域。