红宝书-第十章-函数

红宝书-第十章-函数

  • 函数实际上是对象,每个函数都是Function类型的实例,而Function也有属性和方法,跟其他引用类型一样。

箭头函数

  • 箭头函数虽然语法简洁,但也有很多场合不适用。箭头函数不能使用arguments、super 和new.target,也不能用作构造函数。此外,箭头函数也没有prototype 属性。

函数名

  • 函数名就是指向函数的指针,这意味着一个函数可以有多个名称。
  • 使用不带括号的函数名会访问函数指针,而不会执行函数。
  • ECMAScript 6 的所有函数对象都会暴露一个只读的name 属性,其中包含关于函数的信息。多数情况下,这个属性中保存的就是一个函数标识符,或者说是一个字符串化的变量名。即使函数没有名称,也会如实显示成空字符串。
    • 如果函数是一个获取函数、设置函数,或者使用bind()实例化,那么标识符前面会加上一个前缀。

理解参数

  • ECMAScript 函数的参数跟大多数其他语言不同。ECMAScript 函数既不关心传入的参数个数,也不关心这些参数的数据类型。

  • ECMAScript 函数的参数在内部表现为一个数组。函数被调用时总会接收一个数组,但函数并不关心这个数组中包含什么。

  • arguments对象

    • arguments 对象是一个类数组对象(但不是Array 的实例),因此可以使用中括号语法访问其中的元素(第一个参数是arguments[0],第二个参数是arguments[1])。而要确定传进来多少个参数,可以访问arguments.length 属性。

    • arguments对象的值始终会与对应的命名参数同步。

      function doAdd( num1, num2) {
      	arguments[1] = 10 ;
      	console.log(arguments[0] + num2); // num2 的值始终等于arguments[1]
      }// 但是严格模式下num2不受argument[1]改变的影响
      
    • 严格模式下,arguments 会有一些变化,arguments在严格模式下属于传入的实参对象,并且是不可变的,即不可在函数内部通过arguments来修改实参值。

箭头函数中的参数

  • 如果函数是使用箭头语法定义,那么传给函数的参数将不能使用arguments关键字访问,而只能通过定义的命名参数访问。(箭头函数中没有arguments对象)

没有重载

  • ECMAScript 函数没有签名,因为参数是由包含零个或多个值的数组表示的。

  • 可以通过检查参数的类型和数量,然后分别执行不同的逻辑来模拟函数重载。

默认参数值

  • 只要在函数定义中的参数后面用=就可以为参数赋一个默认值

    function makeKing(name = 'defaultName') {
    	return `King ${name} `;
    }
    
    • 给参数传undefined相当于没有传值,仍然使用默认参数。
  • 默认参数值并不限于原始值或对象类型,也可以使用调用函数返回的值。

  • 函数的默认参数只有在函数被调用时才会求值,不会在函数定义时求值。而且,计算默认值的函数只有在调用函数但未传相应参数时才会被调用。

默认参数作用域与临时性死区

  • 因为参数是按顺序初始化的,所以后定义默认值的参数可以引用先定义的参数。

    function makeKing(name = 'Henry', numerals = naem) {
    	return `King ${name} ${numerals}`;
    }
    
  • 参数初始化顺序遵循“暂时性死区”规则,即前面定义的参数不能引用后面定义的。

参数扩展与收集

  • 扩展参数

    • let values = [1,2,3,4];
      console.log(getSum(...values));
      
    • 对函数中的arguments 对象而言,它并不知道扩展操作符的存在,而是按照调函数时传入的参数接收每一个值。

  • 收集参数

    • 收集参数的前面如果还有命名参数,则只会收集其余的参数;如果没有则会得空数组。因为收集参数的结果可变,所以只能把它作为最后一个参数

函数声明与函数表达式

  • JavaScript 引擎在加载数据时对它们是区别对待的。JavaScript 引擎在任何代码执行之前,会先读取函数声明,并在执行上下文中生成函数定义。而函数表达式必须等到代码执行到它那一行,才会在执行上下文中生成函数定义
  • 函数声明会在任何代码执行之前先被读取并添加到执行上下文。这个过程叫作函数声明提升(function declaration hoisting)

函数作为值

  • 函数名在ECMAScript中就是变量,所以函数可以用在任何可以使用变量的地方。这意味着不仅可以把函数作为参数传给另一个函数,而且还可以在一个函数中返回另一个函数

  • //函数作为参数及返回值。
    function callSomeFunction(someFunction, someArgument) {
    	return someFunction(someArgument);
    }
    
  • 如果是访问函数而不是调用函数,那就必须不带括号。

  • 从一个函数中返回另一个函数,例子:一个包含对象的数组,需要按照任意对象属性对数组进行排序。

    function createComparisonFunction(propertyName) {
    	return function (object1, object2) {
    		let value1 = object1[propertyName];
    		let value2 = object2[propertyName];
    		
    		if(value1 < value2) {
    			return -1;
    		} else if (value1 > value2) {
    			return 1;
    		} else {
    			return 0;
    		}
    	};
    }
    
    let data = {
        {name: "Zachary", age: 28},
        {name: "Nicholas", age: 29}
    };
    
    data.sort(createComparisonFunction("name"));
    data.sort(createComparisonFunction("age"));
    

函数内部

  • ES5中,函数内部有两个特殊对象:arguments和this,ES6有新增了new.target属性。

  • arguments

    • arguments对象还有一个callee属性,是一个指向arguments对象所在函数的指针。

    • 使用arguments.callee可以让函数逻辑与函数名解耦。

// 使用arguments.callee代替之前硬编码的factorial,无论函数叫什么名称,都可以引用正确的函数
function factorial(num) {
if(num <= 1) {
return 1;
} else {
return num * arguments.callee(num - 1);
}
}
let trueFactorial = factorial;
factorial = function() {
return 0;
};
console.log(trueFactorial(5)); //120
```

  • this

    • 在标准函数和箭头函数中有不同的行为。

    • 在标准函数中,this引用的是把函数当成方法调用的上下文对象。到底引用哪个对象必须到this函数被调用时才能确定

    • 在箭头函数中,this引用的是定义箭头函数的上下文。

    • 在事件回调或定时回调中调用某个函数时,this 值指向的并非想要的对象。此时将回调函数写成箭头函数就可以解决问题。这是因为箭头函数中的this 会保留定义该函数时的上下文

      function King() {
      	this.royaltyname = 'Henry';
      	
      	//箭头函数的this引用King的实例
      	setTimeout( () => console.log(this.royaltyName), 1000);
      }
      
      function Queen() {
      	this.royaltyname = 'Elizabeth';
      	
      	//this引用window对象
      	setTimeout( function() { console.log(this.royaltyName);}, 1000);
      }
      
      new King(); //Henry
      new Queen(); //undefined 在全局上下文调用this指向window
      
  • caller

    • 这个属性引用的是调用当前函数的函数,或者如果是在全局作用域中调用的则为null。

      function outer() {
      	inner();
      }
      function outer() {
      	console.log(inner.caller);
          //或者
      	console.log(arguments.callee.caller); //降低耦合度
      }
      
  • new.target

    • ECMAScript 6 新增了检测函数是否使用new 关键字调用的new.target 属性。

    • 如果函数是正常调用的,则new.target的值是undefined;如果是使用new关键字调用的,则new.target将引用被调用的构造函数。

      function King() {
      	if(!new.target) {
      		throw 'King must be instantiated using "new"';
      	}
      	console.log('King instantiated using "new"');
      }
      
      new King();
      King();
      

函数属性与方法

  • ECMAScript 中的函数是对象,因此有属性和方法。每个函数都有两个属性:length和prototype。

  • length 属性保存函数定义的命名参数的个数。

  • prototype 属性是保存引用类型所有实例方法的地方。比如toString()、valueOf()等,进而由所有实例共享。

  • 函数有两个方法:apply()call()。这两个方法都会以指定的this 值来调用函数,即会设置调用函数时函数体内this 对象的值。

    function sum(num1, num2) {
    	return num1 + num2;
    }
    function callSum1(num1, num2) {
    	return sum.apply(this, arguments); //传入arguments对象
    }
    function callSum2(num1,num2) {
    	return sum.apply(this, [num1, num2]); //传入数组
    }
    console.log(callSum1(10,10)); //20
    console.log(callSum2(10,10)); //20
    
    • call()方法与apply()的作用一样,只是传参的形式不同。通过call()向函数传参时,必须将参数一个一个地列出来。

    如果想直接传arguments对象或者一个数组,使用apply(); 否则,使用call()。

  • apply()和call()真正强大的地方并不是给函数传参,而是控制函数调用上下文函数体内this值的能力。

    window.color = 'red';
    let o = {
    	color: 'blue'
    };
    function sayColor() {
    	console.log(this.color);
    }
    sayColor(); //red
    sayColor.call(this); //red
    sayColor.call(window); //red
    sayColor.call(o); //blue
    
  • 新方法:bind()。bind()方法会创建一个新的函数实例,其this值会被绑定到传给bind()的对象。

    window.color = 'red';
    var o = {
    	color: 'blue'
    };
    function sayColor() {
    	console.log(this.color);
    }
    // objetSayColor()中的this值被设置为o
    let objectSayColor = sayColor.bind(o);
    objectSayColor();	//blue
    
  • 对函数而言,继承的方法toLocaleString()和toString()始终返回函数的代码。返回代码的
    具体格式因浏览器而异。

函数表达式

  • 函数表达式不会“提升”。

    //没问题, 如果用函数声明则有问题
    let sayHi; 
    if(condition) {
    	sayHi = function() {
    		console.log("Hi!");
    	};
    } else {
    	sayHi = function() {
    		console.log("Yo!");
    	};
    }
    
  • 任何时候,只要函数被当作值来使用,它就是一个函数表达式。

递归

  • 另一个解决函数名变化的方法:使用命名函数表达式(因为在严格模式下运行的代码是不能访arguments.callee)

    //使用命名函数表达式
    const factorial = (function f(num) {
    	if(num <= 1) {
    		return 1;
    	} else {
    		return num * f(num - 1);
    	}
    });
    

尾调用优化

  • “尾调用”,即外部函数的返回值是一个内部函数的返回值。

    function outerFunction() {
    	return innerFunction(); //尾调用
    }
    
  • ES6优化后,把原来需要等待innerFunction计算返回后才能弹出outerFunction()的栈帧,优化为到达return语句时就将outerFunction弹出栈帧

  • 尾调用优化条件:

    • 代码在严格模式下执行;
    • 外部函数的返回值是对尾调用函数的调用;
    • 尾调用函数返回后不需要执行额外的逻辑;
    • 尾调用函数不是引用外部函数作用域中自由变量的闭包。
    "use strict";
    
    //无优化:尾调用没有返回
    function outerFunction() {
    	innerFunction();
    }
    //无忧化:尾调用没有直接返回
    function outerFunction() {
    	let innerFunctionResult = innerFunction();
    	return innerFunctionResult;
    }
    //无优化:尾调用返回后必须转型为字符串
    function outerFunction() {
    	return innerFunction().toString();
    }
    //无忧化:尾调用是一个闭包
    function outerFunction() {
    	let foo = 'bar';
    	function innerFunction() { return foo; }
    	return innerFunction();
    }
    
    // 有优化:栈帧销毁前执行参数计算
    function outerFunction(a,b) {
        return innerFunction(a+b);
    }
    // 有优化:初始返回值不涉及栈帧
    function outerFunction(a,b) {
        if(a<b) {
            return a;
        }
        return innerFunction(a+b);
    }
    // 有优化:两个内部函数都在尾部
    function outerFunction(condition) {
        return condition ? innerFunctionA() : innerFunctionB();
    }
    
    • 之所以要求严格模式,主要因为在非严格模式下函数调用中允许使用f.arguments和f.caller,而它们都会引用外部函数的栈帧
  • 优化实例:

    //无优化:返回语句有相加操作
    function fib(n) {
        if(n < 2) {
            return n;
        }
        return fib(n-1) + fib(n-2);
    }
    
    "use strict";
    //基础框架
    function fib(n) {
    	return fibImpl(0, 1, n);
    }
    //执行递归
    function fibImpl(a, b, n) {
    	if(n === 0) {
    		return a;
    	}
    	return fibImpl(b, a+b, n-1);
    }
    //重构后,调用fib(1000)就不会对浏览器造成威胁。
    

闭包

  • 匿名函数经常被人误认为是闭包(closure)。闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。

    function createComparisonFunction(propertyName) {
    	return function (object1, object2) {
            // 该函数引用了外部函数的变量propertyName
            // 这是因为内部函数的作用域链包含createComparisonFunction()函数的作用域
    		let value1 = object1[propertyName];
    		let value2 = object2[propertyName];
    		
    		if(value1 < value2) {
    			return -1;
    		} else if (value1 > value2) {
    			return 1;
    		} else {
    			return 0;
    		}
    	};
    }
    
  • 作用域链实际上就是一个包含指针的列表,每个指针分别指向一个变量对象,但物理上并不会包含相应的对象。

  • 在调用一个函数时,会为这个函数调用创建一个执行上下文,并创建一个作用域链。然后用arguments和其他命名参数来初始化这个函数的活动对象。外部函数的活动对象是内部函数作用域链上的第二个对象。这个作用域链一直向外串起了所有包含函数的活动对象,直到全局执行上下文才终止。

    let compare = createComparisonFunction('name');
    let result = compare({name: 'Nicholas'}, {name: 'Matt'});
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JXUzSxxq-1607071215586)(红宝书-第十章-函数/AcroRd32_XMMAeGfWbg.png)]

  • createComparisonFunction()的活动对象并不能在它执行完毕后销毁,因为匿名函数的作用域链中仍然有对它的引用。createComparisonFunction()执行完毕后,其执行上下文的作用域链会销毁,但它的活动对象仍然会保留在内存中,直到匿名函数被销毁后才会被销毁。

    //需要手动解除对函数的引用
    let compareNames = createComparisonFunction('name');	//创建比较函数
    let result = compareNames({name: 'Nicholas'}, {name: 'Matt'});	//调用函数
    compareNames = null;	//解除对函数的引用,这样就可以释放内存
    

闭包会保留它们包含函数的作用域,所以比其他函数更占用内存。

  • this对象

    • 在闭包中使用this会让代码变复杂,如果内部函数没有使用箭头函数定义,则this对象会在运行时绑定到执行函数的上下文

    • 每个函数在被调用时都会自动创建两个特殊变量:this和arguments。内部函数永远不可能直接访问外部函数的这两个变量。但是,如果把this保存到闭包可以访问的另一变量中,则可以实现外部对其的访问。

      window.identity = 'The Window';
      
      let object = {
      	identity: 'My Object',
      	getIdentityFunc() {
      		let that = this;	//将外部函数的this保存到变量that中。
      		return function() {
      			return that.identity;
      		};
      	}
      }
      
      console.log( object.getIdentittyFunc()); //'My Object'
      
    • this和arguments都是不能直接在内部函数中访问的。如果想访问包含作用域中的arguments对象,则同样需要将其引用先保存到闭包能访问的另一个变量中。

  • 内存泄漏

    • 在旧版本IE中,把HTML元素保存在某个闭包的作用域中,就相当于宣布该元素不能被消耗。例子如下:

    • function assignHandler() {
      	let element = document.getElementById('someElement');
      	element.onclick = () => console.log(element.id);
      }
      

      以上代码创建了一个闭包,即element元素的事件处理程序,而这个处理程序又创建了一个循环引用。匿名函数引用着assignHandler()的活动对象,阻止了对element的引用计数归零。只要这个匿名函数存在,element的引用计数就至少等于1。即内存不会被回收。

      function assignHandler() {
      	let element = document.getElementById('someElement');
      	let id = element.id;	//消除循环引用
      	
      	element.onclick = () => console.log(id);
          
      	element = null;		//解除对这个对象的引用
      }
      

      将代码简单修改后就可以避免这种情况,闭包改为引用一个保存着element.id 的变量id,从而消除了循环引用。此时闭包还是会引用包含函数的活动对象,而其中包含element,必须再把element设置为null,才解除了对这个对象的引用。

立即调用的函数表达式

  • 立即调用的匿名函数又被称作立即调用的函数表达式IIFE,Immediately Invoked Function Expression)。

  • 使用IIFE 可以模拟块级作用域,即在一个函数表达式内部声明变量,然后立即调用这个函数。这样位于函数体作用域的变量就像是在块级作用域中一样。CMAScript 5 尚未支持块级作用域,使用IIFE模拟块级作用域是相当普遍的。

    // IIFE
    (function() {
    	for (var i = 0 ; i<count ; i++) {
    		console.log(i);
    	}
    }) ();
    
    console.log(i);
    
    // 解决var关键字声明提升的问题
    let divs = document.querySelectorAll('div');
    for(var i = 0 ; i<divs.length; i++) {
    	divs[i].addEventListener('click',(function(frozenCounter) {
    		return function() {
    			console.log(frozenCOunter);
    		};
    	})(i) );	//立即将i传入函数
    }
    

私有变量

  • 严格来讲,JavaScript 没有私有成员的概念,所有对象属性都公有的。但是有私有变量的概念,任何定义在函数或块中的变量,都可以认为是私有的,因为在这个函数或块的外部无法访问其中的变量。

  • 特权方法(privileged method)是能够访问函数私有变量(及私有函数)的公有方法。

    //在构造函数中实现特权方法
    function MyObject() {
    	//私有变量和私有函数
    	let privateVariable = 10;
    	
    	function privateFunction() {
    		return false;
    	}
    	
    	//特权方法
    	this.publicMethod = function() {
    		privateVariable++;
    		return privateFunction();
    	};
    }
    

    定义在构造函数中的特权方法其实是一个闭包,它具有访问构造函数中定义的所有变量和函数的能力。变量privateVariable 和函数privateFunction()只能通过publicMethod()方法来访问。

  • 静态私有变量

    • 特权方法也可以通过使用私有作用域定义私有变量和函数来实现。

      (function() {
      	//私有变量和私有函数
      	let privateVariable = 10;
      	
      	function privateFunction() {
      		return false;
      	}
      	
      	//构造函数
      	MyObject = function() {};
      	
      	//公有和特权方法
      	MyObject.prototype.publicMethod = function() {
      		privateVariable++;
      		return privateFUnction();
      	}
      })();
      

      这里声明MyObject 并没有使用任何关键字。因为不使用关键字声明的变量会创建在全局作用域中,所以MyObject 变成了全局变量,可以在这个私有作用域外部被访问。

    • 这个模式的特点是:私有变量和私有函数是由实例共享的。因为特权方法定义在原型上,所以同样是由实例共享的。特权方法作为一个闭包,始终引用着包含它的作用域。

    • 创建静态私有变量可以利用原型更好地重用代码,只是每个实例没有了自己的私有变量。

    注意 使用闭包和私有变量会导致作用域链变长,作用域链越长,则查找变量所需的时间也越多。

  • 模块模式

    • 模块模式是在单例对象基础上加以扩展,使其通过作用域链来关联私有变量和特权方法

      let singleton = function() {
      	//私有变量和私有函数
      	let privateVariable = 10;
      	
      	function privateFunction() {
      		return false;
      	}
      	
      	//特权/公有方法和属性
      	return {
              publicProperty: true,
              
              publicMethod() {
                  privateVariable++;
      			return privateFunction();
              }
      	};
      }();
      

      模块模式使用匿名函数返回一个对象,之后创建一个要通过匿名函数返回的对象字面量。

    • 本质上,对象字面量定义了单例对象的公共接口。下面例子创建一个管理组件的单例对象。

      let application = function() {
      	//私有变量和私有函数
      	let components = new Array();
      	
      	//初始化
      	components.push(new BaseComponent());
      	
      	//公共接口
      	return {
      		getComponentCount() {
      			return components.length;
      		},
      		registerComponent(component) {
      			if(typeof component == 'object') {
      				components.push(component);
      			}
      		}
      	};
      }();
      
  • 模块增强模式

    • 做法是在返回对象之前先对其进行增强。这适合单例对象需要是某个特定类型的实例,但又必须给它添加额外属性或方法的场景。

      let application = function() {
      	//私有变量和私有函数
      	let components = new Array();
      	
      	//初始化
      	components.push(new BaseComponent());
      	
      	//创建局部变量保存实例
      	let app = new BaseComponent();
      	
      	//公共接口
      	app.getComponentCount = function() {
      		return components.length;
      	};
      	app.registerComponent = function(component) {
      		if(typeof component === "object") {
      			components.push(component);
      		}
      	};
      	
      	//返回实例
      	return app;
      }();
      

      在给局部变量app 添加了能够访问私有变量的公共方法之后,匿名函数返回了这个对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值