JS高级 之【函数】

一、箭头函数

如果不使用大括号,那么箭头后面就只能有一行代码,比如一个赋值操作,或者一个表达式。而且,省略大括号会隐式返回这行代码的值:

//这是一种错误的写法
let mutiply = (a,b)=> return a * b;

分析:这种写法会报错,因为箭头函数 如果没有加{}的话,会隐式返回这行代码的值,不需要retrun ;

//正确的写法:
let mutiply = (a,b) => a * b;

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

下面是个很有意思的代码,可以让你更好的理解,函数名是一个指针

function sum (num1,num2){
	return num1 + num2;
}
console.log(sum(10,10));// 20
let anotherSum = sum;
console.log(anotherSum(10,10));// 20

sum = null;
console.log(anotherSum(10,10));//20

以上代码定义了一个名为sum()的函数,用于求两个数之和。然后又声明了一个变量anotherSum,并将它的值设置为等于sum。注意,使用不带括号的函数名会访问函数指针,而不会执行函数。此时,anotherSum和sum都指向同一个函数。调用anotherSum()也可以返回结果。把sum设置为null之后,就切断了它与函数之间的关联。而anotherSum()还是可以照常调用,没有问题。

二、函数作为值

首先先了解一下,形参和实参的区别:

	function funName(形参){}//定义函数
	funName(实参);//调用函数
	

关于更加具体的了解函数的length:请看下面的链接

一个典型的例子,能够的帮助我们理解:

从一个函数中返回另一个函数也是可以的,而且非常有用。例如,假设有一个包含对象的数组,而我们想按照任意对象属性对数组进行排序。为此,可以定义一个sort()方法需要的比较函数,它接收两个参数,即要比较的值。但这个比较函数还需要想办法确定根据哪个属性来排序。这个问题可以通过定义一个根据属性名来创建比较函数的函数来解决。比如:

function createComparisonFunction(proptyName){
	return function (object1,object2){
		let value1 = object1[proptyName];
		let value2 = object2[proptyName];
		if(value1 < value2){
			return -1;
		}else if( value1 > value2){
			return 1;
		}else{
			return 0;
		}
	}
}

这个函数的语法乍一看比较复杂,但实际上就是在一个函数中返回另一个函数,注意那个return操作符。内部函数可以访问propertyName参数,并通过中括号语法取得要比较的对象的相应属性值。取得属性值以后,再按照sort()方法的需要返回比较值就行了。这个函数可以像下面这样使用:

这个函数的使用:

let data = [
	{name:"Zachary",age:28},
	{name:"Nicholas",age:29}
];
data.sort(createComparisonFunction("name"));
console.log(data[0].name);//Nicholas
data.sort(createComparisonFunction("age"));
console.log(data[0].name);//Zachary

在上面的代码中,数组data中包含两个结构相同的对象。每个对象都有一个name属性和一个age属性。默认情况下,sort()方法要对这两个对象执行toString(),然后再决定它们的顺序,但这样得不到有意义的结果。而通过调用createComparisonFunction(“name”)来创建一个比较函数,就可以根据每个对象name属性的值来排序,结果name属性值为"Nicholas"、age属性值为29的对象会排在前面。而调用createComparisonFunction(“age”)则会创建一个根据每个对象age属性的值来排序的比较函数,结果name属性值为"Zachary"、age属性值为28的对象会排在前面。

这个有个疑惑的点就是函数返回的函数没有被执行,其实是因为sort可以接收一个比较函数,然后执行,可以参考数组的sort方法

在这里插入图片描述

三、函数中的this指向

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

看一下的例子,方便理解:

function King(){
            this.royaltyName = 'Henry';
            //this 引用King的实例
            setTimeout(()=>{console.log(this.royaltyName,this)},5000);
        }
        function Queen(){
            this.royaltyName = 'Elizabeth';
            //this引用window对象
            setTimeout(function(){console.log(this.royaltyName,this)},5000)
        }
        new King();//Henry King {royaltyName: 'Henry'}
        new Queen();// undefined window(...)

导致上面发生的原因是,在箭头函数中,this引用的是定义箭头函数的上下文,而 定义的全局函数则是,谁调用(并且在哪里调用)的this指向谁。

四.函数的两个方法

1.apply

这个方法会以指定的this值来调用函数,即会设置调用函数时函数体内this对象的值。

使用的方式如下: 第一个参数是指定调用函数的对象也就是this,第二个参数就是需要的参数,必须是个数组。

function sum (num1,num2){
	return num1 + num2;
}
sum.apply(this,[10,10]);//20

2.call()

作用同上面一样,区别就是传参的方式不同:call 第一个参数也是调用函数的对象,即this,第二个参数就是参数,是一个个传递进去的,不是对象

总结

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

window.color = 'red';
let o = {
	color:'blue'
};
function sayColor(){
	console.log(this.color);
}
sayColor();// red
sayColor.apply(this);//red
sayColor.call(window);// red
sayColor.call(o);// blue

这个例子是在之前那个关于this对象的例子基础上修改而成的。同样,sayColor()是一个全局函数,如果在全局作用域中调用它,那么会显示"red"。这是因为this.color会求值为window.color。如果在全局作用域中显式调用sayColor.call(this)或者sayColor.call(window),则同样都会显示"red"。而在使用sayColor.call(o)把函数的执行上下文即this切换为对象o之后,结果就变成了显示"blue"了。

这个例子是在之前那个关于this对象的例子基础上修改而成的。同样,sayColor()是一个全局函数,如果在全局作用域中调用它,那么会显示"red"。这是因为this.color会求值为window.color。如果在全局作用域中显式调用sayColor.call(this)或者sayColor.call(window),则同样都会显示"red"。而在使用sayColor.call(o)把函数的执行上下文即this切换为对象o之后,结果就变成了显示"blue"了。

3.length和propotype

函数中有两个属性就是length和propotype: length 是用来返回函数的命名函数的传入形参的个数。
propotype属性也许是ECMAScript核心中最有趣的部分。propotype是保存引用类型所有实例方法的地方,这意味着toString()、valueOf()等方法实际上都保存在propotype上,进而由所有实例共享。这个属性在自定义类型时特别重要。

4.bind方法

ECMAScript 5出于同样的目的定义了一个新方法:bind()。bind()方法会创建一个新的函数实例,其this值会被绑定到传给bind()的对象。比如:

window.color = 'red';
let o = {
	color:'blue'
};
function sayColor(){
	console.log(this.color);
}
let objectSayColor = sayColor.bind(o);
objectSayColor()

对函数而言,继承的方法toLocaleString()和toString()始终返回函数的代码。返回代码的具体格式因浏览器而异。有的返回源代码,包含注释,而有的只返回代码的内部形式,会删除注释,甚至代码可能被解释器修改过。由于这些差异,因此不能在重要功能中依赖这些方法返回的值,而只应在调试中使用它们。继承的方法valueOf()返回函数本身。

五.递归

我自己的理解就是:在一个函数中调用自己的函数就是递归。

下面是一个经典的递归函数:

function factorial(num){
	if(num<= 1){
		return 1;
	}esle{
		return num * factorial(num - 1);
	}
}

上面的代码看起来不会有问题,但是看下面的代码,你会发现bug:

var anotherFactorial = factorial;
factorial = null;
console.log(anotherFactorial(5));//Uncaught TypeError: factorial is not a function 

这里把factorial()函数保存在了另一个变量anotherFactorial中,然后将factorial设置为null,于是只保留了一个对原始函数的引用。而在调用anotherFactorial()时,要递归调用factorial(),但因为它已经不是函数了,所以会出错。在写递归函数时使用arguments.callee可以避免这个问题。

arguments.callee就是一个指向正在执行的函数的指针,因此可以在函数内部递归调用,如下所示:

function factorial(num){
	if(num <= 1){
		return 1;
	}else{
		return num * arguments.callee(num - 1);
	}
}

像这里加粗的这一行一样,把函数名称替换成arguments.callee,可以确保无论通过什么变量调用这个函数都不会出问题。因此在编写递归函数时,arguments.callee是引用当前函数的首选。

不过,在严格模式下运行的代码是不能访问arguments.callee的,因为访问会出错。此时,可以使用命名函数表达式(named function expression)达到目的。比如:

const factorial = (function f(num){
	if(num <= 1){
		return 1;
	}esle{
		return num * f(num - 1);
	}
})

这里创建了一个命名函数表达式f(),然后将它赋值给了变量factorial。即使把函数赋值给另一个变量,函数表达式的名称f也不变,因此递归调用不会有问题。这个模式在严格模式和非严格模式下都可以使用。

六.尾调用优化

ECMAScript 6规范新增了一项内存管理优化机制,让JavaScript引擎在满足条件时可以重用栈帧。具体来说,这项优化非常适合“尾调用”,即外部函数的返回值是一个内部函数的返回值。比如:

function outerFunction(){
	return innerFunction();//尾调用
}

在ES6优化之前,执行这个例子会让在内存中发生如下操作:

(1)执行到outerFunction函数体,第一个栈帧被推到栈上。

(2)执行outerFunction函数体,到return语句。计算返回值必须先计算innerFunction。

(3)执行到innerFunction函数体,第二个栈帧被推到栈上。

(4)执行innerFunction函数体,计算其返回值。

(5)将返回值传回outerFunction,然后outerFunction再返回值。

(6)将栈帧弹出栈外。

在ES6优化之后,执行这个例子会在内存中发生如下操作:

(1)执行到outerFunction函数体,第一个栈帧被推到栈上。

(2)执行outerFunction函数体,到达return语句。为求值返回语句,必须先求值innerFunction。

(3)引擎发现把第一个栈帧弹出栈外也没问题,因为innerFunction的返回值也是outerFunction的返回值。

(4)弹出outerFunction的栈帧。

(5)执行到innerFunction函数体,栈帧被推到栈上。

(6)执行innerFunction函数体,计算其返回值。

(7)将innerFunction的栈帧弹出栈外。

总结:

很明显,第一种情况下每多调用一次嵌套函数,就会多增加一个栈帧。而第二种情况下无论调用多少次嵌套函数,都只有一个栈帧。这就是ES6尾调用优化的关键:如果函数的逻辑允许基于尾调用将其销毁,则引擎就会那么做。

大无语事件,居然不知道能不能优化:

现在还没有办法测试尾调用优化是否起作用。不过,因为这是ES6规范所规定的,兼容的浏览器实现都能保证在代码满足条件的情况下应用这个优化。

7.闭包

我自己的理解就是:在外部函数中返回内部函数,内部函数引用了外部函数的变量,就是闭包,闭包会导致内存泄漏。

//直接上案例:
window.identity = 'The Window';
let object = {
	identity:'my object',
	getIdentityFunc(){
	//返回的是一个匿名函数
	return function(){
		return this.identity;
	}
	}
};
console.log(object.getIdentityFunc()());//'The Window'

这里我们有一个疑问,为什么匿名函数没有使用其包含作用域(getIdentityFunc())的this对象呢?

重要首先匿名函数的执行环境具有全局性,因此其 this 对象通常指向 window

前面介绍过,每个函数在被调用时都会自动创建两个特殊变量:this和arguments。内部函数永远不可能直接访问外部函数的这两个变量。但是,如果把this保存到闭包可以访问的另一个变量中,则是行得通的。比如:

改变方法:

window.identity = 'The Window';
let object = {
	identity : 'My Object',
	getIdentityFunc(){
		let _this = this;
		return function(){
			return _this.identity;
		};
	}
};
console.log(object.getIdentityFunc()());//'My Object'

这里加粗的代码展示了与前面那个例子的区别。在定义匿名函数之前,先把外部函数的this保存到变量_this中。然后在定义闭包时,就可以让它访问_this,因为这是包含函数中名称没有任何冲突的一个变量。即使在外部函数返回之后,that仍然指向object,所以调用object.getIdentityFunc()()就会返回"My Object"。

this和arguments都是不能直接在内部函数中访问的。如果想访问包含作用域中的arguments对象,则同样需要将其引用先保存到闭包能访问的另一个变量中。

看几个改变this的特殊案例:

window.identity = 'The Window';
let object = {
	identity : 'My Object',
	getIdentity(){
			return this.identity;
		};
};

object.getIdentity();//My Object;
(object.getIdentity)();//My object;
(object.getIdentity = object.getIdentity)();// The Window

第一行调用object.getIdentity()是正常调用,会返回"My Object",因为this.identity就是object.identity。第二行在调用时把object.getIdentity放在了括号里。虽然加了括号之后看起来是对一个函数的引用,但this值并没有变。这是因为按照规范,object.getIdentity和(object.getIdentity)是相等的。第三行执行了一次赋值,然后再调用赋值后的结果。因为赋值表达式的值是函数本身,this值不再与任何对象绑定,所以返回的是"The Window"。

7.内存泄露

直接上案例:

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

以上代码创建了一个闭包,即element元素的事件处理程序(事件处理程序将在第13章讨论)。而这个处理程序又创建了一个循环引用。匿名函数引用着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,包含函数的活动对象上还是保存着对它的引用。因此,必须再把element设置为null。这样就解除了对这个COM对象的引用,其引用计数也会减少,从而确保其内存可以在适当的时候被回收。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值