你疏漏的 JS 函数硬核知识?这里帮你总结了
Posted 几何心凉
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了你疏漏的 JS 函数硬核知识?这里帮你总结了相关的知识,希望对你有一定的参考价值。
重点
更多前端知识 诚邀各位前端从事者爱好者加入前端大佬技术交流社区,本社区主要分享技术栈、个人心得、技术交流、问题解惑等前端体系交流
点击下方文字加入
1. 函数的定义和调用
1.1 函数的定义方式
-
方式1 函数声明方式 function 关键字 (命名函数)
function f1() { console.log('命名函数') }
-
方式2 函数表达式(匿名函数)
var f2 = function () { console.log('匿名函数') }
-
方式3 new Function()
/* 参数1:函数形参 参数2:函数形参 参数3:函数体 */ var f3 = new Function('m', 'n', 'console.log(m-n)') // 传入实参 f3(4,2)
注意
- Function 里面参数都必须是字符串格式
- 第三种方式执行效率低,也不方便书写,因此较少使用
1.2 函数的调用
// 普通函数:命名函数和匿名函数
function f1() {
console.log('人生何处不相逢');
}
var f2 = function () {
console.log('寒江孤影,江湖故人,相逢何必曾相识');
}
// 对象中的函数,专业点叫做方法,通过 对象.方法 方式调用
var Person = {
speak: function () {
console.log('人生处处是歌声');
}
}
// 构造函数:通过 new 关键字调用
function Star() { }
new Star()
// 事件处理函数:事件触发时被调用
element.onclick = function () { }
// 定时器函数:由定时器选择时机调用
setInterval(function () { }, 1000)
// 立即执行函数:立即调用
(function () {
console.log('first off')
}())
2.this
2.1函数内部的this指向
这些 this 的指向,是当我们调用函数的时候确定的。调用方式的不同决定了this 的指向不同
一般指向我们的调用者.
2.2改变函数内部 this 指向
2.2.1 call方法
call()方法调用一个对象。简单理解为调用函数的方式,但是它可以改变函数的 this 指向
应用场景: 经常做继承.
var o = {
name: 'andy'
}
function fn(a, b) {
console.log(this);
console.log(a+b)
};
fn(1,2)// 此时的this指向的是window 运行结果为3
fn.call(o,1,2)//此时的this指向的是对象o,参数使用逗号隔开,运行结果为3
以上代码运行结果为:
复习使用this实现继承
2.2.2 apply方法
apply 方法的用于与call 非常类似,区别在于,不能以参数列表的形式传递实参,必须以数组的形式传递
var o = {
name: 'andy'
}
function fn(a, b) {
console.log(this);
console.log(a + b)
};
// 普通调用
fn(1, 2)
// call调用,将函数中的this修改为o对象
fn.call(o, 1, 2)
// apply 调用,区别在于参数列表必须以数组形式传递
fn.apply(o,[1,2])
可以看到,我们传入函数的实参是数组,但是函数内部会自动展开为参数列表
利用这一特性,我们可以结合 Math.max 方法求数组的最大值或者最小值
Math.max 方法可以求一组数字的最大值,语法如下
Math.max(1,2,3,4,5)
参数为参数列表形式
利用apply 可以将数组传入
var numbers = [3, 2, 44, 11, 66, 7]
var max=Math.max.apply(null,numbers)
console.log(max);
也可以利用 ES6 的 Spread syntax 语法
var max=Math.max(...numbers)
2.2.3 bind方法
https://developer.mozilla.org/zh-CN/docs/Web/javascript/Reference/Global_Objects/Function/bind
bind() 方法不会调用函数,但是能改变函数内部this 指向
如果只是想改变 this 指向,并且不想调用这个函数的时候,可以使用bind
应用场景:不调用函数,但是还想改变this指向
var o = {
name: 'andy'
};
function fn(a, b) {
console.log(this);
console.log(a + b);
};
var f = fn.bind(o, 1, 2); //此处的f是bind返回的新函数
f();//调用新函数 this指向的是对象o 参数使用逗号隔开
解惑
上面的代码
var f = fn.bind(o, 1, 2)
的作用就是将 fn 函数拷贝了一份,名称叫做 f,同时将函数 f 中的 this 修改为对象o,所以函数 f 与 fn 的函数体是一样的,只不过内部 this 的指向不同了
如下代码,才是调用执行函数 f,而上面的代码仅仅完成上面的任务,但不会执行函数 fn 也不会执行新函数 f
f()
应用
页面中有1个div,默认为红色,鼠标悬浮后,变为蓝色,3秒之后恢复成红色
var div = document.querySelector('div')
div.addEventListener('mouseover', function () {
this.style.backgroundColor = 'blue'
setTimeout(function () {
div.style.backgroundColor = 'red'
}, 3000);
})
定时器中,this=window,所以不能使用this,必须使用元素名称div
当然可以使用 var that=this 的方式
但这两种方式都有问题,如实现下面的效果
当前页面上有3个div,默认都为红色,鼠标悬浮到某个div上,颜色变为蓝色,3秒后恢复成红色
var divs = document.querySelectorAll('div')
for (var i = 0; i < divs.length; i++) {
divs[i].addEventListener('mouseover', function () {
// 这里的this=当前触发事件的某个具体div
this.style.backgroundColor = 'blue'
setTimeout(function () {
// 下面的代码应该怎么写
}, 3000);
})
}
定时器中的this=window,所以不能使用window
也不能使用divs[i],因为 i 的索引在事件发生时,并不是你想象中的索引
当然可以使用 that=this 的方式,但会多创建局部变量
可以利用bind方法
这里使用bind最合适,因为仅仅是修改了函数中this的指向,并不会马上执行,3秒之后由系统再次调用
注意:bind 方法会创建一个新函数,所以3秒后调用的是新函数,在新函数中,this=div
问?上面不是需要声明一个变量接收bind创建的新函数吗?为什么这里不需要?
同学,请先搞清楚:什么时候需要返回值,什么时候不需要
2.2.4 call、apply、bind三者的异同
-
共同点 : 都可以改变this指向
-
不同点:
- call 和 apply 会调用函数, 并且改变函数内部this指向.
- call 和 apply传递的参数不一样,call传递参数使用逗号隔开,apply使用数组传递
- bind 不会调用函数, 可以改变函数内部this指向.
-
应用场景
- call 经常做继承.
- apply经常跟数组有关系. 比如借助于数学对象实现数组最大值最小值
- bind 不调用函数,但是还想改变this指向. 比如改变定时器内部的this指向.
3.严格模式
3.1什么是严格模式
JavaScript 除了提供正常模式外,还提供了严格模式(strict mode)。ES5 的严格模式是采用具有限制性 JavaScript变体的一种方式,即在严格的条件下运行 JS 代码。
严格模式在 IE10 以上版本的浏览器中才会被支持,旧版本浏览器中会被忽略。
严格模式对正常的 JavaScript 语义做了一些更改:
1.消除了 Javascript 语法的一些不合理、不严谨之处,减少了一些怪异行为。
2.消除代码运行的一些不安全之处,保证代码运行的安全。
3.提高编译器效率,增加运行速度。
4.禁用了在 ECMAScript 的未来版本中可能会定义的一些语法,为未来新版本的 Javascript 做好铺垫。比如一些保留字如:class,enum,export, extends, import, super 不能做变量名
聊聊 TS
3.2开启严格模式
严格模式可以应用到整个脚本或个别函数中。因此在使用时,我们可以将严格模式分为为脚本开启严格模式和为函数开启严格模式两种情况。
- 脚本开启严格模式
- 函数开启严格模式
<script>
// 在当前脚本中启用严格模式:script开始标记和结束标记之间生效
// 'use strict'
var name='yhb'
age=20
function fn(){
// 在函数中开启严格模式
'use strict'
gender='男'
}
fn()
</script>
3.3严格模式中的变化
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Strict_mode
严格模式对 Javascript 的语法和行为,都做了一些改变。
'use strict'
num = 10
console.log(num)//严格模式后使用未声明的变量
--------------------------------------------------------------------------------
var num2 = 1;
delete num2;//严格模式不允许删除变量:删除变量的目的是希望释放内存,处理方式一般是将变量的值设置为null
--------------------------------------------------------------------------------
function fn() {
console.log(this); // 严格模式下全局作用域中函数中的 this 是 undefined
}
fn();
--------------------------------------------------------------------------------- function Star() {
this.gender = '男';
}
// 不加new 调用Star,则this=undefined
console.log(Star().gender)
// 加new 调用Star,则this=对象
console.log(new Star().gender)
----------------------------------------------------------------------------------
setTimeout(function() {
console.log(this); //严格模式下,定时器 this 还是指向 window
}, 2000);
另外,严格模式下函数的参数名称应该唯一
// 非严格模式下
function f1(m, m) {
console.log(m + m)
}
f1(1,2) // 4,分析一下原因为什么事4
严格模式下,会报错
4.高阶函数
高阶函数是对其他函数进行操作的函数,它接收函数作为参数或将函数作为返回值输出。
函数作为参数:
函数作为返回值
函数也是一种数据类型,同样可以作为参数,传递给另外一个参数使用。最典型的就是作为回调函数。
同理函数也可以作为返回值传递回来
函数作为参数案例:
function fn(m, n, callback) {
console.log(m + n)
// 函数主体执行结束后才会执行回调函数
callback && callback()
}
fn(3, 4, function () {
console.log('我就是回调函数')
})
jquery 中大量应用了回调函数,比如动画完成后执行某个操作
5.闭包
5.1变量的作用域复习
变量根据作用域的不同分为两种:全局变量和局部变量。
- 函数内部可以使用全局变量。
- 函数外部不可以使用局部变量。
- 当函数执行完毕,本作用域内的局部变量会销毁。
5.2什么是闭包
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures
在JS中,函数每次创建,都会生成闭包(closure),这个闭包包含函数及其词法环境(大概类似于执行上下文)
闭包是一种执行机制:内部函数总是可以访问其所在的外部函数中声明的变量和参数,即使在其外部函数执行结束之后
/*
在一个函数中又嵌套函数,在JS中是没有问题的
1)内部作用域可以访问外部作用域,所以f2中可以访问f1中的变量
2)在全局作用域内,不访问函数f1中的变量
*/
function f1() {
var m = 100
return function() {
console.log(m)
}
}
var myfunc=f1()
// myfunc 是一个变量,引用了f2的地址,myfunc() 就相当于执行了 f2()
myfunc() // f2()
// console.log(m)
通过上面案例发现:闭包可以延伸变量的作用范围。
解惑
一般情况下,下面代码执行后,外部函数f1就执行完毕了,那么函数内部的成员数据都会被销毁,包括变量 m,但是因为内部函数f2使用了变量m,而函数f2又被返回给了变量 myfunc,即变量myfuc引用了内部函数f2,此时
f2还没有执行,所以外部函数f1就不能释放自己的变量m
f1()
我们可以将 子函数f2 称作闭包函数(有争议)
我们再对闭包做一个总结:函数创建时,形成一个闭包,闭包让内部函数可以访问外部函数的变量和参数,并且通过向外返回内部函数,使得在函数外部也可以访问函数内部的数据
闭包的三个特性
1)函数嵌套函数
2)函数内部可以访问函数外部的变量和参数
3)外部函数执行完毕后,参数和变量不会被垃圾回收机制回收
5.3闭包的案例
- 利用闭包的方式得到当前li 的索引号
for (var i = 0; i < lis.length; i++) {
// 利用for循环创建了4个立即执行函数
// 立即执行函数也成为小闭包因为立即执行函数里面的任何一个函数都可以使用它的i这变量
(function(i) {
lis[i].onclick = function() {
console.log(i);
}
})(i);
}
但是,这种案例使用闭包其实并不是最好的解决方案,因为每次循环都要创建一个函数,而且每个i 的值都会被保存,不能释放,所以执行效率会低
将i的值存储于li标签的自定义属性中的方式更加可取
代码 (任务单)
- 闭包应用-3秒钟之后,打印所有li元素的内容
下面代码遍历所有li标签,创建了三个定时器,3秒之后打印每个li标签元素内容
for (var i = 0; i < lis.length; i++) {
(function(i) {
setTimeout(function() {
console.log(lis[i].innerhtml);
}, 3000)
})(i);
}
其实,使用我们前面学习的 bind 方法也是可以的
代码 (任务单)
- 闭包应用-计算打车价格
/*需求分析
打车起步价13(3公里内), 之后每多一公里增加 5块钱. 用户输入公里数就可以计算打车价格
如果有拥堵情况,总价格多收取2块钱拥堵费*/
var car = (function () {
var start_price = 13
var start_juli = 3
var total = 0
return {
price: function (juli) {
if (juli <= start_juli) {
total = start_price
} else if (juli > 3) {
total = start_price + (juli - start_juli) * 5
}
return total
},
yondu: function (flag) {
return flag ? total + 2 : total
}
}
})()
console.log(car.price(3));
console.log(car.price(10));
console.log(car.yondu(false));
console.log(car.yondu(true));
解惑
1、立即执行函数中的代码立即执行,不会等待调用,所以 变量 car 引用的是一个对象
2、引用的对象中包含两个函数 price 和 yongdu,这两个函数属于立即执行函数的子函数
3、子函数中引用了外部函数中的变量,所以形成了闭包
4、上面的案例并非非要这么编写程序,只是为了练习闭包的使用
6.递归
6.1什么是递归
**递归:**如果一个函数在内部可以调用其本身,那么这个函数就是递归函数。简单理解:函数内部自己调用自己, 这个函数就是递归函数
**注意:**递归函数的作用和循环效果一样,由于递归很容易发生“栈溢出”错误(stack overflow),所以必须要加退出条件return。
6.2利用递归求1~n的阶乘
//利用递归函数求1~n的阶乘 1 * 2 * 3 * 4 * ..n
function fn(n) {
if (n == 1) { //结束条件
return 1;
}
return n * fn(n - 1);
}
console.log(fn(3));
6.3利用递归求斐波那契数列
// 利用递归函数求斐波那契数列(兔子序列) 1、1、2、3、5、8、13、21...
// 用户输入一个数字 n 就可以求出 这个数字对应的兔子序列值
// 我们只需要知道用户输入的n 的前面两项(n-1 n-2)就可以计算出n 对应的序列值
function fb(n) {
if (n === 1 || n === 2) {
return 1;
}
return fb(n - 1) + fb(n - 2);
}
console.log(fb(3));
6.4利用递归遍历数据
// 我们想要做输入id号,就可以返回的数据对象
var data = [{
id: 1,
name: '家电',
goods: [{
id: 11,
gname: '冰箱',
goods: [{
id: 111,
gname: '海尔'
}, {
id: 112,
gname: '美的'
},
]
}, {
id: 12,
gname: '洗衣机'
}]
}, {
id: 2,
name: '服饰'
}];
//1.利用 forEach 去遍历里面的每一个对象
function getID(json, id) {
var o = {};
json.forEach(function(item) {
// console.log(item); // 2个数组元素
if (item.id == id) {
// console.log(item);
o = item;
return o;
// 2. 我们想要得里层的数据 11 12 可以利用递归函数
// 里面应该有goods这个数组并且数组的长度不为 0
} else if (item.goods && item.goods.length > 0) {
o = getID(item.goods, id);
}
});
return o;
}
重点
更多前端知识 诚邀各位前端从事者爱好者加入前端大佬技术交流社区,本社区主要分享技术栈、个人心得、技术交流、问题解惑等前端体系交流
点击下方文字加入
以上是关于你疏漏的 JS 函数硬核知识?这里帮你总结了的主要内容,如果未能解决你的问题,请参考以下文章
超硬核!16000 字 Redis 面试知识点总结,这还不赶紧收藏?
全网最硬核 Java 新内存模型解析与实验单篇版(不断更新QA中)
指针的这些知识你知道吗?C语言超硬核指针进阶版3w+字详解+指针笔试题画图+文字详细讲解