javascript 函数的进阶

529 阅读10分钟

涉及的函数知识

  • 熟练函数的多种定义和调用方式。
  • 能够理解和改变函数内部 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. 求 1 * 2 * 3... * n 阶乘。

     function fn(n){
         if(n == 1){
             return  1
         }
         return n * fn(n - 1)
     }
     console.log(fn(3));
    

递归只有在触发到 return 退出条件才会开始返回,否则就会一直执行下去,不断的开辟新的栈来创建函数。

  1. 利用递归函数求斐波那契数列(兔子序列)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

可以看到利用深拷贝的方法,之后改变原拷贝对象属性的值,拷贝对象第二层的属性依旧是拷贝时的值。