JavaScript 系列博客(三)
前言
本篇介绍 JavaScript 中的函数知识。
函数的三种声明方法
function 命令
可以类比为 python 中的 def 关键词。
function 命令声明的代码区块,就是一个函数。命令后面是函数名,函数名后面的圆括号里面是要传入的形参名。函数体放在大括号里面。
function fn(name) { console.log(name); }
使用 function 命名了一个 fn 函数,以后可以通过调用 fn 来运行该函数。这叫做函数的声明(Function Declaration)。
函数表达式
除了使用 function 命令声明函数外,可以采用变量赋值的写法。(匿名函数)
var fn = function(name) { console.log(name); };
这种写法将一个匿名函数赋值给变量。这时,这个匿名函数又称之为函数表达式(Function Expression),因为赋值语句的等号右侧只能放表达式。
采用函数表达式声明函数时,function 命令后面不带有函数名。如果加上函数名,该函数名只能在函数体内访问,在函数体外部无效。
var fn = function x(name) { console.log(typeof x); }; x// ReferenceError: x is not definedfn();// function
声明函数时,在函数表达式后加了函数名 x,这个 x 只可以在函数内部使用,指代函数表达式本身。这种写法有两个用处:一可以在函数体内部调用自身;二方便debug(debug 显示函数调用栈时,会显示函数名)。需要注意的是,函数表达式需要在语句的结尾加上分号,表示语句结束。而函数的声明在结尾的大括号后面不用加分号。
Function 构造函数
第三种声明函数的方法是通过构造函数,可以理解为 python 中的函数类,通过传入参数并且返回结果就可以创建一个函数。
构造函数接收三个参数,最后一个为 add函数的‘’函数体‘’,其他参数为add 函数的参数。可以为构造函数传递任意数量的参数,不过只有最后一个参数被当做函数体,如果只有一个参数,该参数就是函数体。
Function 构造函数也可以不用 new 命令,结果一样。这种声明函数的方式不直观,使用概率很少。
函数的调用
和 python 一样,调用一个函数通过圆括号,圆括号中是要传入的实参。
函数体内部的 return 语句,表示返回。JavaScript 引擎遇到 return 时,就直接返回 return 后面表达式的值(和 python 一样),所以 return 后面的代码是无意义的,如果没有 return 那么就会返回 undefined(python 中返回 None)。
函数作用域
作用域的定义
作用域指的是变量存在的范围。在 ES5中,JavaScript 只有两种作用域:一种是全局作用域,变量在整个程序中一直存在,任意位置可以访问到;另一种是函数作用域,也称之为局部作用域,变量只有在函数内部才能访问到。ES6新增了块级作用域,等价于局部作用域一样,就是新增了一种产生局部作用域的方式。通过大括号产生块级作用域。
在函数外部声明的变量就是全局变量,可以在任意位置读取。
在函数内部定义的变量,外部无法读取,只有在函数内部可以访问到。并且函数内部定义的同名变量,会在函数内覆盖全局变量。
注意:对于 var 命令来说,局部变量只可以在函数内部声明,在其他区块中声明,一律都是全局变量。ES6中声明变量的命令改为 let,在区块中声明变量产生块级作用域。
函数内部的变量提升
与全局作用域一样,函数作用域也会产生‘’变量提升‘’现象。var 命令生命的变量,不管在什么位置,变量声明都会被提升到函数体的头部。
function foo(x) { if (x > 100) { var tmp = x - 100; } }// 等同于function foo(x) { var tmp; if (x > 100) { tmp = x - 100; } }
函数本身的作用域
函数和其他值(数值、字符串、布尔值等)地位相同。凡是可以使用值得地方,就可以使用函数。比如,可以把函数赋值给变量和对象的属性,也可以当做参数传入其他函数,或者作为函数的结果返回。函数是一个可以执行的值,此外没有特殊之处。
函数也有自己的作用域,函数的作用域称为局部作用域。与变量一样,就是其生命时所在的作用域,与其运行时所在的作用域无关(闭包、装饰器)。通俗地讲就是在定义函数的时候,作用域已经就确定好了,那么在访问变量的时候就开始从本作用域开始查找,而与函数的调用位置无关。
var x = function () { var a = 1; console.log(a); };function y() { var a = 2; x(); } y(); // 1
函数 x 是在函数 f 的外部生命的,所以它的作用域绑定外层,内部变量 a 不会到函数 f 体内取值,所以输出1,而不是2。
总之,函数执行时所在的作用域,是定义时的作用域,而不是调用时所在的作用域。
函数参数
调用函数时,有时候需要外部传入的实参,传入不同的实参会得到不同的结果,这种外部数据就叫参数。
参数的省略
在 JavaScript 中函数参数不是必需的,就算传入的参数和形参的个数不相等也不会报错。调用时无论提供多少个参数(或者不提供参数),JavaScript 都不会报错。省略的参数的值变为 undefined。需要注意的是,函数的 length 属性值与实际传入的参数个数无关,只反映函数预期传入的参数个数。
但是,JavaScript 中的参数都是位置参数,所以没有办法只省略靠前的参数,而保留靠后的参数。如果一定要省略靠前的参数,只有显示的传入 undefined。
传递方式
函数参数如果是原始类型的值(数值、字符串、布尔值),传递方式是传值传递(pass by value)。这意味着,在函数体内修改参数值,不会影响到函数外部(局部变量的修改不会影响到全局变量:对于基本数据类型)。
但是,如果函数参数是复合类型的值(数组、对象、其他函数),因为传值方式为地址传递(pass by reference)。也就是说,传入函数的原始值的地址,因此在函数内部修改参数,将会影响到原始值。
注意:如果函数内部修改的不是参数对象的某个属性,而是直接替换掉整个参数,这时不会影响到原始值。
var obj = [1, 2, 3];function f(o) { o = [2, 3, 4]; } f(obj); obj // [1, 2, 3]
上面代码,在函数 f 内部,参数对象 obj 被整个替换成另一个值。这时不会影响到原始值。这是因为,形式参数(o)的值实际上是参数 obj 的地址,重新对o 赋值导致 o 指向另一个地址,保存在原地址上的数据不会被改变。
同名参数
如果有同名的参数,则取最后出现的那个值。
function f(a, a) { console.log(a); } f(1, 2) // 2
上面代码中,函数 f 有两个参数,且参数名都是 a。取值的时候,以后面的 a 为准,即使后面的a 没有值或被省略,也是以其为准。
function f(a, a) { console.log(a); } f(1) // undefined
调用函数 f 时,没有提供第二个参数,a 的取值就变成了 undefined。这时,如果要获得第一个 a 的值,可以使用 arguments 对象(类比linux 中的arg)。
function f(a, a) { console.log(arguments[0]); } f(1) // 1
arguments 对象
定义
由于 JavaScript 允许函数有不定数目的参数,所以需要一种机制,可以在函数体内部读取所有参数。这就是 arguments 对象的由来。
arguments 对象包含了函数运行时的所有参数,arguments[0]就是第一个参数,以此类推。注意:该对象只有在函数体内部才可以使用。
正常模式下,arguments 对象可以在运行时修改。
var f = function(a, b) { arguments[0] = 3; arguments[1] = 3; return a + b; } f(1, 1) // 5
上面代码中,调用 f 时传入的参数,在函数体内被修改了,那么结果也会修改。
严格模式下,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) // 3f(1) // 1
与数组的关系
需要注意的是,虽然 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]); }
callee 属性
arguments 对象带有一个 callee 属性,返回它所对应的原函数。
var f = function() { console.log(arguments.callee === f); } f(); // true
可以通过 arguments.callee,达到调用自身的目的。这个属性在严格模式里面是禁用的,不建议使用。
函数闭包
闭包是所有编程语言的难点,在 python 中闭包的多应用于装饰器中。在 JavaScript 中闭包多用于创建作用域,或者解决变量污染的问题。
理解闭包,首先需要理解变量作用域。在 ES5中,JavaScript 只有两种作用域:全局作用于和函数作用域。函数内部可以直接读取全局变量。
var n = 999;function f1() { console.log(n); } f1(); // 999,n是全局变量,可以被访问到
但是函数外部无法读物函数内部声明的变量。
function f1() { var n = 999; }console.log(n);// Uncaught ReferenceError: n is not defined
因为变量作用域的关系,在外部需要访问到局部变量在正常情况下是做不到的,这就可以通过闭包来实现。下来来看一个经典例子:循环绑定事件产生的变量污染
<div class="box"> 0000001</div><div class="box"> 0000002</div><div class="box"> 0000003</div><script> var divs = document.querySelectorAll(".box"); // 存在污染的写法 for (var i =0; i < divs.length; i++) { divs.onclick = function () { console.log('xxx', i) } } // 运行结果显示4</script>
会产生变量污染的原因是作用域,因为 var 并不产生作用域,所以在 for循环中的变量就是全局变量,只要 for循环结束那么 i 的值就确定了,除非在极限情况下,你的手速比 cpu 还要快,那么可能会看到小于4的值。这样的问题可以通过函数的闭包来解决。产生新的作用域用来保存 i 的值。
for (var i = 0; i < divs.length; i++) { (function () { var index = i; divs[index].onclick = function () { console.log('xxx', index); } })() }// 另一种版本for (var i = 0; i < divs.length; i++) { function(i) { divs[i].onclick = function () { console.log('yyy', i) } }(i) }
利用闭包原理产生新的作用域用来保存变量 i 的值,这样就解决了变量污染的问题,还有利用ES6的声明变量关键词 let,也会产生新的作用域(块级作用域)也可以解决变量污染的问题。
在 JavaScript 中,嵌套函数中的子函数中可以访问到外部函数中的局部变量,但是外部函数访问不到子函数中的局部变量,这是 JavaScript 中特有的‘’链式作用域‘’结构(python 也一样),子对象会一级一级的向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。可以简单地把闭包理解为‘’定义在一个函数内部的函数‘’,闭包最大的特点就是它可以‘’记住‘’诞生的环境,在本质上闭包就是将函数内部和函数外连接起来的一座桥梁。
必报的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量始终保持在内存中,即闭包可以使得它诞生的环境一直存在。下面的例子:
function createIncrementor(start) { return function () { return start++; }; }var inc = createIncrementor(5); inc(); // 5inc(); // 6inc(): // 7
上面代码中,start 是函数 createIncrementor 的内部变量。通过闭包,start 的状态被保存,每一次调用都是在上一次调用的基础上进行计算。从中可以看出,闭包 inc 使得函数 createIncrementor 的内部环境一直存在。所以闭包可以看做是函数内部作用域的一个接口。为什么会这样呢?原因就在于 inc 始终在内存中,而 inc 的存在依赖于 createIncrementor,因此也一直存在于内存中,不会再外层函数调用结束后 start 变量被垃圾回收机制回收。
闭包的另外一个用处是封装对象的私有属性和私有方法。(这部分还不太懂,还需要琢磨)
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的私有变量。
注意:外城函数每次运行,都会产生一个新的闭包,而这个闭包又会保留外城函数的内部变量,所以内存消耗很大。
共同学习,写下你的评论
评论加载中...
作者其他优质文章