- JavaScript的函数不但是“头等公民”,而且可以像变量一样使用,具有非常强大的抽象能力
-
函数体内部的语句在执行时,一旦执行到
return
时,函数就执行完毕,并将结果返回。因此,函数内部通过条件判断和循环可以实现非常复杂的逻辑。如果没有
return
语句,函数执行完毕后也会返回结果,只是结果为undefined
。 - 定义(2)
var abs = function (x) { if (typeof x !== 'number') { throw 'Not a number'; } if (x >= 0) { return x; } else { return -x; } }; //可以通过变量abs就可以调用该函数。 abs(10); // 返回10 abs(-9); // 返回9
-
arguments
它只在函数内部起作用,并且永远指向当前函数的调用者传入的所有参数。arguments
类似Array
但它不是一个Array。即使函数不定义任何参数,还是可以拿到参数的值:
function abs() { if (arguments.length === 0) { return 0; } var x = arguments[0]; return x >= 0 ? x : -x; } abs(); // 0 abs(10); // 10 abs(-9); // 9 //通常用于判断参数个数 // 接收2~3个参数,b是可选参数,如果只传2个参数,b默认为null: function foo(a, b, c) { if (arguments.length === 2) { // 实际拿到的参数是a和b,c为undefined c = b; // 把b赋给c b = null; // b变为默认值 } // ... }
-
rest参数(ES6)
ES6标准引入了rest参数,用于接收额外的rest
参数
function foo(a, b, ...rest) { console.log('a = ' + a); console.log('b = ' + b); console.log(rest); } foo(1, 2, 3, 4, 5); // 结果: // a = 1 // b = 2 // Array [ 3, 4, 5 ] foo(1); // 结果: // a = 1 // b = undefined // Array [] 如果传入的参数连正常定义的参数都没填满,也不要紧,rest参数会接收一个空数组(注意不是undefined)。
- 变量作用域
如果两个不同的函数各自申明了同一个变量,那么该变量只在各自的函数体内起作用。换句话说,不同函数内部的同名变量互相独立,
由于JavaScript的函数可以嵌套,此时,内部函数可以访问外部函数定义的变量
如果内部函数定义了与外部函数重名的变量,则内部函数的变量将“屏蔽”外部函数的变量。
'use strict'; function foo() { var x = 1; function bar() { var x = 'A'; alert('x in bar() = ' + x); // 'A' } alert('x in foo() = ' + x); // 1 bar(); } //这说明JavaScript的函数在查找变量时从自身函数定义开始,从“内”向“外”查找
-
变量提升
JavaScript的函数定义有个特点,它会先扫描整个函数体的语句,把所有申明的变量“提升”到函数顶部:
function foo() { var x = 'Hello, ' + y; alert(x); var y = 'Bob'; } //JavaScript引擎看到的代码相当于: function foo() { var y; // 提升变量y的申明 var x = 'Hello, ' + y; alert(x); y = 'Bob'; } //由于JavaScript的这一怪异的“特性”,我们在函数内部定义变量时,请严格遵守“在函数内部首先申明所有变量”这一规则。最常见的做法是用一个var申明函数内部用到的所有变量: function foo() { var x = 1, // x初始化为1 y = x + 1, // y初始化为2 z, i; // z和i为undefined // 其他语句: for (i=0; i<100; i++) { ... } }
-
全局作用域
不在任何函数内定义的变量就具有全局作用域。实际上,JavaScript默认有一个全局对象window
,全局作用域的变量实际上被绑定到window
的一个属性。因此,直接访问全局变量course
和访问window.course
是完全一样的。实际上alert等函数也是window的一个变量。
-
名字空间
全局变量会绑定到window
上,不同的JavaScript文件如果使用了相同的全局变量,或者定义了相同名字的顶层函数,都会造成命名冲突,并且很难被发现。
减少冲突的一个方法是把自己的所有变量和函数全部绑定到一个全局变量中。例如:
// 唯一的全局变量MYAPP: var MYAPP = {}; // 其他变量: MYAPP.name = 'myapp'; MYAPP.version = 1.0; // 其他函数: MYAPP.foo = function () { return 'foo'; };
-
局部作用域let(ES6)
用let
替代var
可以申明一个块级作用域的变量:
'use strict'; function foo() { var sum = 0; for (let i=0; i<100; i++) { sum += i; } i += 1; // SyntaxError }
-
常量const(ES6)
ES6标准引入了新的关键字const
来定义常量,const
与let
都具有块级作用域:
'use strict'; const PI = 3.14; PI = 3; // 某些浏览器不报错,但是无效果! PI; // 3.14
- this
JavaScript的函数内部如果调用了this,那么这个
this
到底指向谁?答案是,视情况而定!
由于这是一个巨大的设计错误,要想纠正可没那么简单。ECMA决定,在strict模式下让函数的this
指向undefined;这个决定只是让错误及时暴露出来,并没有解决
this
应该指向的正确位置。
function getAge() { var y = new Date().getFullYear(); return y - this.birth; } var xiaoming = { name: '小明', birth: 1990, age: getAge }; xiaoming.age; // function xiaoming.age() xiaoming.age(); // 25, 正常结果 getAge(); // NaN 如果单独调用函数,比如getAge(),此时,该函数的this指向全局对象,也就是window。 如果这么写也是不行,要保证this指向正确,必须用obj.xxx()的形式调用!: var fn = xiaoming.age; // 先拿到xiaoming的age函数 fn(); // NaN
修复的办法也不是没有,我们用一个that
变量首先捕获this
:
'use strict'; var xiaoming = { name: '小明', birth: 1990, age: function () { var that = this; // 在方法内部一开始就捕获this function getAgeFromBirth() { var y = new Date().getFullYear(); return y - that.birth; // 用that而不是this } return getAgeFromBirth(); } }; xiaoming.age(); // 25
-
apply与call
要指定函数的this
指向哪个对象,可以用函数本身的apply
方法,它接收两个参数,第一个参数就是需要绑定的this
变量,第二个参数是Array
,表示函数本身的参数。
用apply
修复getAge()
调用:
function getAge() { var y = new Date().getFullYear(); return y - this.birth; } var xiaoming = { name: '小明', birth: 1990, age: getAge }; xiaoming.age(); // 25 getAge.apply(xiaoming, []); // 25, this指向xiaoming, 参数为空
另一个与apply()
类似的方法是call()
,唯一区别是:
-
apply()
把参数打包成Array
再传入; -
call()
把参数按顺序传入。
Math.max.apply(null, [3, 5, 4]); // 5 Math.max.call(null, 3, 5, 4); // 5 对普通函数调用,我们通常把this
绑定为null
。
-
装饰器
JavaScript的所有对象都是动态的,即使内置的函数,我们也可以重新指向新的函数,利用apply()
,我们还可以动态改变函数的行为。
现在假定我们想统计一下代码一共调用了多少次parseInt()
,可以把所有的调用都找出来,然后手动加上count += 1
,不过这样做太傻了。最佳方案是用我们自己的函数替换掉默认的parseInt()
:
var count = 0; var oldParseInt = parseInt; // 保存原函数 window.parseInt = function () { count += 1; return oldParseInt.apply(null, arguments); // 调用原函数 }; // 测试: parseInt('10'); parseInt('20'); parseInt('30'); count; // 3
高阶函数
既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。
function add(x, y, f) { return f(x) + f(y); }
map
由于map()
方法定义在JavaScript的Array
中,我们调用Array
的map()
方法,传入我们自己的函数,就得到了一个新的Array
作为结果:
function pow(x) { return x * x; } var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9]; arr.map(pow); // [1, 4, 9, 16, 25, 36, 49, 64, 81] //map()作为高阶函数,事实上它把运算规则抽象了,因此,我们不但可以计算简单的f(x)=x2,还可以计算任意复杂的函数,比如,把Array的所有数字转为字符串: var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9]; arr.map(String); // ['1', '2', '3', '4', '5', '6', '7', '8', '9']
reduce
Array的reduce()
把一个函数作用在这个Array
的[x1, x2, x3...]
上,这个函数必须接收两个参数,reduce()
把结果继续和序列的下一个元素做累积计算,其效果就是:
[x1, x2, x3, x4].reduce(f) = f(f(f(x1, x2), x3), x4 //比方说对一个Array求和,就可以用reduce实现: var arr = [1, 3, 5, 7, 9]; arr.reduce(function (x, y) { return x + y; }); // 25
常用js方法
x.charAt(0) //取字符串的某个索引处的值 x.toUpperCase()//转大写 x.toLowerCase();//转小写 var arr = ['1', '2', '3']; var r=r = arr.map(parseInt); 结果竟然是[1, NaN, NaN] r = arr.map(str=>parseInt(str,10)); // 通常使用parseInt时,只需要传递一个参数. // 但实际上,parseInt可以有两个参数.第二个参数是进制数. // 可以通过语句"alert(parseInt.length)===2"来验证. // map方法在调用callback函数时,会给它传递三个参数:当前正在遍历的元素, // 元素索引, 原数组本身. // 第三个参数parseInt会忽视, 但第二个参数不会,也就是说, // parseInt把传过来的索引值当成进制数来使用.从而返回了NaN. arrayObject.slice(start,end)//slice() 方法可从已有的数组中返回选定的元素。 //start 必需。规定从何处开始选取。如果是负数,那么它规定从数组尾部开始算起的位置。也就是说,-1 指最后一个元素,-2 指倒数第二个元素,以此类推。 //end 可选。规定从何处结束选取。该参数是数组片断结束处的数组下标。如果没有指定该参数,那么切分的数组包含从 start 到数组结束的所有元素。如果这个参数是负数,那么它规定的是从数组尾部开始算起的元素。
filter
filter也是一个常用的操作,它用于把Array
的某些元素过滤掉,然后返回剩下的元素。和map()
不同的是,filter()
把传入的函数依次作用于每个元素,然后根据返回值是true
还是false
决定保留还是丢弃该元素。
//把一个Array中的空字符串删掉,可以这么写: var arr = ['A', '', 'B', null, undefined, 'C', ' ']; var r = arr.filter(function (s) { return s && s.trim(); // 注意:IE9以下的版本没有trim()方法 }); r; // ['A', 'B', 'C'] //回调函数 var arr = ['A', 'B', 'C']; var r = arr.filter(function (element, index, self) { console.log(element); // 依次打印'A', 'B', 'C' console.log(index); // 依次打印0, 1, 2 console.log(self); // self就是变量arr return true; }); //删除数组你重复元素 arr.filter(function (element, index, self) { return self.indexOf(element) === index; });//去除重复元素依靠的是indexOf总是返回第一个元素的位置,后续的重复元素位置与indexOf返回的位置不相等,因此被filter滤掉了。
sort
通常规定,对于两个元素x
和y
,如果认为x < y
,则返回-1
,如果认为x == y
,则返回0
,如果认为x > y
,则返回1
,这样,排序算法就不用关心具体的比较过程,而是根据比较结果直接排序。
sort()
方法会直接对Array
进行修改,它返回的结果仍是当前Array
:
// apple排在了最后:因为字符串根据ASCII码进行排序,而小写字母a的ASCII码在大写字母之后。 ['Google', 'apple', 'Microsoft'].sort(); // ['Google', 'Microsoft", 'apple'] // 无法理解的结果:sort()方法默认把所有元素先转换为String再排序,结果'10'排在了'2'的前面,因为字符'1'比字符'2'的ASCII码小。 [10, 20, 1, 2].sort(); // [1, 10, 2, 20]
//对字符串排序,是按照ASCII的大小比较的,现在,我们提出排序应该忽略大小写,按照字母序排序。
var arr = ['Google', 'apple', 'Microsoft'];
arr.sort(function (s1, s2) {
x1 = s1.toUpperCase();
x2 = s2.toUpperCase();
if (x1 < x2) {
return -1;
}
if (x1 > x2) {
return 1;
}
return 0;
}); // ['apple', 'Google', 'Microsoft']