一、在JavaScript中,定义函数的方式如下: 1、第一种方式: function abs(x) { if (x >= 0) { return x; } else { return -x; } } 上述abs()函数的定义如下: function指出这是一个函数定义; abs是函数的名称; (x)括号内列出函数的参数,多个参数以,分隔; { ... }之间的代码是函数体,可以包含若干语句,甚至可以没有任何语句。 结果: 1、执行到return时,函数就执行完毕,并将结果返回 2、如果没有return语句,函数执行完毕后也会返回结果,只是结果为undefined。 2、第二种定义函数的方式 var abs = function (x) { if (x >= 0) { return x; } else { return -x; } }; 在这种方式下,function (x) { ... }是一个匿名函数,它没有函数名。但是,这个匿名函数赋值给了变量abs,所以,通过变量abs就可以调用该函数。 上述两种定义完全等价,注意第二种方式按照完整语法需要在函数体末尾加一个;,表示赋值语句结束。 3、调用函数时,按顺序传入参数即可: abs(10); // 返回10 abs(-9); // 返回9 3.1、由于JavaScript允许传入任意个参数而不影响调用,因此传入的参数比定义的参数多也没有问题,虽然函数内部并不需要这些参数: abs(10, 'blablabla'); // 返回10 abs(-9, 'haha', 'hehe', null); // 返回9 3.2、传入的参数比定义的少也没有问题:abs(); // 返回NaN 3.3、要避免收到undefined,可以对参数进行检查: function abs(x) { if (typeof x !== 'number') { throw 'Not a number'; } if (x >= 0) { return x; } else { return -x; } } 常用函数: 1、typeof 判断元素的类型 类型 结果 String "string" Number "number" Boolean "boolean" Undefined "undefined" Object "object" function函数对象 "function" Symbol(ES6新增) "symbol" 案例:用法 // Numbers typeof 37 === 'number'; typeof 3.14 === 'number'; typeof Math.LN2 === 'number'; typeof Infinity === 'number'; typeof NaN === 'number'; // 尽管NaN是"Not-A-Number"的缩写 typeof Number(1) === 'number'; // 但不要使用这种形式! // Strings typeof "" === 'string'; typeof "bla" === 'string'; typeof (typeof 1) === 'string'; // typeof总是返回一个字符串 typeof String("abc") === 'string'; // 但不要使用这种形式! // Booleans typeof true === 'boolean'; typeof false === 'boolean'; typeof Boolean(true) === 'boolean'; // 但不要使用这种形式! // Symbols typeof Symbol() === 'symbol'; typeof Symbol('foo') === 'symbol'; typeof Symbol.iterator === 'symbol'; // Undefined typeof undefined === 'undefined'; typeof declaredButUndefinedVariable === 'undefined'; typeof undeclaredVariable === 'undefined'; // Objects typeof {a:1} === 'object'; // 使用Array.isArray 或者 Object.prototype.toString.call // 区分数组,普通对象 typeof [1, 2, 4] === 'object'; typeof new Date() === 'object'; // 下面的容易令人迷惑,不要使用! typeof new Boolean(true) === 'object'; typeof new Number(1) ==== 'object'; typeof new String("abc") === 'object'; // 函数 typeof function(){} === 'function'; typeof Math.sin === 'function'; //NaN typeof 1/0 === 'NaN'; 2、关键字arguments:arguments类似Array但它不是一个Array【它只在函数内部起作用,并且永远指向当前函数的调用者传入的所有参数】 案例1: function foo(x) { console.log('x = ' + x); // 10 for (var i=0; i<arguments.length; i++) { console.log('arg ' + i + ' = ' + arguments[i]); // 10, 20, 30 } } foo(10, 20, 30); 案例2: 利用arguments,你可以获得调用者传入的所有参数。也就是说,即使函数不定义任何参数,还是可以拿到参数的值: 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 案例3: 实际上arguments最常用于判断传入参数的个数。你可能会看到这样的写法: // foo(a[, b], c) // 接收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变为默认值 } // ... } 要把中间的参数b变为“可选”参数,就只能通过arguments判断,然后重新调整参数并赋值。 3、rest参数 案例: 'use static' function foo(a, b, ...rest) { console.log('85a = ' + a); console.log('b = ' + b); console.log(rest); } 调用函数1: foo(1, 2, 3, 4, 5); // 结果: // a = 1 // b = 2 // Array [ 3, 4, 5 ] 调用函数2: foo(1); // 结果: // a = 1 // b = undefined // Array [] 结果: rest参数只能写在最后,前面用...标识,从运行结果可知,传入的参数先绑定a、b,多余的参数以数组形式交给变量rest,所以, 不再需要arguments我们就获取了全部参数。 如果传入的参数连正常定义的参数都没填满,也不要紧,rest参数会接收一个空数组(注意不是undefined)。 3、小心你的return语句、 前面我们讲到了JavaScript引擎有一个在行末自动添加分号的机制,这可能让你栽到return语句的一个大坑: function foo() { return { name: 'foo' }; } foo(); // { name: 'foo' } 如果把return语句拆成两行: function foo() { return { name: 'foo' }; } foo(); // undefined 要小心了,由于JavaScript引擎在行末自动添加分号的机制,上面的代码实际上变成了: function foo() { return; // 自动添加了分号,相当于return undefined; { name: 'foo' }; // 这行语句已经没法执行到了 } 所以正确的多行写法是: function foo() { return { // 这里不会自动加分号,因为{表示语句尚未结束 name: 'foo' }; } 案例: function area_of_circle(r, pi) { if (arguments.length < 2) { pi = 3.14; return pi * (r * r); } else { return pi * (r * r); } } // 测试: if (area_of_circle(2) === 12.56 && area_of_circle(2, 3.1416) === 12.5664) { console.log('测试通过'); } else { console.log('测试失败'); };
变量的作用域:如果两个不同的函数各自申明了同一个变量,那么该变量只在各自的函数体内起作用。 换句话说,不同函数内部的同名变量互相独立,互不影响: 案例1: 'use strict'; function foo() { var x = 1; x = x + 1; } x=x+1 //函数体外不能调用内部变量 function bar() { var x = 'A'; x = x + 'B'; } 案例2: 由于JavaScript的函数可以嵌套,此时,内部函数可以访问外部函数定义的变量,反过来则不行: 'use strict'; function foo() { var x = 1; function bar() { var y = x + 1; // bar可以访问foo的变量x! } var z = y + 1; // ReferenceError! foo不可以访问bar的变量y! } 结论:JavaScript的函数在查找变量时从自身函数定义开始,从“内”向“外”查找。 如果内部函数定义了与外部函数重名的变量,则内部函数的变量将“屏蔽”外部函数的变量。 案例3: 变量提升 JavaScript的函数定义有个特点,它会先扫描整个函数体的语句,把所有申明的变量“提升”到函数顶部: 'use strict'; function foo() { var x = 'Hello, ' + y; console.log(x); var y = 'Bob'; } foo(); 虽然是strict模式,但语句var x = 'Hello, ' + y;并不报错,原因是变量y在稍后申明了。但是console.log显示Hello, undefined, 说明变量y的值为undefined。这正是因为JavaScript引擎自动提升了变量y的声明,但不会提升变量y的赋值。 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的一个属性: 'use strict'; var course = 'Learn JavaScript'; alert(course); // 'Learn JavaScript' alert(window.course); // 'Learn JavaScript' 因此,直接访问全局变量course和访问window.course是完全一样的 了,由于函数定义有两种方式,以变量方式var foo = function () {}定义的函数实际上也是一个全局变量,因此,顶层函数的定义也被视为一个全局变量,并绑定到window对象: 'use strict'; function foo() { alert('foo'); } foo(); // 直接调用foo() window.foo(); // 通过window.foo()调用 进一步大胆地猜测,我们每次直接调用的alert()函数其实也是window的一个变量 说明:说明JavaScript实际上只有一个全局作用域。任何变量(函数也视为变量),如果没有在当前函数作用域中找到, 就会继续往上查找,最后如果在全局作用域中也没有找到,则报ReferenceError错误。 名字空间: 全局变量会绑定到window上,不同的JavaScript文件如果使用了相同的全局变量,或者定义了相同名字的顶层函数,都会造成命名冲突,并且很难被发现。 减少冲突的一个方法是把自己的所有变量和函数全部绑定到一个全局变量中。 案例:把自己的代码全部放入唯一的名字空间MYAPP中,会大大减少全局变量冲突的可能。【许多著名的JavaScript库都是这么干的:jQuery,YUI,underscore等等。】 // 唯一的全局变量MYAPP: var MYAPP = {}; // 其他变量: MYAPP.name = 'myapp'; MYAPP.version = 1.0; // 其他函数: MYAPP.foo = function () { return 'foo'; }; 案例:局部作用域 由于JavaScript的变量作用域实际上是函数内部,我们在for循环等语句块中是无法定义具有局部作用域的变量的: 'use strict'; function foo() { for (var i=0; i<100; i++) { // } i += 100; // 仍然可以引用变量i } 为了解决块级作用域,ES6引入了新的关键字let,用let替代var可以申明一个块级作用域的变量: 案例: 'use strict'; function foo() { var sum = 0; for (let i=0; i<100; i++) { sum += i; } // SyntaxError: i += 1; } 常量: 由于var和let申明的是变量,如果要申明一个常量,在ES6之前是不行的,我们通常用全部大写的变量来表示“这是一个常量,不要修改它的值”: var PI = 3.14; ES6标准引入了新的关键字const来定义常量,const与let都具有块级作用域: 'use strict'; const PI = 3.14; PI = 3; // 某些浏览器不报错,但是无效果! PI; // 3.14 对一个对象进行解构赋值时,同样可以直接对嵌套的对象属性进行赋值,只要保证对应的层次是一致的: 案例: var person = { name: '小明', age: 20, gender: 'male', passport: 'G-12345678', school: 'No.4 middle school', address: { city: 'Beijing', street: 'No.1 Road', zipcode: '100001' } }; var {name, address: {city, zip}} = person; name; // '小明' city; // 'Beijing' zip; // undefined, 因为属性名是zipcode而不是zip // 注意: address不是变量,而是为了让city和zip获得嵌套的address对象的属性: address; // Uncaught ReferenceError: address is not defined 使用解构赋值对对象属性进行赋值时,如果对应的属性不存在,变量将被赋值为undefined, 这和引用一个不存在的属性获得undefined是一致的。如果要使用的变量名和属性名不一致,可以用下面的语法获取:
方法: 在一个对象中绑定函数,称为这个对象的方法。 案例: var xiaoming = { name: '小明', birth: 1990, age: function () { var y = new Date().getFullYear(); return y - this.birth; } }; xiaoming.age; // function xiaoming.age() xiaoming.age(); // 今年调用是25,明年调用就变成26了 重点概念:this 在一个方法内部,this是一个特殊变量,它始终指向当前对象,也就是xiaoming这个变量。 所以,this.birth可以拿到xiaoming的birth属性。 让我们拆开写: 案例: function getAge() { var y = new Date().getFullYear(); return y - this.birth; } var xiaoming = { name: '小明', birth: 1990, age: getAge }; xiaoming.age(); // 25, 正常结果 getAge(); // NaN ECMA决定,在strict模式下让函数的this指向undefined,因此,在strict模式下,你会得到一个错误: 'use strict'; var xiaoming = { name: '小明', birth: 1990, age: function () { var y = new Date().getFullYear(); return y - this.birth; } }; var fn = xiaoming.age; fn(); // Uncaught TypeError: Cannot read property 'birth' of undefined 我们用一个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 用var that = this;,你就可以放心地在方法内部定义其他函数,而不是把所有语句都堆到一个方法中。 案例:apply 虽然在一个独立的函数调用中,根据是否是strict模式,this指向undefined或window,不过,我们还是可以控制this的指向的! 要指定函数的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(3, 5, 4),分别用apply()和call()实现如下: Math.max.apply(null, [3, 5, 4]); // 5 Math.max.call(null, 3, 5, 4); // 5 装饰器: 利用apply(),我们还可以动态改变函数的行为。 JavaScript的所有对象都是动态的,即使内置的函数,我们也可以重新指向新的函数
Higher-order function
JavaScript的函数其实都指向某个变量。既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。
一个最简单的高阶函数:
function add(x, y, f) {
return f(x) + f(y);
}
当我们调用add(-5, 6, Math.abs)时,参数x,y和f分别接收-5,6和函数Math.abs,根据函数定义,我们可以推导计算过程为:
x = -5;
y = 6;
f = Math.abs;
f(x) + f(y) ==> Math.abs(-5) + Math.abs(6) ==> 11;
return 11;
案例:
用代码验证一下:
'use strict';
function add(x, y, f) {
return f(x) + f(y);
}
var x = add(-5, 6, Math.abs); // 11
console.log(x);
二、Map、Reduce
map 举例说明,比如我们有一个函数f(x)=x2,要把这个函数作用在一个数组[1, 2, 3, 4, 5, 6, 7, 8, 9]上,就可以用map实现如下
'use strict';
function pow(x) {
return x * x;
}
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
var results = arr.map(pow); // [1, 4, 9, 16, 25, 36, 49, 64, 81]
console.log(results);
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']