@(js进阶)
函数与函数式编程
一.几个基本概念
函数声明:
JavaScript中有两种声明方式,一个是使用var
的变量声明,另一个就是使用function
的函数声明
变量对象的创建过程中,函数声明比变量声明具有更为优先的执行顺序,即我们常常提到的函数声明提前。因此我们在执行上下文中,无论在什么位置声明了函数,我们都可以在同一个执行上下文中直接使用该函数。
函数表达式
函数表达式使用var
进行声明,因此函数表达式是否可以使用遵循变量声明的规则,变量声明其实是分为两个步骤
// 变量声明
var a = 20;
// 实际执行顺序
var a = undefined; // 变量声明,初始值undefined,变量提升,提升顺序次于function声明
a = 20; // 变量赋值,该操作不会提升
函数表达式的提升方式与变量声明一致
fn(); // 报错
var fn = function() {
console.log('function');
}
上面的例子的执行顺序是
var fn = undefined; // 变量声明提升
fn(); // 执行报错
fn = function() { // 赋值操作,此时将后边函数的引用赋值给fn
console.log('function');
}
匿名函数
匿名函数,顾名思义,就是指的没有被显示进行赋值操作的函数。它的使用场景,多作为一个参数传入另一个函数中。
var a = 10;
var fn = function(bar, num) {
return bar() + num;
}
fn(function() {
return a;
}, 20)
我们没有办法在外部执行上下文中引用到它,但是在fn函数内部,我们将该匿名函数赋值给了变量bar,保存在了fn变量对象的arguments对象中。
// 变量对象在fn上下文执行过程中的创建阶段
VO(fn) = {
arguments: {
bar: undefined,
num: undefined,
length: 2
}
}
// 变量对象在fn上下文执行过程中的执行阶段
// 变量对象变为活动对象,并完成赋值操作与执行可执行代码
VO -> AO
AO(fn) = {
arguments: {
bar: function() { return a },
num: 20,
length: 2
}
}
由于匿名函数传入另一个函数之后,最终会在另一个函数中执行,因此我们也常常称这个匿名函数为回调函数。
函数自执行与块级作用域
在ES5中,没有块级作用域,因此我们常常使用函数自执行的方式来模仿块级作用域,这样就提供了一个独立的执行上下文,结合闭包,就为模块化提供了基础。
一个模块往往可以包括:私有变量、私有方法、公有变量、公有方法。
根据作用域链的单向访问,可以很容易知道在这个独立的模块中,外部执行环境是无法访问内部的任何变量与方法的,因此我们可以很容易的创建属于这个模块的私有变量与私有方法。
(function() {
// 私有变量
var age = 20;
var name = 'Tom';
// 私有方法
function getName() {
return `your name is ` + name;
}
})();
利用闭包,我们可以访问到执行上下文内部的变量和方法,因此,我们只需要根据闭包的定义,创建一个闭包,将你认为需要公开的变量和方法开放出来即可。
(function() {
// 私有变量
var age = 20;
var name = 'Tom';
// 私有方法
function getName() {
return `your name is ` + name;
}
// 共有方法
function getAge() {
return age;
}
// 将引用保存在外部执行环境的变量中,形成闭包,防止该执行环境被垃圾回收
window.getAge = getAge;
})();
详细见
二、函数参数的传递方式:按值传递
基本类型复制,是直接值发生了复制,因此改变后互不影响;然而对象变量复制,是引用地址的复制,因此复制之后两个变量指向同一个引用地址,两者中一个改变另一个也改变。
var a = 20;
var b = a;
b = 10;
console.log(a); // 20
var m = { a: 1, b: 2 }
var n = m;
n.a = 5;
console.log(m.a) // 5
函数的参数在进入函数后,实际是被保存在了函数的变量对象中,如argument
中,因此,这个时候相当于发生了一次复制,如下例
var a = 20;
function fn(a) {
a = a + 10;
return a;
}
console.log(a); // 20
var a = { m: 10, n: 20 }
function fn(a) {
a.m = 20;
return a;
}
fn(a);
console.log(a); // { m: 20, n: 20 }
实际上结论仍然是按值传递,只不过当我们期望传递一个引用类型时,真正传递的,只是这个引用类型保存在变量对象中的引用而已,如下例:
var person = {
name: 'Nicholas',
age: 20
}
function setName(obj) { // 传入一个引用
obj = {}; // 将传入的引用指向另外的值
obj.name = 'Greg'; // 修改引用的name属性
}
setName(person);
console.log(person.name); // Nicholas 未被改变
在上面的例子中,如果person是按引用传递,那么person就会自动被修改为指向其name属性值为Gerg的新对象。但是我们从结果中看到,person对象并未发生任何改变,因此只是在函数内部引用被修改而已。
三、函数式编程
函数是第一等公民
指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数传入另一个函数,或者作为别的函数的返回值。如下例:
var a = function foo() {} // 赋值
function fn(function() {}, num) {} // 函数作为参数
// 函数作为返回值
function var() {
return function() {
... ...
}
}
只用“表达式”,不用“语句”
"表达式"(expression)是一个单纯的运算过程,总是有返回值;"语句"(statement)是执行某种操作,没有返回值。函数式编程要求,只使用表达式,不使用语句。也就是说,每一步都是单纯的运算,而且都有返回值。
没有副作用
函数式编程强调没有"副作用",意味着函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值。
即所谓的只要是同样的参数传入,返回的结果一定是相等的。
闭包与 柯里化
四、函数封装
普通封装
function add(num1, num2) {
return num1 + num2;
}
add(20, 10); // 30
挂载在对象上
if(typeof Array.prototype.add !== 'function') {
Array.prototype.add = function() {
var i = 0,
len = this.length,
result = 0;
for( i=0; i < len; i++) {
result += this[i]
}
return result;
}
}
[1, 2, 3, 4].add() // 10
修改数组对象的例子,常在面试中被问到类似的,但是并不建议在实际开发中扩展原生对象。与普通封装不一样的是,因为挂载在对象的原型上我们可以通过this来访问对象的属性和方法,所以这种封装在实际使用时会有许多的难点,因此我们一定要掌握好this。