js函数使用详细讲解!

使用函数

定义函数

在js中有三种定义函数的方法,使用function语句,使用Function()构造函数,定义函数直接量。

声明函数

  • 在js中可以使用function语句声明函数。用法如下:
function funName([args]) {
    
    
	//statements
}
  • funName是函数名,与变量名一样都必须是js合法的标识符。在函数名之后是一个由小括号包含的参数列表,参数之间以都好间隔。参数是可选的,没有数量限制。
  • 作为标识符,参数仅在函数体没被访问,参数是函数作用域的私有成员。调用函数时,通过为函数传递值,然后使用参数获取外部传入的值,并在函数体内干预函数的执行。
  • 在小括号之后是一个大括号,大括号内包含的语法就是函数体结构的主要内容。在函数体内,大括号必不可少!
  • 实例:function语句必须包含函数名、小括号、大括号,其他代码都可以省略,因此,最简单的一个函数就是一个空函数:
function funName(){
    
    }

如果使用匿名函数,则可以省略函数名。

function (){
    
    } //匿名空函数
  • 提示:var语句和function()语句都是声明语句,他们声明的变量和函数都在js预编译时被解析,也被称为变量提升和函数提升。在预编译期,js引擎会为function创建上下文,定义变量对象,同时把函数内所有形参、私有变量、嵌套函数作为属性注册到变量对象上。

构造函数

使用Function()构造函数可以快速生成函数。具体语法:

var funName = new Function(p1,p2,p3,...pn,body);
  • Function()的参数类型都是字符串,p1~pn表示所创建函数的名称列表,body表示所创建的函数的函数结构体语句,在body语句之间以分号分隔。
  • 实例1:可以省略所有参数,仅传递一个字符串,用来表示函数体。
var f = new Function("a","b","return a+b"); // 通过构造函数来克隆函数结构

在上面代码中,f就是所创建函数的名称。同样是定义函数,使用function语句可以设计相同结构的函数:

function f(a,b) {
    
    
	return a + b;
}
  • 实例2:使用Function()构造函数可以不指定任何参数,创建一个空函数结构体
var f = new Function();
  • 实例3:在Function()构造函数参数中,p1~pn是参数名称的列表,即p1不仅能够代表一个参数,还可以是一个逗号分隔开的参数列表。下面定义的方法是等价的:
var f = new Function("a","b","c","return a+b+c");
var f = new Function("a,b,c","return a+b+c");
var f = new Function("a,b","c","return a+b+c");
  • 注意:使用Function()构造函数不是很常用,因为一个函数体通常会包含很多代码,如果将这些代码以一行字符串的形式传递,代码的可读性差。
  • 提示
    • 使用Function()构造函数可以动态的创建函数,它不会把用于限制在function语句预声明的函数体中。
    • 使用Function()构造函数能够把函数当做表达式来使用,而不是当做一个结构,因此使用起来更灵活。
    • 但是,缺点就是,Function()构造函数在执行期被编译,执行效率非常低,一般不推荐使用

函数直接量

函数直接量也称为匿名函数,即函数没有名字,仅包含function关键字、参数和函数体。具体用法如下:

function([args]){
    
    
	//statements
}
  • 实例1:下面代码定义了一个函数直接量。
function(a,b){
    
    
	return a + b;
}

上面代码中,函数直接量与使用function语句定义函数结构基本相同,它们的结构都是固定的。但是函数直接量没有指定函数名,而直接利用关键字function来表示函数的结构,这种函数也被称为匿名函数

  • 实例2:匿名函数就是一个表达式,即函数表达式,而不是函数结构的语句。下面把匿名函数作为一个值赋值给f:
//把函数作为一个值直接赋值给变量f
var f = function(a,b) {
    
    
	return a + b;
};

当把函数结构作为一个值赋值给变量之后,变量就可以作为函数被调用,此时比阿娘就是指向那个匿名函数。

console.log(f(1,2)); // 3
  • 实例3:匿名函数作为值,可以参与更复杂的表达式运算。针对上面实例可以使用一下代码完成函数定义和调用一体化操作。
console.log(function(a, b) {
    
    
    return a + b;
}(4, 5)); // 9

定义嵌套函数

js允许函数相互嵌套,因此可以定义复杂的嵌套结构函数。

  • 实例:嵌套函数只能在函数内可见,函数外不允许直接访问、调用。
function f(x, y) {
    
    
    function e(a, b) {
    
    
        return a * b;
    }
    console.log(e(3, 6)); // 18
    return e(3, 6) + y;

}
console.log(f(3, 6)); // 24

调用函数

js提供4中函数调用模式:函数调用、方法调用、使用call或apply动态调用、使用new间接调用,下面分别介绍。

函数调用

  • 在默认状态下,函数是不会被执行的。使用小括号“()”可以激活并执行函数。在小括号中可以包括零个或多个参数,参数之间通过逗号进行分隔。
  • 实例1:在下面实例中,使用小括号调用函数,然后直接把返回值传入函数,进行第二次调用,这样可以节省两个临时变量。
function f(x, y) {
    
    
    return x * y;
}
console.log(f(f(3, 4), f(5, 7))) //420
  • 实例2:如果函数返回值为一个函数,则在调用时可以使用多个小括号反复调用。
function f(x, y) {
    
    
    return function() {
    
    
        return x * y;
    }
}

console.log(f(7, 8)()); // 56
  • 实例3:设计递归调用,即在函数内调用自身,这样可以反复调用,但最终返回的都是函数自身。
function f() {
    
    
	return f;
}
console.log(f()()()()()());

当然,上述设计方法在实际开发中没有任何应用价值,不建议使用。

函数的返回值

  • 函数提供两个结构实现与外界的交互,其中参数作为入口,接收外界信息;返回值作为出口,把运算结果反馈给外界。
  • 在函数体内,使用return语句可以设置函数的返回值。一旦执行return语句,将停止函数的执行 ,并运行和返回return后面的表达式的值。如果函数不包含return语句,则执行完函数体内每条语句后返回undefined值。
  • 提示:js是一种弱类型语言,所以函数对接收和输出的值都没有类型限制,js也不会自动检测输入和输出类型。
  • 实例1:下面代码定义函数的返回值为函数
function f(){
    
    
	return function(x, y){
    
    
		return x + y;
	}
}
  • 实例2:函数的参数没有限制,但是返回值只能是一个,如果要输出多个值,可以通过数组或对象进行设计。
function f() {
    
    
	var a = [];
	a[0] = true;
	a[1] = function(x,y){
    
    
		return x + y;
	};
	a[2] = 123;
	return a;
}

在上面代码中,函数返回值为数组,该数组包含3个元素,从而实现一个return语句,返回多个值的目的。

  • 实例3:在函数体内可以包含多条return语句,但是仅能执行一条return语句,因此在函数内可以使用分支结构决定函数返回值,或者使用return语句提前终止函数运行。
function f(x, y) {
    
    
    // 如果参数为非数字类型,则终止函数
    if (typeof x != "number" || y != "number") {
    
    
        return;
    }
    if (x > y) {
    
    
        return x - y;
    }
    if (x < y) {
    
    
        return y - x;
    }
    if (x + y <= 0) {
    
    
        return x + y;
    }
}

方法调用

当一个函数被设置为对象的属性值时,称之为方法。使用点语法可以调用一个方法。

  • 实例:下面实例创建一个obj对象,它有一个value属性和一个increment方法。increment方法接收一个可选参数,如果该参数不是数字,那么默认使用数字1。
var obj = {
    
    
    value: 0,
    increment: function(inc) {
    
    
        this.value += typeof inc === 'number' ? inc : 1;
    }
}
obj.increment();
console.log(obj.value); // 1
obj.increment(2);
console.log(obj.value); // 3

使用点语法可以调用obj的方法increment,然后通过increment方法改写value属性的值。在increment方法中可以使用this访问obj对象,然后使用obj.value方式读写value属性值。

使用call和apply调用

call和apply是Function的原型方法,它们能够将特定函数当做一个方法绑定到指定对象上,并进行调用。具体用法如下:

function.call(thisobj,args...)
function.apply(thisobj,[args])
  • function表示要调用的函数;参数thisobj表示绑定对象,即this指代的对象;参数args表示要传递给被调用函数的参数。call方法可以接受多个参数列表,而apply只能接受一个数组或者伪数组,数组元素将作为参数列表给被调用的函数。
  • 实例1:下面实例使用call动态调用函数f,并传入参数值3和4,返回运算值:
function f(x, y) {
    
    
    return x + y;
}
console.log(f.call(null, 3, 4)); //7

在上面实例中,f是一个简单的求和函数,通过call方法把函数绑定到null身上是,以实现动态调用函数f,同时把参数3和4传递给函数f,返回值为7。实际上,f.call(null,3,4)等价于null.m(3,4)。

  • 实例2:实例1使用call调用,实际上也可以使用apply方法来调用函数f。
function f(x, y) {
    
    
    return x + y;
}
console.log(f.apply(null, [3, 4])); //7
  • 注意:如果把一个数组或伪数组的所有元素作为参数进行传递,使用apply方法就非常便利。
  • 实例3:下面使用apply方法设计一个求最大值的函数。
function max() {
    
    
    var m = Number.NEGATIVE_INFINITY;
    for (var i = 0; i < arguments.length; i++) {
    
    
        if (arguments[i] > m) {
    
    
            m = arguments[i];
        }
    }
    return m;
}
var a = [23, 45, 2, 46, 62, 45, 56, 63];
var m = max.apply(Object, a);
console.log(m); //63

在上面实例中,设计定义一个函数max(),用来计算所有参数的最大参数。首先通过apply方法动态调用max()函数,然后把它绑定Object对象的一个方法,并把包含多个值的数组传递给它,最后经过max()计算后的最大数组元素。

如果使用call方法,就需要把数组所有元素全部读取出来,在逐一传递给call方法,显然这种方法不是很方便。

  • 实例4:也可以动态调用Math的max()方法来计算数组的最大值元素。
var a = [23, 45, 2, 46, 62, 45, 56, 63];
var m = Math.max.apply(Object, a);
console.log(m); //63
  • 实例5:使用call和apply方法可以把一个函数转换为指定对象的方法,并在这个对象上调用该方法。当函数动态调用之后,这个对象的临时方法就不存在了。
function f() {
    
    
    return "函数f";
}
var obj = {
    
    };
f.call(obj);
console.log(obj.f());
  • 注意:call和apply方法的主要功能如下:
    • 调用函数。
    • 修改函数体内的this指代对象。
    • 为对象绑定方法。
    • 跨越限制调用不同类型的方法。

使用new调用

使用new命令可以实例化对象,这是他的主要功能,但是在创建对象的过程中会激活并运行函数,因此,使用new命令可以间接调用函数。

  • 注意:使用new调用函数时,返回的是对象,而不是return的返回值。如果不需要返回值,或者return的返回值的对象,则可以选用new间接调用函数。
  • 实例:下面实例简单演示了如何用new命令,把传入的参数值显示在控制台。
function(x,y) {
    
    
	console.log("x="+x+",y="+y);
}
new f(3,4);

函数参数

参数是函数对外联系的唯一入口,用户只能通过函数来控制函数的运行。

形参和实参

函数的参数包括以下两种类型:

  • 形参:在定义函数时,声明的参数变量仅在函数内部可见。
  • 实参:在调用函数时,实际传入的值。
  • 实例1:定义js函数时,可以设置零个或多个参数。
function f(a,b) {
    
    
	return a + b;
}
var x = 1, y = 2;
console.log(f(x,y));

上面的实例中,a、b就是形参,而在调用函数时,向函数传递的变量x、y就是实参。

一般情况下,函数的形参和实参数量应该相同,但js没有硬性要求,可以不同。

  • 实例2:如果函数实参数量小于形参,那么多出来的值默认为undefined。
(function(a, b) {
    
    
	console.log(typeof a);
  console.log(typeof b); // 返回undefined
}(1));
  • 实例3:如果函数实参数量多于形参数量,那么多出来的实参就不能够通过形参来访问,函数会忽略掉多于的实参。在下面这个实例中,实参3、4就被忽略了。
(function(a, b) {
    
    
	console.log(typeof a);
  console.log(typeof b); 
}(1, 2, 3, 4));
  • 提示:在实际应用中,经常会出现实参数量小于形参数量的情况,但在函数中依然可以使用这些形参,因为在定义函数的时候,已经对他们进行了初始化,设置了默认值。在调用函数时,如果用户不传递或少传递参数,则函数会采用默认值。而形参数量少于实参的情况比较少见,这种情况一把发生在参数数量不确定的函数中。

获取参数个数

  • 使用arguments对象的length属性可以获取函数的实参个数。arguments对象只能在函数体内可见,因此arguments.length也只能在函数体内使用。
  • 使用函数对象的length属性可以获取函数的形参个数。该属性为只读属性,在函数体内、体外都以使用。
  • 实例:下面实例设计一个checkArg()函数,用来检测一个函数的形参和实参是否一致,如果不一致,抛出异常。
function checkArg(a) {
    
    
    if (a.length != a.callee.length) {
    
    
        throw new Error("实参和形参不一致");
    }
}

function f(a, b) {
    
    
    checkArg(arguments);
    return ((a * 1 ? a : 0) + (b * 1 ? b : 0)) / 2;
}
console.log(f(6)); //报错:实参和形参不一致

使用arguments对象

arguments对象表示函数的实参集合,仅能够在函数体内可见,并可以直接访问。

  • 实例1:下面实例中,函数没有定义形参,但是在函数体内通过arguments对象可以获取调用函数时的每个实参值。
function f() {
    
    
    for (var i = 0; i < arguments.length; i++) {
    
    
        console.log(arguments[i]);
    }
}

f(3, 4, 5, 6); // 3,4,5,6
  • 注意:arguments对象是一个伪类数组,不能够继承Array的原型方法。可以使用数组下标的形式访问每个实参,如果arguments[0]表示第一个实参。下标值从0开始,直到arguments.length-1。其中length是arguments对象的属性,表示函数包含的实参个数。同时,arguments对象允许更新其包含的实参值。
  • 实例2:在下面实例中使用for循环遍历arguments对象,然后把循环变量的值传入arguments,以改变实参值。
function f() {
    
    
    for (var i = 0; i < arguments.length; i++) {
    
    
        arguments[i] = i;
        console.log(arguments[i]);
    }
}
f(3, 3, 4, 6); // 返回0,1,2,3
  • 实例3:通过修改length属性值,也可以改变函数的实参个数。当length属性值增大时,则增加的实参为undefined;如果length属性值减小,则会丢弃length长度值之后的实参值。
function f() {
    
    
    arguments.length = 2;
    for (var i = 0; i < arguments.length; i++) {
    
    
        console.log(arguments[i]);
    }
}
f(3, 3, 6); // 3,3

使用callee

callee是arguments对象的属性,它引用当前arguments对象所在的函数。使用该属性可以在函数体内调用函数自身。在匿名函数中,callee属性比较有用。例如,利用它可以设计递归调用。

  • 实例:在下面实例中,使用arguments.callee获取匿名函数,然后通过函数的length属性获取函数形参个数,最后比较实参个数,以检测用户传递的参数是否符合要求。
function f(x, y, z) {
    
    
    var a = arguments.length;
    var b = arguments.callee.length;
    if (a != b) {
    
    
        throw new Error("传递的参数不匹配");
    } else {
    
    
        return x + y + z;
    }
}
console.log(f(3, 4, 5)); // 12
  • 提示:arguments.callee等价于函数名,在上面实例,arguments.callee等于f。

应用arguments对象

在实际开发中,arguments对象非常有用。灵活使用arguments对象,可以提升使用函数的灵活性,增强函数在抽象编程中的适应能力的纠缠功能。下面结合几个典型实例展示arguments的应用。

  1. 技巧一,使用arguments对象能够增强函数应用的灵活性。例如,如果函数的参数个数不确定,或者函数的参数个数很多,而又不想逐一定义每一个形参,则可以省略定义参数,直接在函数体内使用arguments对象来访问调用函数的实参值。
  • 实例1:下面实例定义了一个求平均值的函数,该函数借助arguments对象来计算参数的平均值。在调用函数时,可以传入任意多个参数。
function avg() {
    
    
    var num = 0,
        l = 0;
    for (var i = 0; i < arguments.length; i++) {
    
    
        if (typeof arguments[i] != "number") {
    
    
            continue;
        }
        num += arguments[i];
        l++;
    }
    num /= l;
    return num;
}
console.log(avg(1, 2, 3, 4)); // 2.5
console.log(avg(1, 2, "3", 4)) // 2.3333333333333335
  1. 技巧二,arguments对象是伪数组,不是数组,可以通过length属性和中括号语法来遍历或访问实参的值。不过,通过动态调用的方式,也可以使用数组的方法,如push、pop、slice等。
  • 实例2:使用arguments可以模拟重载。实现方法:arguments.length属性值判断实际参数的个数和类型,决定执行不同代码。
function sayHello() {
    
    
    switch (arguments.length) {
    
    
        case 0:
            return "Hello";
        case 1:
            return "Hello, " + arguments[0];
        case 2:
            return (arguments[1] == "cn" ? "你好," : "Hello,") + arguments[0];
    };
}
console.log(sayHello()); // Hello
console.log(sayHello("Alex")); // Hello, Alex
console.log(sayHello("Alex", "cn")); // 你好,Alex
  • 实例3:下面实例使用动态的方法,argument是对象调用数组方法slice(),可以把函数的参数对象转换为数组。
function f() {
    
    
    return [].slice.apply(arguments);
}
console.log(f(1, 2, 3, 4, 5, 6)); //[1,2,3,4,5,6]

函数作用域

js支持全局作用域和局部作用域。这个局部作用域,也就是函数作用域,局部变量在函数内可见,也称为私有变量。

词法作用域

作用域(scope)表示变量的作用范围,可见区域,包括词法作用域执行作用域

  • 词法作用域:根据代码的结构关系来确定作用域。词法作用域是一种静态的词法结构,js解析器主要根据词法结构确定每个变量的可见性和有效区域。
  • 执行作用域:当代码被执行时,才能够确定变量的作用范围和可见性。与词法作用域相比,它是一种动态作用域,函数的作用域会因为调用对象不同而发生变化。
  • 注意:js支持词法作用域,js函数只能运行在被预先定义好的词法作用域里,而不是被执行的作用域里。

执行上下文和活动对象

  • js代码是按照顺序从上到下被解析的,当然js引擎并非逐行的分析和执行代码,而是逐段的去分析和执行。当执行一段代码时,先进行预处理,如变量提升函数提升等。

  • js可执行代码包括3种类型:全局代码函数代码eval代码。每执行一段可执行代码,都会创建对应的执行上下文。在脚本中可能会存在大量的可执行代码段,所以js引擎先创建执行上下文栈,来管理脚本中的执行上下文。

  • 提示:执行上下文是一个专业术语,比较抽象,实际上就是内存中开辟的一块独立运行的空间。执行上下文栈相当于一个数组,数组元素就是一个个独立的执行上下文区域。

  • 当js开始解释程序时,最先遇到的是全局代码,因此在初始化程序的时候,首先向执行上下文栈压入一个全局执行上下文,并且只有当整个应用程序结束的时候,全局执行上下文才会被清空。

  • 当执行一个函数的时候,会创建一个函数的执行上下文,并且压入到执行上下文栈,当函数执行完毕,会将函数的执行上下文从栈中弹出。

  • 每个执行上下文都有3个基本属性:变量对象作用域链this。下面将重点介绍变量对象。

  • 变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。js代码不能直接访问该对象,但是可以访问该对象的成员(如:arguments)。不同代码段中的变量对象也不同,简单说明如下:

1. 全局上下文的变量对象

全局上下文的变量对象,初始化是全局对象。

全局对象是预定义的对象,作为js的全局函数和全局属性的占位符。通过全局对象,可以访问其他所有预定义的对象、函数和属性。

在客户端js中,全局对象是window对象,通过window对象的window属性指向自身。

  • 实例1:下面代码演示了在全局作用域中用于中声明变量b,并赋值,然后通过window对象的属性b来读取这个全局变量值。同时演示了使用this访问window对象,使用this.window同样可以访问window对象。
var b = true;
console.log(window.b); // true
this.window.b = false;
console.log(this.b); // false

2. 函数上下文的变量对象

变量对象是ECMAScript规范术语。在一个执行上下文中,变量对象才被激活。只有激活的变量对象,其各种属性才能被访问。

在函数执行上下文中,变量对象尝尝被称为活动对象,两者意思相同。活动对象是在进入函数上下文时被创建,初始化时只是包括Arguments对象。它通过函数的arguments属性访问,arguments属性值为Arguments对象。

执行上下文的代码处理可以分为两个阶段:分析执行,简单说明如下。

  • 【执行过程】
  • 第一步,进入执行上下文。当进入执行上下文时,不会执行代码,只进行分析。此时变量对象包括:
  • 函数的所有形参(如果是执行上下文)——由名称和对应值组成的一个变量对象的属性被创建。如果没有实参,属性值设为undefined。
  • 函数声明——由名称和对应值(函数对象)组成的一个变量对象的属性被创建。如果变量对象已经存在相同名称的属性,则会完全替换这个属性。
  • 变量声明——由名称和对应值(undefined)组成的一个变量对象的属性被创建。如果变量名称与已经声明的形参或函数相同,则变量声明不会覆盖已经存在的这类属性。
  • 实例2:在进入函数执行上下文时,会给变量对象添加形参、函数声明、变量声明等初始的属性值。下面代码简单演示了这个阶段的处理过程:
function f(a) {
    
     // 声明外部函数
    var b = 1; // 声明局部变量,并赋值1
    function c() {
    
     }; // 声明内部函数
    var d = function () {
    
     }; // 声明局部变量,并赋值为匿名函数
    b = 2; // 修改变量b的值为2
}
f(3); // 调用函数,并传入实参值3

在进入函数执行上下文后,活动对象的结构模拟如下。

AO = {
    
    
	arguments: {
    
    
		0: 3,
		length: 1
	},
	a: 3,
	b: undefined,
	c: function c(){
    
    },
	d: undefined
}
  • 第二步,执行代码。在代码执行阶段会按照顺序执行代码,这时可能会修改变量对象的值。
  • 实例3:在执行代码阶段,可能会修改变量对象的属性值。针对上面实例,当代码执行完后,活动对象的结构模型如下:
AO = {
    
    
	arguments: {
    
    
		0: 3,
		length: 2
	},
	a: 3,
	b: 1,
	c: function c(){
    
    },
	d: function(){
    
    },
}

作用域链

  • js作用域属于静态概念,根据语法结构来确定,而不是根据执行来确定。作用域是js提供的一套解决标识符的访问机制——js规定每一个作用域都有一个与之相联的作用域链。
  • 作用域链用来在函数执行时求出标识符的值。该链中包含多个对象,在对标识符进行求值过程中,会从链首的对象开始,然后依次查找后面的对象,直到在某个对象中找到与标识符名称相同的属性。如果在作用域的顶端(全局对象)中仍然没有找到同名的属性,则返回undefined的属性值。
  • 注意:在每个对象中进行属性查找的时候,还会使用该对象的原型域链。在一个执行上下文中,与其关联的作用域链只会被with语句和catch子句影响。
  • 实例1:下面实例中,通过多层嵌套函数设计一个作用域,在最内层函数中可以逐级访问外层函数的私有变量。
var a = 1;
(function(){
    
    
	var b = 2;
	(function(){
    
    
		var c = 3;
		(function(){
    
    
			var d = 4;
			console.log(a + b + c + d); // 返回10
		})()
	})()
})()

在上面代码中,js引擎首先在最内层活动对象中查询属性a、b、c和d,从中只找到d,并获得了它的值4,然后沿着作用域链,再上一层活动对象中继续查找属性a、b、c,从中找到了属性c,获得了它的值3…依次类推,直到找到所有的变量值为止。

  • 下面结合一个实例,通过函数的创建和激活两个阶段来介绍作用域链的创建过程。
  • 函数创建

函数的作用域在函数定义的时候已经确定。每个函数都有一个内部属性[[scope]],当函数创建的时候,[[scope]]保存所有父变量对象的引用,[[scope]]就是一个层级链。注意,[[scope]]并不代表完整的作用域链。例如:

function f1() {
    
    
	function f2() {
    
    
		//...
	}
}

在函数创建时,每个函数的[[scope]]如下,其中globalContext表示全局上下文,VO表示变量对象,f1Context表示f1的上下文,AO表示活动对象

f1.[[scope]] = [
	globalContext.VO
];
f2.[[scopr]] = [
	f1Context.AO,
	glabalContext.VO
]
  • 函数激活

当函数激活时,进入函数上下文,创建VO/AO后,就会将活动对象添加到作用域链的前端。这时如果命名执行上下文的作用域为Scope,则可以表示为:

Scope = [AO].concat([[Scope]]);

至此,作用域链创建完毕。

  • 实例2:下面实例结合变量对象执行上下文栈,总结函数执行上下文中作用域和变量对象的创建过程。
var g = "global scope";
function f() {
    
    
	var l = "local scope";
	retune l;
}
f();

执行过程

第一步,f函数被创建,保护作用域链到内部属性[[scope]]。

f1.[[scope]] = [
	globalContext.VO
];

第二步,执行f函数,创建f函数的执行上下文,f函数的执行上下文被压入执行上下文栈。

ECStack = [
	fContext,
	globalContext
]

第三步,f函数并不立即执行,开始做准备工作。准备工作包括以下三项:

  • (1)复制函数f的[[scope]]属性,创建作用域。
fContext = {
	Scope: f.[[scope]],
}
  • (2)使用arguments创建活动对象,然后初始化活动对象,加入形参、函数声明、变量声明。
fContext = {
    
    
	AO: {
    
    
		arguments: {
    
    
			length: 0
		},
		l: undefined
	}
}
  • (3)将活动对象压入f作用域顶端。
fContext = {
    
    
	AO: {
    
    
		arguments: {
    
    
			length: 0
		},
		l: undefined
	},
	Scope: [AO,[[scope]]]
}

第四步,准备工作做完,开始执行函数。随着函数的执行,修改AO的属性值。

fContext = {
    
    
	AO: {
    
    
		arguments: {
    
    
			length: 0
		},
		l: "local scope"
	},
	Scope: [AO, [[scope]]]
}

第五步,查找本地变量l的值,然后返回l的值。

第六步,函数执行完毕,函数上下文从执行上下文栈中弹出。

ECStack = [
	globalContex
];

this和调用对象

  • js函数的作用域是静态的,但是函数的调用却是动态的。由于函数可以在不同的运行环境内执行,因此js在函数体内定义了this关键字,用来获取当前的运行环境。
  • this是一个指针型变量,它动态调用当前的运行环境。具体来说,就是调用函数的对象。调用对象是可以访问的js对象,而执行上下文的变量对象是一个不可访问的抽象概念。同时,在一个执行上下文中会存在多个可调用函数,但是只一有个变量对象。
  • 实例1:下面实例会在全局上下文中声明一个变量x,初始化为1。然后在obj对象内定义一个属性x,初始化为2。使用函数f检测不同运行环境下x值的变化,以此检测this指针的引用对象。
var x = 1;
var obj = {
    
    
    f: function() {
    
    
        console.log(this.x);
    },
    x: 2,
};
obj.f(); // 2
var f1 = obj.f;
f1(); // 1

在上面代码中,obj.f()表示在obj对象上调用f函数,则调用对象为obj,此时this就执行obj,this.x就等于obj.x,即返回结果为2。若把obj.f赋值给变量f1,然后在全局上下文中调用f1,则f函数体的运行环境在全局上下文中执行,此时this就指向window,this.x就等于window.x,即返回结果1。

  • 实例2:this总是指代函数的当前运行环境。针对实例1的代码,如果使用下面三种方式调用f函数,会发现返回值都是1。
var x = 1;
var obj = {
    
    
    f: function() {
    
    
        console.log(this.x);
    },
    x: 2,
};
(obj.f = obj.f)(); // 1
(false || obj.f)(); // 1
(obj.f, obj.f)(); // 1
  • 在上面代码中,小括号左侧都是一个表达式,表达式的值都是obj.f,而实例1中可以看到使用obj.f()调用函数f,返回值是2。为什么现在换一种表达方法返回值都是1呢?
  • 问题的关键是如何正确理解“运行环境”。上面三个表达式中,obj.f = obj.f是赋值表达式,把obj.f赋值被obj.f,obj.f是一个地址,把地址赋值给obj.f属性,表达式的运行环境在全局上下文中,所以此时函数f内的this就指向了全局上下文的调用对象window。
  • false || obj.f是一个逻辑表达式,左侧操作数为false,则运算右侧操作数,返回obj.f对的值,即引用地址。由于这个逻辑表达式运算发生在全局作用域内,此时的f函数内this就指向了全局对象。
  • obj.f,obj.f是一个逗号运算符,逗号左侧和右侧的obj.f都是一个地址,都被运算一次,最后返回第二个操作数的值,即返回引用地址。由于这个操作发生在全局作用域内,所以f函数内this也指向了全局对象。
  • 但是,对于下面像是的调用不会返回1,而是返回2,即this指向obj对象。因为小括号是一个运算符,它仅是一个逻辑分隔符,不执行运算,不会产生运行环境。当使用小括号调用函数时,此时生成的运行环境就是obj了。
(obj.f)();

比较3种函数的作用域

在js中,创建函数的方法有3种,即使使用function语句、使用function表达式和使用Function构造函数。下面分别使用3种方法定义一个空函数。

function f(){
    
    }
var f = function () {
    
    }
var f = new Function()

通过这3种方法创建的函数对象的[[scope]]属性值会有所不同,进而影响函数执行过程中的作用域链。

  • 使用function语句声明的函数对象是在进入执行上下文时的变量初始化过程中创建的。该对象的[[scope]]属性值是他被创建时的执行上下文对应的作用域链。
  • 使用function表达式定义的函数对象是在该表达式被执行的时候创建的。该对象的[[scope]]属性值与使用function声明创建的对象一样。
  • 使用Function构造器声明一个函数通常使用两种方式。常用格式是var funName = new Function(p1,p2,…,pn,body),其中p1,p2,…,pn表示的是该function的形式参数,body是function的内容。使用该方法的function对象是在构造函数被调用的时候创建的。该对象的[[scope]]属性值总是一个只包含全局对象的作用域链。

函数标识符

在函数结构中,一般包含以下类型的标识符。

  • 函数参数。
  • arguments。
  • 局部变量。
  • 内部函数。
  • this。

其中this和arguments是系统默认标识符,不需要特别声明。这些标识符在函数体内的优先级是:this>局部变量>形参>arguments>函数名。

  • 实例1:下面实例将在函数结构内显示函数结构的字符串。
function f() {
    
    
	console.log(f)
}
f(); // 调用函数,返回函数f
  • 实例2:如果定义形参f,则同名情况下参数变量的优先级会大于函数的优先级。
function f(f) {
    
    
	console.log(f)
}
f(true); // 返回true
  • 比较形参与arguments属性的优先级。
function f(arguments) {
    
    
	console.log(typeof arguments)
}
f(true); // 返回boolean,而不是arguments的类型object

上面实例说明了形参变量会优先于arguments属性对象。

  • 实例4:比较arguments属性与函数名优先级。
function arguments(){
    
    
	console.log(typeof arguments)
}
arguments(); // 返回arguments属性的类型object
  • 实例5:比较局部变量和形参变量的优先级。
function f(x){
    
    
	var x = 10;
	console.log(x);
}
f(5); //10

上面实例说明函数内部局部变量要优先于形参变量的值。

  • 实例6:如果局部变量没有赋值,则会选择形参变量。
function f(x) {
    
    
	var x = x;
	console.log(x);
}
f(5); // 5

如果从局部变量与形参变量之间的优先级来看,则var x = x左右两侧都应该是局部变量。由于x初始化为undefined,所以该表达式就表示把undefined传递给自身。但是从上面实例来看,这说明的是由var语句声明的局部变量,而右侧的是形参变量。也就是说,当局部变量没有初始化时,应用的是形参变量优先于局部变量。

闭包

闭包是js的重要特性之一,在函数式变成中有着重要的作用,本节将介绍闭包的结构和基本用法。

闭包定义

闭包就是一个能够持续存在的函数上下文活动对象

1.形成原理

  • 函数被调用时,会产生一个临时上下文活动对象。它是函数作用域的顶级对象,作用域内所有私有变量、参数、私有函数等都将作为上下文活动对象的属性而存在。

  • 函数被调用后,在默认情况下上下文对象都会被立即释放,避免占用系统资源。但是,若函数内的私有变量、参数、私有函数被外界引用,则这个上下文对象暂时会继续存在,直到所有外界引用被注销。

  • 但是,函数作用域是封闭的,外界无法访问。那么在什么情况下,外界可以访问到函数内的私有成员呢?

  • 根据作用域链,内部函数可以访问外部函数的私有成员。如果内部函数引用了外部函数的私有成员。同时内部函数又被传给外界,或者对外界开放,那么闭包体就存在,通过内部函数可以储蓄读写外部函数的私有成员。

2.闭包结构

  • 经典的闭包是一个嵌套结构的函数。内部函数引用外部函数的私有成员,同时内部函数又被外界引用,当外部函数被调用后,就形成了闭包。这个函数也称为闭包函数

  • 下面是一个典型的闭包结构:

function f(x) {
    
    
	return function (y) {
    
    
		return x + y;
	};
}
var c = f(5);
console.log(c(6)); // 11

解析过程简单描述如下:

  1. 在js预编译期,声明函数f和变量c,先被词法预解析。
  2. 在js执行期,调用函数f,并传入值5。
  3. 在解析函数f时,将创建执行环境(函数作用域)和活动对象,并把参数和私有变量、内部函数都映射为活动对象的属性。
  4. 参数x的值为5,映射到活动对象的x属性。
  5. 内部函数通过作用域链引用了参数x,但是还没有被执行。
  6. 外部函数被调用后,返回内部函数,导致内部函数被外界变量c引用。
  7. js解析器检测到外部函数的活动对象的属性被外界引用,无法注销该活动对象,于是在内存中继续维持该对象的存在。
  8. 当调用c,即调用内部函数是,可以看到外部函数的参数x存储的值继续存在。这样就可以实现后续运算操作,返回x + y = 5 + 6 = 11.
  • 注意:如下结构形式也可以形成闭包:通过全局变量引用内部函数,实现内部函数对外开放。
var c;
function f(x) {
    
    
	c = function(y) {
    
    
		return x + y;
	};
}
f(5);
console.log(c(6)); // 11

3.闭包变体

除了嵌套函数外,如果外部引用函数内部的私有数组或对象,也容易形成闭包。

var add;
function f(){
    
    
	var a = [1,2,3];
	add = function(x) {
    
    
		a[0] = x * x;
	}
	return a;
}
var c = f();
console.log(c[0]); // 读取闭包内数组,返回1
add(5); // 修改数组
console.log(c[0]); // 读取闭包内数组,返回25
add(10);
console.log(c[0]); // 

与函数相同,对象和数组也是引用型数据。调用函数f,返回私有数组a的引用,即传递给全局变量c,而a是函数f的私有变量,当被调用后,活动对象继续存在,这样就形成了闭包。

  • 注意:这种特殊形式的闭包没有实际应用价值,因为其功能单一,只能作为一个静态的、单向的闭包。而闭包函数可以设计各种复杂的运算表达式,它是函数式编程的基础。

反之,如果返回的是一个简单的值,值传递是直接复制。外部变量c得到的仅是一个值,而不是对函数内部变量的引用。这样当函数调用后,将直接注销活动对象。

function f(x) {
    
    
	var a = 1;
	return a;
}
var c = f(5);
console.log(c);

使用闭包

下面结合实例介绍闭包的简单使用,以加深对闭包的理解。

  • 实例1:使用闭包实现优雅的打包,定义存储器。
var f = function() {
    
    
	var a = [];
	return function(x) {
    
    
		a.push(x);
		return a;
	};
}();
var a = f(1); // 添加值
console.log(a); // 返回值1
var b = f(2); //添加值
console.log(b); // 返回值1、2

在上面实例中,通过外部函数设计一个闭包,定义一个永久的存储器。当调用外部函数生成执行环境之后,就可以利用返回的匿名函数不断地向闭包内的数组a传入新值,传入的值会持续存在。

  • 实例2:在网页中事件处理函数很容易形成闭包。
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script>
        function f() {
     
     
            var a = 1;
            b = function() {
     
     
                console.log("a = " + a);
            };
            c = function() {
     
     
                a++;
            }
            d = function() {
     
     
                a--;
            }
        }
    </script>
</head>

<body>
    <button onclick="f()">生成闭包</button>
    <button onclick="b()">查看a的值</button>
    <button onclick="c()">递增</button>
    <button onclick="d()">递减</button>

</body>

</html>

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4fQaFpyC-1600003964774)(/Users/mac/Desktop/MarkDown /Javascript/js复习笔记/js复习笔记五/1.gif)]

闭包的局限性

闭包的价值是方便在表达式运算过程中存储数据。但是,它的缺点也不容忽视。

  • 由于函数调用后,无法注销调用对象,会占用系统资源,在脚本中大量使用闭包,容易导致内容泄露。解决方法:慎用闭包,不要滥用。
  • 由于闭包的作用,其保存的值是动态的,如果处理不当,容易出现异常或错误。下面结合实例进行具体说明。

函数式运算

函数式编程有两种基本的运算:compose(函数合成)和curry(柯里化)

函数合成

在函数变成中,经常见到如下表达式运算。

a(b(c(x)));

这是“包菜式”多层函数,但是不是很优雅。为了解决函数多层调用的嵌套问题,我们需要用到函数合成。其语法格式如下:

var f = compose(a, b, c);
f(x);

例如:

var compose = function (f, g) {
    
    
	return function(x) {
    
    
		return f(g(x));
	};
};
var add = function (x) {
    
    
	return x + 1;
}
var mul = function (x) {
    
    
	return x * 5;
}
compose(mul, add)(2); // 15

在上面的代码中,compose函数的作用就是组合函数,将函数串联起来执行,一个函数的输出结果是另一个函数的输入参数,一旦第一个函数开始执行,就想多古诺骨牌一样推导执行了。

  • 注意
    • 使用compose要注意以下3点:
    • compose的参数时函数,返回的值也是一个函数。
    • 除了初始函数(最右侧的一个)外,其他函数的接收参数都是上一个函数的返回值,所以初始函数的是多元的,而其他函数的接收值是一元的。
    • compose函数可以接收任意的参数,所有的参数都是函数,且执行方向为自右向左。初始函数一定要放到参数的最右侧。
  • 实现代码】下面来完善compose实现,实现无线函数合成。
  • 设计思路】既然函数以多米诺骨牌式执行,那么可以使用递归或迭代,在函数体内不断的执行arguments中的函数,将上一个函数的执行结果座位下一个执行函数的输入参数。
// 函数合成,从右到左
var compose = function() {
    
    
        var _arguments = arguments;
        var length = arguments.length;
        var index = length;
        // 检测参数,如果存在非函数参数,则抛出异常
        while (index--) {
    
    
            if (typeof _arguments[index] != 'function') {
    
    
                throw new TypeError('参数必须为函数!');
            }
        }
        return function() {
    
    
            var index = length - 1; // 定位到最后一个参数下标
            // 如果存在两个及以上个参数,则调用最后一个参数函数,并传入内层参数;否则直接返回第1个参数函数
            var result = length ? _arguments[index].apply(this, arguments) : arguments[0];
            // 迭代参数函数
            while (index--) {
    
    
                // 把右侧函数的执行结果作为参数传递给左侧函数,并调用
                result = _arguments[index].call(this, result);
            }
            return result;
        }
    }
    // 反向函数合成,即从左到右
var composeLeft = function() {
    
    
    return conpose.apply(null, [].reverse.call(arguments));
}
  • 应用代码】在上面代码实现中,compose实现是从右到左进行合成的,也提供了从左到右的composeLeft。
var add = function(x) {
    
    
    return x + 5;
}
var mul = function(x) {
    
    
    return x * 5;
}
var sub = function(x) {
    
    
    return x - 5;
}
var div = function(x) {
    
    
    return x / 5;
}
var fn = compose(add, mul, sub, div);
console.log(fn(50)); // 30
var fn = compose(add, compose(mul, sub, div));
console.log(fn(50)); // 30
var fn = compose(compose(add, mul), sub, div);
console.log(fn(50)); //30

函数柯里化

  • 问题提出

函数合成是把多个单一参数函数合成一个多参数函数的运算。例如,a(x)和b(x)组合为a(b(x)),则合称为f(a,b,x)。注意这里的a(x)和b(x)都只能接受一个参数。如果接收多个参数,如a(x,y)和b(a,b,c),那么函数合成就比较麻烦。

这里就需要用到函数柯里化。所谓柯里化,就是把一个多参数的函数转化为一个参数函数,有了柯里化运算之后,我们就能做到所有函数只接受一个参数。

  • 设计思路

先调用传递函数的一部分参数来调用它,让它返回一个函数,然后再去处理剩下的参数。也就是说,把多参数的函数分解为多步操作的函数,以实现每次调用函数时,仅需要传递更少或单个参数。例如,下面是一个简单的求和函数add()。

var add = function (x, y) {
    
    
	return x + y;
} 

每次调用add(),需要同时传入两个参数。如果希望每次仅传入一个参数,可以这样进行柯里化。

var add = function (x) {
    
    
	return function (y) {
    
    
		return x + y;
	}
	console.log(add(2)(6)); //8
	var add1 = add(200);
	console.log(add1(2)); //202 
}

函数add接受一个参数,并返回一个函数,这个返回的函数可以再接受一个参数,并返回两个参数之和。从某种意义上讲,这是一种对参数的“缓存”,是一种非常高效的函数式运算方法。柯里化在DOM的回调中非常有用。

  • 实现代码

设想curry可以接收一个函数,即原始函数,返回的也是一个函数,即柯里化函数。这个返回的柯里化函数在执行的过程中会不断地返回一个存储了传入参数的函数,直到触发了原始函数执行的条件。例如,设计一个add()函数,计算两个参数之和。

var add = function (x, y) {
    
    
	return x + y;
}

柯里化函数:

var curryAdd = curry(add);

这个add需要两个参数,但是执行curryAdd时,可以传入更少的参数。当传入的参数少于add需要的参数时,add函数并不会执行,curryAdd就会将这个参数记录下来,并返回另外一个函数,这个函数可以继续执行传入参数。如果传入参数的总数等于add所需要参数的总数,则执行原始参数,返回想要的结果。如果没有参数限制,最后根据空的小括号作为执行原始参数的条件,返回运算结果。

  • curry实现的封装代码如下:
// 柯里化函数
function curry(fn) {
    
    
    var _argLen = fn.length; // 记录原始参数的形参个数
    var args = [].slice.call(arguments, 1); // 把传入的第二个及以后参数转换为数组
    function wrap() {
    
    
        // 把当前参数转换为数组,与前面参数进行合并
        _args = args.concat([].slice.call(arguments));

        function act() {
    
    
            // 把当前参数转换为数组,与前面参数进行合并
            _args = _args.concat([].slice.call(arguments));
            // 如果传入参数综合大于等于原始参数的个数,触发执行条件
            if ((_argLen == 0 && arguments.length == 0) ||
                (_argLen > 0 && _args.length >= _argLen)) {
    
    
                // 执行原始函数,并把每次传入参数都传入进去,返回执行结果,停止curry
                return fn.apply(null, _args);
            }
            return arguments.callee;
        }
        // 如果传入参数大于等于原始函数的参数个数,即触发了执行条件
        if ((_argLen == 0 && arguments.length == 0) ||
            (_argLen > 0 && _args.length >= _argLen)) {
    
    
            // 执行原始函数,并把每次传入参数都传入进去,返回执行结果,停止curry
            return fn.apply(null, _args);
        }
        act.toString = function() {
    
     // 定义处理函数的字符串表示为原始函数的字符串表示
            return fn.toString();
        }
        return act;

    }
    return wrap;
}
  • 应用代码】设计求和函数,没有形参限制,柯里化函数将根据空小括号作为调用原始函数的条件。
var add = function() {
    
    
    return [].slice.call(arguments).reduce(function(a, b) {
    
    
        return (typeof a == "number" ? a : 0) + (typeof b == "number" ? b : 0);
    })
}

var curried = curry(add);
console.log(curried(1)(2)(3)()); //6
var curried = curry(add);
console.log(curried(1)(2)(3)(4)()); //10
var curried = curry(add, 1);
console.log(curried(1, 2)(3)(3)()); //10
var curried = curry(add, 1, 5);
console.log(curried(1, 2, 3, 4)(5)()); //21
  • 提示:curry函数的设计不是固定的,可以根据具体应用场景灵活定制。curry主要有3个作用:缓存参数,暂缓函数执行,分解执行任务。

高阶函数

  • 高阶函数也称为算子(运算符)或泛函。作为函数式编程最显著的特征,高阶函数是对函数运算进行进一步的抽象。高阶函数的形式应至少满足下列条件之一
    • 函数可以作为参数被传入,也称为回调函数,如函数合成运算。
    • 可以返回函数作为输出,如函数柯里化运算。
  • 下面结合不同的应用场景,介绍高阶函数的常规应用。

回调函数

把函数作为值传入另一个函数,当传入函数被调用时,就成为回调函数,即异步调用已绑定的函数。例如,事件处理函数、定时器中的回调函数、异步请求中的回调函数、replace方法中的替换函数、数组迭代中的回调函数(sort、map、forEach、filter、some、every、reduce、reduceRight等)都是回调函数的不同应用形式。

  • 实例1:下面代码根据日期对对象进行排序。
var a = {
    
    
    id: 1,
    date: new Date(2019, 3, 14)
}
var b = {
    
    
    id: 2,
    date: new Date(2001, 2, 24)
}
var c = {
    
    
    id: 3,
    date: new Date(2001, 4, 30)
}
var arr = [a, b, c];
arr.sort(function(x, y) {
    
    
    return x.date - y.date;
});
for (var i = 0; i < arr.length; i++) {
    
    
    console.log(arr[i].id + " " + arr[i].date.toLocaleDateString());
}

输出结果:

2 2001/3/24
3 2001/5/30
1 2019/4/14

在数据排序的时候,会迭代数组每个元素,并注意调用回调函数function(x,y){return x.date-y.date;}

  • 实例2:之前介绍过数组map方法,实际上很多函数式编程语言均有此函数。其语法格式:map(array,func)。map表达式将func函数作用于array的每一个元素,并返回一个新的array。下面使用js实现map(array,func)表达式运算。
function map(array, func) {
    
    
    var res = [];
    for (var i in array) {
    
    
        res.push(func(array[i]));
    }
    return res;
}
console.log(map([1, 2, 5, 7, 8], function(n) {
    
    
    return n * n;
})); //  [1, 4, 25, 49, 64]
console.log(map(["one", "two", "three", "four"], function (item) {
    
    
    return item[0].toUpperCase() + item.slice(1).toLowerCase();
})); // ["One", "Two", "Three", "Four"]

两次调用map,却得到了截然不同的结果,是因为map的参数本身已经进行了一次抽象,map函数做的是第二次抽象。注意:高阶的“阶”可以理解为抽象层次。

单例模式

  • 单例就是保证一个类只有一个实例。实现方法,先判断是否存在,如果存在直接返回,否则就创建实例再返回。
  • 单例模式可以确保一个类型只有一个实例对象。在js中,单例可以作为一个命名空间,提供一个唯一的访问点来访问对象。单例模式封装代码如下:
var getSingle = function (fn) {
    
    
	var ret;
	return function() {
    
    
		return ret || (ret = fn.apply(this, arguments));
	}
}
  • 实例1:在脚本中定义XMLHttpRequest对象,由于一个页面可能需要创建多次异步请求对象,使用单例模式封装之后,就不用重复创建实例对象,共用一个即可。
function XHR() {
    
    
	return new XMLHttpRequest();
}
var xhr = getSingle(XHR);
var a = xhr();
var b = xhr();
console.log(a===b);
  • 实例2:可以限定函数只能调用一次,避免重复调用,这在事件处理函数中非常有用。
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script>
        window.onload = function() {
     
     
            function getSingle(fn) {
     
     
                var ret;
                return function() {
     
     
                    return ret || (ret = fn.apply(this, arguments));
                };
            };
            var f = function() {
     
     
                console.log(this.nodeName);
            }
            document.getElementsByTagName("button")[0].onclick = getSingle(f);
        }
    </script>
</head>

<body>
    <button>仅能点击一次</button>
</body>

</html>

实现AOP

AOP(面向切面编程)就是把一些与业务逻辑块无关的功能抽离出来,如日志统计、安全控制、异常处理等,然后通过“动态织入”的方式参入业务逻辑模块中。这样设计的好处是:首先可以保证业务模块的纯净和高内聚性;其次可以方便的复用日志统计等功能模块。

  • 实例:在js中实现AOP,一般是把一个函数“动态织入”到另一个函数中。具体的实现方法有很多,下面通过扩展Function.prototype方法实现AOP。
Function.prototype.before = function(beforefn) {
    
    
    var __self = this;
    return function() {
    
    
        beforefn.apply(this, arguments);
        return __self.apply(this, arguments);
    }
};
Function.prototype.after = function(afterfn) {
    
    
    var __self = this;
    return function() {
    
    
        var ret = __self.apply(this, arguments);
        afterfn.apply(this, arguments);
        return ret;
    }
};
var func = function() {
    
    
    console.log(2);
}
func = func.before(function() {
    
    
    console.log(1);
}).after(function() {
    
    
    console.log(3);
});
func(); // 1,2,3

函数字节

函数字节就是降低函数被调用的频率,主要针对DOM事件暴露出的问题提出的一种解决方案。例如,使用resize、mousemove、mouseover、mouseout、keydown、keyup等事件,会频繁的触发事件。如果这些事件的处理函数中包含大量耗时操作。如Ajax请求、数据库查询、DOM遍历等,则可能会让浏览器崩溃,严重影响用户体验。

例如,在大型网店平台的导航栏中,为了减轻mouseover和mouseout移动过快给浏览器处理带来的负担,特别是减轻设计Ajax调用给服务器造成的极大负担,都会进行函数字节处理。

  • 设计思想】让代码在间断的情况下重复执行。
  • 实现方法】使用定时器对函数进行节流。
  • 实现代码
// 函数节流封装代码,参数method表示要执行的函数,delay表示要延迟的时间,单位为毫秒
function throttle(method, delay) {
    
    
    var timer = null;
    return function() {
    
    
        var context = this;
        var args = arguments;
        clearTimeout(timer);
        timer = setTimeout(function() {
    
    
            method.apply(context, args);
        }, delay);
    }
}
  • 应用代码

设计文本框keyup事件和窗口resize事件,在浏览器中拖动窗口,或者在文本框中输入字符,然后在控制台查看时间响应次数和速度。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script>
        window.onload = function() {
     
     
            function queryData(text) {
     
     
                console.log("搜索: " + text);
            }
            var input = document.getElementById("search");
            input.addEventListener("keyup", function(event) {
     
     
                queryData(this.value);
            });
            var n = 0;

            function f() {
     
     
                console.log("响应次数:" + ++n);
            }
            window.onresize = f;
        }
    </script>
</head>

<body>
    <input type="text" id="search" name="search">
</body>

</html>

通过观察可以发现,在拖动改变窗口的一瞬间,resize事件响应了几十次。如果在文本框中输入字符,keyup事件会立即响应,等不到用户输入完一个单词。

下面使用throttle()封装函数,把上面的事件处理函数转换为节流函数,同时设置延迟时间为500毫秒。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script>
        window.onload = function() {
    
    

            // 函数节流封装代码,参数method表示要执行的函数,delay表示要延迟的时间,单位为毫秒
            function throttle(method, delay) {
    
    
                var timer = null;
                return function() {
    
    
                    var context = this;
                    var args = arguments;
                    clearTimeout(timer);
                    timer = setTimeout(function() {
    
    
                        method.apply(context, args);
                    }, delay);
                }
            }



            function queryData(text) {
    
    
                console.log("搜索: " + text);
            }
            var input = document.getElementById("search");
            input.addEventListener("keyup", function(event) {
    
    
                throttle(queryData, 500)(this.value);
            });
            var n = 0;

            function f() {
    
    
                console.log("响应次数:" + ++n);
            }
            window.onresize = throttle(f, 500);
        }
    </script>
</head>

<body>
    <input type="text" id="search" name="search">
</body>

</html>

重新进行测试,会发现拖动一次窗口改变大小,仅响应一次,而在为本框中输入字符时,也不会立即响应,等了半秒钟后,才显示输入的字符。

分时函数

分时函数与函数节流设计思路相近,但应用场景略有不同。当批量操作影响到页面性能时,如一次往页面中添加大量的DOM节点,显然会给浏览器渲带来影响,极端情况下可能会出现卡顿或假死等现象。

  • 设计思路】把批量操作分批处理,如把一秒钟创建1000个节点,改为每隔200毫秒创建100个节点等。
  • 实现代码
 var timeChunk = function (ary, fn, count) {
    
    
    var t;
    var start = function () {
    
    
        for (var i = 0; i < Math.min(count || 1, ary.length); i++) {
    
    
            var obj = ary.shift();
            fn(obj);
        }
    }
    return function () {
    
    
        t = setInterval(function () {
    
    
            if (ary.length === 0) {
    
     // 如果全部节点都已经被创建好
                return clearInterval(t);
            }
            start(); 
        }, 200); // 分批执行的时间间隔,也可以用参数的形式传入
    };
};

timeChunk函数接收3个参数,第1个参数表示批量操作时需要用到的数据,第2个参数封装了批量操作的逻辑函数,第3个参数表示分批操作的数量。

  • 代码应用

下面在页面中插入10000个span元素,由于数量巨大,这里使用分时函数进行分批操作。

var arr = [];
for (var i = 1; i <= 10000; i++) {
    var span = document.createElement("span");
    span.style.padding = "6px 12px";
    span.innerHTML = i;
    arr.push(span);
}
var fn = function(obj) {
    document.body.appendChild(obj);
}
timeChunk(arr, fn, 100)();

惰性载入函数

惰性载入就是当第1次根据条件执行函数后,第二次调用函数时,就不再检测条件,直接执行函数。

  • 问题由来

由于浏览器之间的行为差异,很多脚本会包含大量的条件,通过条件决定不同行为的浏览器执行不同的代码。

  • 设计思路

第一步,当函数第1次被调用的时候,执行一次条件检测。

第二步,在第1次调用的过程中,使用另外一个根据条件检测,按合适方式执行的函数,覆盖掉第1次调用的函数。

第三步,当再次调用该函数时,不再是原来的函数,而是直接调用被覆盖后的函数,这样就不用再次执行 条件检测了。

  • 实例:在注册事件处理函数时,经常需要考略浏览器的事件模型。先要检测当前浏览器是DOM模型,还是IE的时间模型,然后调用不同的方法进行注册。
var addEvent = function(element, type, handle) {
    
    
    if (element.addEventListener) {
    
    
        element.addEventListener(type, handle, false);
    } else {
    
    
        element.attachEvent("on" + type, handle);
    }
}
addEvent(document, "mousemove", function() {
    
    
    console.log("移动鼠标:" + ((this.n) ? (++this.n) : (this.n = 1)))
})
addEvent(window, "resize", function() {
    
    
    console.log("改变窗口大小:" + ((this.n) ? (++this.n) : (this.n = 1)));
})

如此简单的条件检测,如果在高频、巨量的操作中,每次调用addEvent()都需要做一次条件检测,无疑是不经济的。下面使用惰性载入方法,重写addEvent()函数。

var addEvent = function(element, type, handle) {
    
    
    // 先检测浏览器,然后把合适的操作函数覆盖掉当前addEvent()
    addEvent = element.addEventListener ? function(element, type, handle) {
    
    
        element.addEventListener(type, handle, false);
    } : function(element, type, handle) {
    
    
        element.attachEvent("on" + type, handle);
    };
    // 在第一次执行addEvent函数时,修改了addEvent函数之后,必须执行一次
    addEvent(element, type, handle);
}

在上面代码中,当第1次调用addEvent()函数时做一次条件检测;然后根据浏览器选择相应的时间注册方法,同时把这个操作封装在一个匿名函数中;接着使用该函数覆盖掉addEvent()函数;最后执行第1次时间注册操作。这样,当第2次开始再次注册事件时,就不需要做条件检测了。

分支函数

分支函数与惰性载入函数都是解决条件检测的问题。分支函数类似面向对象编程的接口,对外提供相同的操作接口,内部实现则会根据不同的条件执行不同的操作。分支函数与惰性载入函数在设计原理上是非常接近的,只是在代码实现方面略有差异。

  • 实例:使用分支函数解决浏览器兼容性的重复判断。解决浏览器兼容性的一般方法是使用if语句进行特性检测或能力检测,然后根据浏览器的不同,实现功能上的兼容。这样做的问题是,每执行一次代码,可能都需要进行一次浏览器兼容方面的检测,这是没有必要的。
  • 分支函数的设计思路:在代码初始化执行的时候检测浏览器的兼容性,在之后的代码执行过程中就不再检测。
  • 下面声明一个XMLHttpRequest实例对象。
var XHR = function () {
    
    
	var standard = {
    
    
		createXHR : function () {
    
    
			return new XMLHttpRequest();
		}
	}
	var newActionXObject = {
    
    
		createHXR : function() {
    
    
			return new ActionXObject("Msxm12.XMLHTTP");
		}
	}
	var oldActionXObject = {
    
    
		createXHR: function() {
    
    
			return new ActionXObject("Microsoft.XMLHTTP");
		}
	}
	if(standard.createXHR()) {
    
    
		return standard;
	} else {
    
    
		try {
    
    
			newActionXObject.createXHR();
			return newActionXObject;
		} catch(o) {
    
    
			oldActionXObject.createXHR();
			return oldActionXObject;
		}
	}
}();
var xhr = XHR.createXHR();

在代码初始化执行之后,XHR被初始化为一个对象,拥有createXHR()方法,该方法的实现已经在初始化阶段根据当前浏览器选择了适合的方法,当调用XHR.createXHR()方法创建XMLHttpRequest实例对象时,就不再去检测浏览器的兼容性问题 。

偏函数

偏函数是函数柯里化运算的一种特定应用场景。简单描述,就是把一个函数的某些参数固定化,也就是设置默认值,返回一个新的函数,在新函数中继续接受剩余参数,这样调用这个新函数就会简单。

  • 实例:下面是一个类型检测函数,接受两个参数,第1个表示类型字符串,第2个表示检测的数据。
var isType = function(type, obj) {
    
    
	return Object.prototype.toString.call(obj) == '[Object ' + type + ']';
}

该函数包含两个设置参数,使用时比较繁琐。一般按以下方式进行设计。

var isString = function (obj) {
    
    
	return Object.prototype.toString.call(obj) == '[Object String]';
}
var isFunction = function (obj) {
    
    
	return Object.prototype.toString.call(obj) == '[Object Function]';
}

函数接收的参数单一,检测的功能也单一和明确,这样更便于在表达式运算中有针对性的调用。下面对isType()函数进行扁平化设计,代码如下:

var isType = function(type) {
    
    
	return function(obj) {
    
    
		return Object.prototype.toString.call(obj) == '[objcet ' + type +']';
	}
}

然后根据偏函数获取不同类型检测函数。

var isString = isType("String");
var isFunction = isType("Function");

引用代码如下:

console.log(isString("12")); // true
console.log(isFunction(function(){
    
    })); // true
console.log(isFunction({
    
    })); // false

泛型函数

js具有动态类型语言的部分特点,如用户不用关心一个对象是否拥有某个方法,一个对象也不限于只能使用自己的方法——使用call和apply动态调用,可以使用其他对象的方法。这样该方法中的this就不再限于原对象,而是被泛化,从而得到更广泛的应用。

泛型函数(Uncurry)的设计目的是:将泛化this的过程提取出来,将fn.call或fn.apply抽象成通用函数。

  • 代码实现
Function.prototype.uncurry = function () {
    
    
    var self = this;
    return function () {
    
    
        return Function.prototype.apply.apply(self, arguments);
    }
};
  • 代码应用
// 泛化Array.prototype.push
var push = Array.prototype.push.uncurry();
var obj = {
    
    };
push(obj, [3, 4, 5]);
for (var i in obj) {
    
    
    console.log(i);
}

输出类数组:{0:3,1:4,2:5,length:3}

  • 逆向解析

简单的逆向分析一下泛型函数的设计思路。

首先,调用push(obj,[3,4,5]);等效于以下原始动态调用方法。

Array.prototype.push.apply(obj,[3,4,5]);

其次,调用Array.prototype.push.uncurry();泛型化之后,实际上push就是以下函数。

push = function () {
    
    
	return Function.prototype.apply.apply(Array.prototype.push, arguments);
}

最后,调用push(obj,[3,4,5]);代码进行一下逻辑转换。

Array.prototype.push.(Function.prototype.apply)(obj,[3,4,5]);

即为:

Array.prototype.push.apply(obj,[3,4,5]);

实际上,上面代码使用了两个apply动态调用,实现逻辑思路的两次翻转。

猜你喜欢

转载自blog.csdn.net/weixin_46351593/article/details/108568664
今日推荐