JavaScript函数是JavaScript中非常重要的组成部分,JavaScript编程中,绝大部分时候都是在跟函数打交道。
一、函数调用:
函数调用在JavaScript中共有4种方法:
1、方法调用模式:
当一个函数被当作为某个对象的一个属性时,我们称之为方法。当这个方法被调用的时候,函数执行过程中的this指向当前这个对象。
var obj ={
name:'obj',
sayHello:function(){
console.log('hello world,'+this.name); //hello world,obj
}
}
2、函数调用模式:
当一个函数不是对象的一个属性的时候,这时候直接调用就是函数调用模式。谨记,此时函数中的this指向的是全局对象,而不是外部函数的this。这时候如果需要使用外部函数的this,可以借用that变量传递下。
var add = function(num1,num2){
return num1+num2
};
add(1,2) // 3
var obj = {
value: 2,
innerAdd: function(){
var that = this;
var addValue = function(){
that.value = add(that.value, that.value)
};
addValue(); // 4
}
}
3、构造器调用模式:
如果在一个函数前面带上new来调用,那么实际上将会创建一个连接到这个函数prototype成员的新对象,且this指向也会指到这个新对象上。同时,new前缀也会改变return语句的行为。
var A = function(name){
this.name = name;
}
A.prototype.say = function(){
alert('hello');
}
var a = new A('a');
a.say(); // hello
// new操作符具体做了什么?
Function.prototype.new = function(){
// 创建一个新对象,并继承于构造函数的原型对象
var that = Object.create(this.prototype);
// 调用构造器函数,绑定this到新建的这个对象
var other = this.apply(that, arguments);
// 返回语句的表现
return (typeof other === 'object' && other) || that;
}
4、Apply/Call调用模式:
apply/call方法可以让我们可以构建一串参数传递给被调用函数。允许我们选择指定this指向。apply方法会接受2个参数,第一个参数是要绑定给this的值,第二个参数是需要传递的参数数组(call功能与apply一致,只是第二个参数不是数组,而是展开独立的参数)。
var arr = [1,2];
add.apply(null, arr); // 3
var obj = {
name:'beauty'
};
var sayName = function(){
console.log(this.name)
};
sayName.apply(obj); // beauty
二、作用域:
1、变量作用域
变量的作用域无非就是2种:局部作用域和全局作用域。全局作用域就是最外层的函数范围,任何内部的函数都可以访问该变量;而局部作用域则一般在某个固定的代码片段内才能访问到。
// 全局变量
var outer = 123;
var innerfn = function(){
console.log(outer);
}
innerfn() // 123
// 局部变量
var innerfn = function(){
var inner = 456;
}
innerfn();
console.log(inner) // ReferenceError: innerVar is not defined
2、函数作用域&提前声明
在ES6之前,JavaScript之前是不像c语言一样存在块作用域的,只存在函数作用域:变量在声明他们的函数体以及这个函数体嵌套的任意函数体内都有定义的。如下代码:
function check(n){
var a = 1;
if(n>2){
var b = 2;
}
console.log(a) ; // 1
console.log(b); // undefined
}
check(1)
再来看一下变量提前声明的妙处:
function prevDef(){
console.log(innerVar); // undefined
var innerVar = 2;
console.log(innerVar); // 2
}
prevDef();
上面代码,可以看出,在函数体内,即使是在第二行声明的变量,在第一行访问,也只是会打印出undefined。这是因为,js引擎在解释阶段就会把所有该函数体内的变量统一提升到顶部声明,到对应地方再对这个声明的变量进行赋值。
不过,此时有必要说一下ES6中的let 和 const。与var不一样,let 和 const是支持块作用域的。而且let和const也不支持变量提升,而是会生成个临时死区,提前访问会报错。来看下代码:
function check(n){
let a = 1;
if(n>2){
let b = 2;
}
console.log(a) ; // 1
console.log(b); // Uncaught ReferenceError: b is not defined
}
check(1)
=================================
function es6Def(){
console.log(innerVar); // Uncaught ReferenceError: innerVar is not defined
let innerVar = 2;
console.log(innerVar); // 中断了
}
es6Def()
因此,在JavaScript中,我们应当尽可能地将变量声明放在函数体的顶部,避免一些不必要出现的问题。
3、作用域链
每一段JavaScript代码(全局代码或函数)都有一个与之关联的作用域链。这个作用域连是一个对象列表或者链表,这组对象定义了这段代码“作用域中”的变量。当JavaScript需要查找变量x的值的时候(这个过程称作“变量解析”),它会从链中的第一个对象开始查找,如果这个对象有一个名为x属性,则会直接使用这个属性的值,如果第一个对象中不存在,则会继续寻找下一个对象,依次类推。如果作用域链上没有任何一个对象含有属性x,则抛出错误(ReferenceError)异常。
不同的层级作用域上对象的分布
在JavaScript的最顶层(也就是不包含任何函数定义内的代码)=》作用域链由一个全局对象组成。
在不包含嵌套的函数体内,作用域链上有两个对象 =》第一个是定义函数参数和局部变量的对象;第二个是全局对象。
在一个嵌套的函数体内,作用域链上至少有三个对象 =》当调用这个函数时,它创建一个新的对象来存储它的局部变量,它实际上保存在同一个作用域链。
三、闭包:
作用域的好处就是内部函数可以访问定义在它们外部函数的参数和变量。闭包我们可以理解为是一个函数,一个可以访问其他函数内部变量的函数。
我们来看下代码:
function calCount() {
var ncount = 0;
function countAdd() {
ncount++;
console.log(ncount);
}
return countAdd;
}
var count = calCount();
count(); // 1
count(); // 2
count(); // 3
上面的countAdd就可以理解为是一个闭包。这个函数可以访问calCount函数内部的ncount变量,让其始终保持在内存中运行着。
从上面可以看出,闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来。这显然类似于面向对象编程。在面向对象编程中,对象允许我们将某些数据(对象的属性)与一个或者多个方法相关联。
闭包的2个很经典的应用:
1、依赖于外部参数来返回一个操作函数:
function closure(nodes){
for(var i = 0;i<nodes.length;i++){
nodes[i].onclick = function(i){
return function(){
alert(i);
}
}(i)
}
}
2、利用闭包来模拟私有化模块
var Closure = function(){
var value = 1;
function changeValue(flag){
value += flag;
}
return {
increment: function(){
changeValue(1);
}
,decrement: function(){
changeValue(-1);
}
,getValue: function(){
return value;
}
}
}();
Closure.getValue(); // 1
Closure.increment();
Closure.increment();
Closure.decrement();
Closure.getValue(); // 2
上面代码实现了一个简单的模块,value作为私有变量放在Closure对象里面,只能通过getValue访问,increment/decrement来修改。
3、闭包的性能考虑:
如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。
例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用时,方法都会被重新赋值一次(也就是,每个对象的创建)。
四、柯里化函数:
柯里化本质上就是将一个依赖多个输入参数的函数变为一个依赖一个参数的函数,通用的实现方式代码如下:
const currying = function(fn,...args){
let arg = args;
return function(...a){
return fn.apply(null,arg.concat(a))
}
}
主要应用场景便是:生成特定应用的函数。举个最简单的例子:
// 假设,我们应用场景中有一个map方法,该方法接受2个参数,执行逻辑如下:
function map(handle,arr){
return arr.map(handle)
}
function add (x){
return x+1;
}
// 当我们实际用起来就是这样:
map(add,[1,2,3])
map(add,[1,2,3])
map(add,[1,2,3])
我们发现这边的add似乎重复了,于是我们柯里化一下:
const mapAdd = currying(map,add);
就变成了
mapAdd(1,2,3)
mapAdd(1,2,3)
mapAdd(1,2,3)