涉及的函数知识
- 熟练函数的多种定义和调用方式。
- 能够理解和改变函数内部 this 的指向。
- 理解严格模式的特点。
- 可以熟练把函数作为参数和返回值传递。
- 理解出闭包的作用。
- 能够说出递归的两个条件。
- 理解深拷贝和浅拷贝的区别。
函数的定义和调用
1.1 函数的定义方式
1. 函数声明方式 function 关键字(命名函数)
function fn1() {
console.log('hello javascript')
}
fn1()
//输出结果:hello javascript
2. 函数的表达式(匿名函数)
let fn2 = function(){
console.log('hello javascript')
}
fn2()
//输出结果:hello javascript
3. 使用 new Function() 来定义函数
注意: 只用于了解不推荐使用,原因:效率低,而且代码书写不便于查看,推荐使用上面两种简写的方式。上面两种是第三种的简写方式,实际上所有的函数都是 Function 的实例(或者叫对象)
//利用 new Function ('参数', '参数2', '函数体')
let fn3 = new Function('a','b','console.log(a + b)')
fn3('hello ', 'javascript')
//输出结果:hello javascript
得出结论 javascript 中函数也属于对象,结合前面的构造函数,对象实例,原型对象我们可以画出它的关系图。
1.2 函数的调用方式
1.普通函数
function fn(){
console.log('hello')
}
fn() //输出结果: hello
fn.call() //输出结果: hello
2.对象的方法
class Fun{
fn(){
console.log('hello')
}
}
let obj = new Fun()
obj.fn() //输出结果: hello
//or
let Fun2 = {
fn: function(){
console.log('hello')
}
}
Fun2.fn() //输出结果: hello
3.构造函数
function Fun3(){
this.fn = function(){
console.log('hello')
}
}
fun3 = new Fun3()
fun3.fn() //输出结果: hello
4.绑定事件函数
//假设 html 结构有一个 id 是 btn 按钮
<button id="btn">调用fn</button>
//javascript
const btn = document.querySelector('#btn')
//点击 button 按钮调用函数
btn.onclick = function(){
console.log('hello')
}
5.定时器函数
setInterval(function () {
console.log('hello')
}, 1000)
6.立即执行函数
//立即执行函数是自动调用
(function (){
console.log('hello')
})()
2.this
2.1 函数内 this 的指向
2.2 改变函数内部的 this 指向
bind() 方法
let o = {
name: 'andy'
}
function fn(a, b){
console.log(a + b)
console.log(this)
}
let f = fn.bind(o ,1 ,2)
f() 结果 3 对象 o
- bind() 不会改变原来的函数。
- bind() 不会调用函数。
- bind() 会返回一个改变了 this 指向的新拷贝的函数。
- bind() 可以传值
- 有很多情况下函数我们不需要立即调用(比如定时器),但又想改变函数内部的 this 指向。
例子:我们有一个按钮,当我点击了之后,就看禁用这个按钮,3秒之后再开启这个按钮。
let btn = document.querySelector('#btn')
btn.onclick = fn
function fn() {
this.disabled = true
//箭头函数不需要改变 this 指向也可以
// setTimeout(() => {
// this.disabled = false
// console.log(this)
// }, 3000)
//假如不用箭头函数的情况下
setTimeout(function () {
this.disabled = false
}.bind(this), 3000)
console.log('调用')
}
call() 方法
function Father(uname) {
this.name = uname
}
Father.prototype = {
sing(){
console.log('唱歌')
}
}
Father.prototype.constructor = Father
function Son(uname) {
Father.call(this, uname)
this.sonSing = function(){
console.log(this)
console.log('儿子唱歌')
}
}
Son.prototype = new Father()
Son.prototype.constructor = Son
let ldh = new Son('刘德华')
console.log(ldh.name)
ldh.sonSing()
ldh.sing()
在调用父构造函数的同时,把父构造函数中的 this 指向子构造函数,这样子构造函数就可以使用父构造函数的属性了。
apply() 方法
let o = {
name: 'andy'
}
function fn(arr){
console.log(this)
}
fn() //打印 window
fn.apply() //打印 window
fn.apply(o) //打印 o 对象
//传递参数必须是数组(伪数组)
fn.apply(o, ['Hello']) //打印 Hello
- 可以调用函数。
- 可以改变函数内部 this 指向问题。
- 但是它的参数必须是数组(伪数组)
- 通过 apply() 传递的数组参数打印出来的结果是字符串。
例子:利用 apply() ,借助于 Math 中的内置对象求最大值和最小值。
let numArr = [10, 5, 100, 200]
let max = Math.max.apply(Math, numArr)
let min = Math.min.apply(Math, numArr)
console.log(max, min);
总结
相同点:
- 都可以改变函数内部的 this 指向。
区别点:
- call 和 apply 都会调用函数,并且改变函数的 this 指向。
- call 和 apply 传递的参数不一样,call 传递 aru1, aru2...形式, apply 必须是数组形式。
- bind 不会直接调用,可以改变函数内部 this 指向。
应用场景:
- call 经常做继承。
- apply 经常和数组有关系的,比如根据 Math 对象实现获取最大值和最小值。
- bind 不用调用函数,但是需要改变函数内部的 this 指向,比如改变定时器内部的 this 指向。
3.严格模式
3.1 什么是严格模式
- JS 在 ES5 之后提供了严格模式( strict mode )。
- IE10 以上的版本才支持,旧版本浏览器会被忽略。
- 消除了 Javascript 中的一些不合理,不严谨之处,减少了一些怪异行为。
- 提高编译器效率,增加运行速度。
- 禁用了在 ECMAScript 的未来版本中可能会定义的一些语法,为未来新版本的 Javascript 做好铺垫。比如一些保留字,例如:class, enum, export, extends, import, super 不能作为变量名。
语法:
use strict
为脚本添加严格模式:
<script>
'use strict'
//下面的代码就会以严格模式来执行
</script>
为立即执行函数添加严格模式:
<script>
(function(){
'use strict'
//下面的代码就会以严格模式来执行
})()
</script>
为函数添加严格模式:
<script>
function fn(){
'use strict'
//下面的代码就会以严格模式来执行
}
function fn2(){
//这里面的代码还是会以普通模式执行
}
</script>
3.2 严格模式中的变化
1.变量的规定
-
在正常模式中,如果一个变量没有被声明就复赋值,默认是全局变量,严格模式是精致这种用法的,变量都必须先用 var 或 let 或 const 命令声明,然后再使用。 不声明也可以成功打印:
num = 10 console.log(num) // 10
严格模式下(必须先声明再使用):
'use strict'
num = 10
console.log(num) // num is not defined
严格模式下严禁删除变量
'use strict'
let num = 10
delete num
console.log(num) // Delete of an unqualified identifier in strict
2.严格模式下 this 指向问题
-
以前在全局作用域中 this 的指向是 window 对象。
function fn() { console.log(this) } fn() //Window -
在严格模式中指向 undefined 。
'use strict' function fn() { console.log(this) } fn() //undefined -
以前构造函数时不加 new 也可以调用,当普通函数, this 指向全局。
function Star(uname, age) { this.name = uname this.age = age } //注意://在严格模式构造函数不加 new 来使用会报错,因为 this 指向已经不是 window 。 Star('刘德华', 33) console.log(window.name) // 刘德华 console.log(window.age) // 33
严格模式下使用构造函数:
'use strict'
function Star(uname, age){
this.name = uanme
this.age = age
}
let ldh = new Star('刘德华', 33)
console.log(ldh.name) // 刘德华
console.log(ldh.age) // 33
- 定时器 this 指向 window ,这个没有变化。
- 事件和对象还是指向调用者,这个没有变化。
2.严格模式下函数变化问题
-
严格模式之前是可以有两个一样的形参也可以被解析。
function fn(a, a) { console.log(a + a) } fn(1, 2) //4 //这里结果为 4 的原因是: //首先调用函数将 1 赋值给了 a //然后又将 2 赋值给了 a ,这样一来重复的就被覆盖掉了 //所以结果为 4 -
严格模式下会形参重复会报错。
'use strict' function fn(a, a) { console.log(a + a) } fn(1, 1) //Duplicate parameter name not allowed in this context -
函数必须声明再顶层,新版本的 Javascript 会引入 “块级作用域”(ES6中已引入)。为了和新版本接轨。不允许在非函数的代码块中声明函数(大致意思就是不允许在 {} 中声明变量,例如 if 语句, for 语句都带有 {} ,当然function(){} 这里是被允许的(函数中嵌套函数是被允许的))。
-
在严格模式中不允许有八进制。
4.高阶函数
高阶函数并不是指很高级的函数,只要满足了以下两个条件都可以称之为高阶函数:
- 它可以接受函数作为参数。
- 它可以将函数作为返回指输出。
例如:将函数作为参数(典型应用回调函数)
//这里的接收方 fn 为高阶函数
function fn(data, callback) {
console.log(data)
callback && callback();
}
fn1('javascript', function () { console.log('hello') })
JQuery 高阶函数应用例子:
<button id="btn">监听点击回调函数</button>
<script>
//这里就是典型的回调函数,on() 可称之为高阶函数。
$('#btn').on('click', function(){
console.log('被点击')
})
</script>
例如:将函数作为返回值
//这里的返回方 fn() 为告阶函数
function fn() {
function fnChild() {
console.log('hello')
}
return fnChild()
}
fn() //hello
5.闭包
5.1 变量作用域
变量根据作用域的不同分为两种:全局变量和局部变量。
- 在函数外部为全局变量。
- 在函数内部为局部变量。
- 全局变量在函数中也可一使用。
- 局部变量在函数外是无法使用的。
- 但函数执行完毕,本作用域内的局部变量也会被销毁。
5.2 什么是闭包
闭包( closure )就是指有权访问另一个函数作用域中变量的函数。
简单理解就是,一个作用域可以访问例外一个函数内部的局部变量
闭包例子:
function fn() {
var num = 10
function fn1() {
console.log(num)
}
fn1()
}
fn()
闭包可以理解为是一种现象
- 我在 fn1 的作用域里面打印了 fn 作用域声明的变量,就是属于使用了不同作用域的变量,此时就 fn 产生了闭包的现象(被访问的变量所在的函数就是一个闭包函数)。
5.3 闭包的作用
可以有在全局作用域使用局部变量的现象,延伸了变量的作用范围。
例子:
function fn(){
let num = 11
function fn1(){
console.log(num)
}
return fn1
}
let f = fn()
f()
这里 fn 把 fn1 返回来,用 f 接收就类似于 let f = function fn1(){console.log(num)} ,然后又调用了 f() ,注意这里的 fn1 是 fn 的嵌套函数所有 fn1 是有权访问 fn 的 num 变量的,所以可以正常打印。
闭包简单写法:(可以直接 return ,之前说能返回一个函数的函输也是高阶函数,闭包就是典型的高阶函数)。
function fn(){
let num = 190
return function (){
let num2 =10
console.log(num + num2)
}
}
let fns = fn()
fns()
闭包案例: 点击 li 输出当前 li 的索引号
-
html (之后的例子也是用这段)
<ul> <li>苹果</li> <li>西瓜</li> <li>桃子</li> <li>哈密瓜</li> </ul> -
利用动态添加属性的方式
let lis = document.querySelector('ul').querySelectorAll('li') for (var i = 0; i < lis.length; i++){ lis[i].index = i lis[i].onclick = function (){ console.log(this.index) } } -
利用闭包的方式得到当前 li 的索引号 这里绑定的匿名函数使用了立即执行函数的值,所以立即执行函数产生了闭包现象。
let lis = document.querySelector('ul').querySelectorAll('li') for (var i = 0; i < lis.length; i++){ (function (ii){ lis[ii].onclick = function (){ console.log(ii) } })(i) }
定时器例子:3 秒之后再打印 li 的内容( 错误写法 )
let lis = document.querySelector('ul').querySelectorAll('li')
for (var i = 0; i < lis.length; i++){
setTimeout(function (i){
console.log(lis[i].innerText) //undefined
},3000)
}
这里 undefined 的原因主要是因为,定时器属于异步任务,儿 for 循环属于同步应用,即使定时器的事件延迟为0,在 for 循环执行的时候定时器任务也会被放入任务队列中,并不会马上执行。
-
正确写法(还是利用闭包,将会因为 var 而改变的值以参数的方式传给闭包函数):
let lis = document.querySelector('ul').querySelectorAll('li') for (var i = 0; i < lis.length; i++){ (function (i){ setTimeout(function (){ console.log(i) console.log(lis[i]) console.log(lis[i].innerText) },3000) })(i) }
闭包总结:
- 闭包是一个函数。
- 闭包延伸了变量的作用范围。
6.递归
6.1 什么是递归
如果一个函数在内部可以调用其本身,那么这个函数就是递归函数。 简单理解:函数的内部自己调用自己,这个函数就是递归函数。 递归函数和循环是有点相似的。
递归很容易发生 “栈溢出” 错误( stack overflow ),所以必须要加退出条件 return 。
典型的死递归例子:
function fn(){
fn()
}
fn()
正确的递归例子:
let num = 1
function fn(){
console.log('打印 6 句话')
if( num == 6 ){
return //递归必须加退出条件
}
num++
fn()
}
fn()
结果:
注意在递归里面必须加入退出条件 return ,否则就会变成死递归。
6.2 利用递归求数学题:
-
求 1 * 2 * 3... * n 阶乘。
function fn(n){ if(n == 1){ return 1 } return n * fn(n - 1) } console.log(fn(3));
递归只有在触发到 return 退出条件才会开始返回,否则就会一直执行下去,不断的开辟新的栈来创建函数。
-
利用递归函数求斐波那契数列(兔子序列)1、1、2、3、5、8、13、21... ,用户输入一个数字 n 就可以求出这个数字对应的兔子序列值。 //(斐波那契数列)我们只需要知道用户输入的第 n 项是前面的两项的和就可以了,再返回用户输入的项的值是多少。
function fb(n){ if(n == 1 || n == 2){ return 1 } return fb( n-1 ) + fb(n - 2) }
console.log(fb(6)); //打印结果 8 。
6.4 利用递归遍历数据
例子:用户输入 id 进行查询对应的元素
let data = [ { id:1, name: '家电', goods: [ { id: 11, name: '冰箱', goods:[ { id: 111, name: '海尔冰箱' }, { id: 111, name: '海尔冰箱' } ]
},
{
id: 12,
name: '洗衣机'
}
]
},
{
id:2,
name: '服装'
}
]
//我们想要做输入 id 号就可以返回数据对象。
//1. 利用 forEach 去遍历每一个对象
function getId(json, id){
//声明一个对象用于保存
let o = {};
json.forEach(function (item, index,arr){
if (item.id == id){
//将找到的数据保存到赋值给 o
o = item
}else if(item.goods && item.goods.length > 0){
//遍历 goods 的数据就可以用到递归方法
//这里一定要再次赋值,因为递归的这个函数最后是返回了一个 o ,简单描述(这里的递归一次它在第二层,你的最终输出是在第 0 层,要等它返回到第 1 层,然后才由第 1 层的 return o 返回给第 0 层)
o = getId(item.goods ,id)
}
})
return o
}
console.log(getId(data, 1).name);
console.log(getId(data, 12).name);
console.log(getId(data, 111).name);
6.5 浅拷贝和深拷贝
6.5.1 浅拷贝
浅拷贝只是拷贝一层,更深层次对象级别的只拷贝引用地址。
例子:for..in 遍历拷贝(浅拷贝)
let obj = {
id: 1,
name: 'andy',
msg: {
age: 18
}
}
let o = {};
//利用 for..in 遍历拷贝对象
for (let key in obj){
//这里的写法一定要注意:
// key 是属性名 obj[key] 是属性值
//console.log(obj.key) 错误理解的写法
o[key] = obj[key]
}
obj.name = 'my'
obj.msg.age = '20'
console.log('我是第一层:',obj.name)
console.log('我是第二层:',obj.msg.age)
console.log('我是第一层:',o.name)
console.log('我是第二层:',o.msg.age)
从结果可以看出来,浅拷贝只能拷贝第一层的对象属性和值,对于第二层的对象不是拷贝而是引用(证明:因为当我们尝试修改被原对象的第一次属性 name 时,拷贝的对象的 name 属性没有发生变化,这是因为它们是各自独立的,但是修改第二层的 msg 下面的 age 属性的值时会影响到拷贝对象的 msg 下的 age 的值,因为他们是拷贝的是引用地址)。
在 ES6 中有一个新增的语法糖可以直接实现浅拷贝
// ( target 参数 拷贝好的对象 )
// ( ...sources 被拷贝的对象 )
Object.assign(target, ...sources)
例子:浅拷贝(使用 ES6 语法糖)
let obj = {
id: 1,
name: 'andy',
msg: {
age: 18
}
}
let o = {};
// ( target 参数 拷贝好的对象 )
// ( ...sources 被拷贝的对象 )
Object.assign(o, obj)
console.log(o) //结果 {id: 1, name: "andy", msg: {…}}
6.5.2 深拷贝
深拷贝拷贝多层,每一级别的数据都会拷贝。
例子:使用函数递归的方式来实现深拷贝
let obj = {
id: 1,
name: 'andy',
msg: {
age: 18
},
color: ['pink','red']
}
let o = {};
function deepCopy (new_obj, old_obj) {
for (let key in old_obj){
//判断我们的属性值属于那种数据类型
let item = old_obj[key]
//判断是否是数组
if(item instanceof Array){
//如果是一个数组,那么就在新对象里面创建当前 key 属性的并且给一个空数组
new_obj[key] = []
//这里给了一个空数组,之后就可已利用递归的方式继续拷贝
deepCopy(new_obj[key], item)
}else if(item instanceof Object){
new_obj[key] = {}
deepCopy(new_obj[key], item)
}else{
new_obj[key] = item
}
//判断是否是对象
//如果都不是就属于简单数据类型
}
}
deepCopy(o, obj)
console.log(o)
注意:一定要先判断数组再判断对象,因为再 js 中数组也属于对象
let arr = [1, 2, 3]
console.log(arr instanceof Object) //true
可以看到利用深拷贝的方法,之后改变原拷贝对象属性的值,拷贝对象第二层的属性依旧是拷贝时的值。