一文带你搞清楚JavaScript中函数this指向、高阶函数、闭包、递归

一、函数的定义和调用
1、函数的定义
(1)函数声明方式function关键字(命名函数)

function fun () { };

(2)函数表达式(匿名函数)

var fun = function() { };

(3)new Function()

new Function('arg1', 'arg2', '函数体');

// 使用 new Function() 定义
        var f = new Function('console.log(123)');
		f();
        var f = new Function('a', 'b', 'console.log(a+b)');
        f(1, 2);
		console.dir(f);  // ƒ anonymous(a,b) 匿名函数
        console.log(f instanceof Object);  // true
  • Function里面的参数必须都是字符串的格式
  • 这种方式执行效率低,也不方便书写,因此较少使用
  • 所有函数都是Function的实例对象
  • 函数也属于对象。js中万物皆对象!
    在这里插入图片描述
2、函数的调用
(1)普通函数
// 1. 普通函数
        function fn() {
            console.log('普通函数');
        }
        fn();
        fn.call();
(2)对象的方法
// 2. 对象的方法
        var o = {
            sayHi: function () {
                console.log('对象的方法');
            }
        };
		o.sayHi();
(3)构造函数
// 3. 构造函数
        function Star() {

        }
        new Star();
(4)绑定事件函数
// 4. 绑定事件函数
        btn.onclick = function () { };  // 点击了按钮就可以进行调用
(5)定时器函数
// 5. 定时器函数
        setInterval(function () {

        }, 1000);  // 该函数是定时器自动1秒钟调用一次
(6)立即执行函数
// 6. 立即执行函数
        (function () {
            console.log('立即执行函数');
        })();  // 立即执行函数是自动调用的
二、this指向
1、函数内的this指向

这些this的指向,是调用函数的时候确定的。调用方式的不同决定了this的指向不同。一般指向调用者。

调用方式this指向
普通函数调用window(完整写法是window.fn(),实际上的调用者是window,所以this指向window)
构造函数调用实例对象 原型对象中的方法也指向实例对象
对象方法调用该方法所属对象
事件绑定方法绑定事件对象
定时器函数window(window.setInterval(function(){}, 1000)😉
立即执行函数window
// 构造函数
        function Star() {
            // 内部this指向实例对象
        };
        Star.prototype.sing = function () {
            // 内部this指向实例对象
        };
        var ldh = new Star();
2、改变函数内部this指向

js专门提供了一些函数方法来更优雅地处理函数内部this的指向问题,常用的有bind()、call()、apply()三种方法。

(1)call方法

call方法可以调用一个函数。简单理解为调用函数的方式,但它可以改变函数的this指向。

// 1、call()
        var o = {
            name: 'andy'
        };
        function fn(a, b) {
            console.log(this);
            console.log(a + b);
        };
        // 调用函数并改变this指向
        fn.call(o, 1, 2);

call主要的作用是实现继承:

// call的主要作用可以实现继承
        function Father(uname, age, sex) {
            this.uname = uname;
            this.age = age;
            this.sex = sex;
        };
        function Son(uname, age, sex) {
            Father.call(this, uname, age, sex);
        };
        var son = new Son('ldh', 18, '男');
        console.log(son);
(2)apply方法

apply()方法调用一个函数。简单理解为调用函数的方式,但它可以改变函数的this指向。

fun.apply(thisArg, [argsArray])

  • thisArg:在fun函数运行时指定的this值
  • argsArray:传递的值,必须包含在数组
  • 返回值就是函数的返回值,因为它就是调用函数
// 2、apply()
        var o = {
            name: 'andy'
        };
        function fn(arr) {
            console.log(this);
            console.log(arr);  // 打印的是字符串'pink'
        };
        fn.apply(o, ['pink']);

apply主要的应用:可以利用apply借用数学内置对象求最大值

Math.max()方法接收的参数不是数组,而是单个数字;可以利用apply方法的特性,借助于数学内置对象求数组的最大值。

// apply主要应用:比如可以利用apply借用数学内置对象求最大值
        var arr = [1, 66, 3, 99, 4];
        var max1 = Math.max(arr);
        console.log(max1);  // NaN
        var max = Math.max.apply(Math, arr);
        console.log(max);  // 99

apply调用数学内置对象Math.max方法,并将函数的this指向Math对象;然后将数组arr传递给max,虽然传递的是数组,但在函数内部会将数组转换为我们想要的数据格式:比如数字型数组转换为数字型参数;字符串数组转换为字符串型参数。

(3)bind方法

bind()方法不会调用函数,但是能改变函数内部this指向。

fun.bind(thisArg, arg1, arg2, ...)

  • thisArg:在fun函数运行时指定的this值
  • arg1, arg2:传递的其他参数
  • 返回由指定的this值和初始化参数改造的新函数
// 3、bind()
        var o = {
            name: 'andy'
        };
        function fn(a, b) {
            console.log(this);
            console.log(a + b);
        };
        // 1. 不调用原函数 但是会改变原函数内部的this指针
        // 2. 返回的是原函数改变this后产生的新函数
        var f = fn.bind(o, 1, 2);
        f();

bind应用:有的函数不需要立即调用,但又想改变这个函数内部的this指向

// bind应用:有的函数不需要立即调用,但又想改变这个函数内部的this指向
        // 比如,有一个按钮,点击之后,就禁用,3秒后开启这个按钮
        var btn = document.querySelector('button');
        btn.onclick = function () {
            this.disabled = true;
            setTimeout(function () {
                // this.disabled = false; // 定时器的this指向window,所以3s后并不会开启
                this.disabled = false;
            }.bind(this), 3000);  // 外面的this指向的是btn对象,使用bind将定时器内部的this指针指向btn
        }

这里bind中绑定的this是btn对象,使用bind方法将定时器内部的this指针由window改为btn对象。

(4)使用bind修改面向对象Tab栏

在该案例中,如果函数内部想要使用全局的this,是通过在外部声明一个全局变量that将this存储起来,然后在函数内部调用的。

现在可以使用bind修改this指针的指向,但是又不能直接将点击事件绑定函数中的this直接修改为外部的this,因为绑定函数内部的this指针也需要使用。所以将外部的this当做参数传入事件绑定函数,然后在事件绑定函数中,使用形参进行接收。

// 将外部的this当做参数传入事件绑定函数
this.lis[i].onclick = this.toggleTab.bind(lis[i], this);

// 在事件绑定函数中,使用形参进行接收
    toggleTab(that) {
        that.clearClass();
        this.className = 'liactive';
        that.sections[this.index].className = 'conactive';
    }

这样的写法更加优雅,不需要每次使用当前类的时候,都需要带上that。

(5)call apply bind总结

相同点:都可以改变函数内部this指向。

区别

  1. call和apply会调用函数,并改变函数内部的this指向
  2. call和apply传递的参数不同,call传递参数arg1, arg2, … 的形式,apply必须传入数组形式[arg]
  3. bind不会调用函数,可以改变函数内部this指向

主要应用场景

  1. call经常做继承
  2. apply经常跟数组有关,比如借助于数学对象实现数组最大值最小值
  3. bind不调用函数,但又想改变this指向,比如改变定时器内部的this指向
三、严格模式
1、什么是严格模式

js除了提供正常模式外,还提供了严格模式(strict mode)。js变体的一种方式,即在严格的条件下运行js代码。

严格模式在IE10+版本以上的浏览器才支持,旧版本浏览器中会被忽略。

严格模式对正常的js语义做了一些更改:

  • 消除了js语法的一些不合理、不严谨之处,减少了一些怪异行为
  • 消除代码运行的一些不安全之处,保证代码运行的安全
  • 提高编译器效率,增加运行速度
  • 禁用了在ECMAScript的未来版本中可能会定义的一些语法,为未来新版本的js做好铺垫。比如一些保留字如:class、enum、export、extends、import、super等,不可以做变量名。
2、开启严格模式

严格模式可以应用到整个脚本或者个别函数中。因此在使用时,我们可以将严格模式分为为脚本开启严格模式为函数开启严格模式两种情况。

(1)为脚本开启严格模式

为整个脚本文件开启严格模式,需要在所有语句之前放一个特定语句"use strict";或'use strict';

<script>
   // 开启严格模式
   'use strict';  //下面的js代码就会按照严格模式执行代码
</script>

但是有的script脚本是严格模式,有的脚本是正常模式,不利于文件合并。所以可以将整个脚本文件放到一个立即执行的匿名函数中,这样独立创造一个作用域而不影响其他script脚本文件。

<script>
    (function () {
    	'use strict';
    })();
</script>
(2)为某个函数开启严格模式

在函数第一行中加"use strict";

	<!-- 为某个函数开启严格模式 -->
    <script>
        // 只给fn函数开启严格模式
        function fn () {
            'use strict';
        }
        function fun () {

        }
    </script>
3、严格模式中的变化

严格模式对js的语法和行为,都做了一些改变。

(1)变量规定
  1. 正常模式中,如果一个变量没有声明就赋值,默认是全局变量。严格模式禁止使用这种用法,变量都必须先用var命令声明,然后再使用。

    // 严格模式的变化
            'use strict';
            num = 10;
            console.log(num);
    
    // ERROR: Uncaught ReferenceError: num is not defined
    
  2. 严禁删除已经声明的变量。例如delete x;语法是错误的。

    // 严格模式的变化
            'use strict';
            var num = 10;
            console.log(num);
            delete num;
    // ERROR: Uncaught SyntaxError: Delete of an unqualified identifier in strict mode.
    
(2)严格模式下this指向
  1. 以前在全局作用域函数中this指向window对象

  2. 严格模式下,全局作用域中函数的this指向undefined

    // 严格模式的变化
            'use strict';		
    		function fn() {
                console.log(this);  // undefined
            }
            fn();
    
  3. 以前构造函数时不加new也可以调用,当普通函数,this指向全局对象。

    		function Star() {
                this.sex = '男';
            }
            Star();
            console.log(window.sex);  // 男
    

    此时,直接调用构造函数,当做普通函数,this指向window,所以相当于给window添加一个sex属性。

    但严格模式下,普通函数this指向undefined,无法给undefined添加sex属性。

    Uncaught TypeError: Cannot set properties of undefined (setting 'sex')

  4. 通过new实例化的构造函数指向创建的对象实例

  5. 定时器中的this还是window

  6. 事件、对象的this还是指向调用者

(3)函数的变化
  1. 严格模式下,函数中的参数不能重名
  2. 函数必须声明在顶层。ES6中引入"块级作用域",为了与新版本接轨,不允许在非函数的代码块中声明函数。
四、高阶函数

高阶函数是对其他函数进行操作的函数,它接收函数作为参数将函数作为返回值输出

函数也是一种数据类型,同样可以作为参数,传递给另外一个参数使用。最典型的就是作为回调函数。

1、高阶函数-函数作为参数传递

最典型的是作为回调函数使用。

// 高阶函数-函数作为参数传递
        function fn(a, b, callback) {
            console.log(a + b);
            callback && callback();
        }
        fn(1, 2, function () {
            console.log('我是最后调用的');
        });
五、闭包
1、变量作用域

变量根据作用域的不同分为两种:全局变量和局部变量。

(1)函数内部可以使用全局变量
(2)函数外部不可以使用局部变量
(3)当函数执行完毕,本作用域内的局部变量会销毁
2、什么是闭包

闭包(closure)指有权访问另一个函数作用域中变量的函数。 --js高级程序设计

简单理解就是,一个作用域可以访问另一个函数内部的局部变量

		// 闭包(closure)指有权访问另一个函数作用域中变量的函数
        // 闭包:fun这个函数作用域访问了另外一个函数fn里面的局部变量num
        function fn() {
            var num = 10;
            function fun() {
                console.log(num);
            }
            fun();
        }
        fn();
3、闭包的作用

闭包的主要作用:延伸了变量的作用范围

// fn外面的作用域可以访问fn内部的局部变量
        function fn() {
            var num = 10;
            function fun() {
                console.log(num);
            }
            return fun;
        }
        var f = fn();
        f();

也可以通过返回一个匿名函数来产生闭包。

		function fn() {
            var num = 10;
            // function fun() {
            //     console.log(num);
            // }
            // return fun;
            return function () {
                console.log(num);
            }
        }
  • 访问另一个作用域内的局部变量
  • 保存函数作用域中的局部变量
4、闭包应用案例
(1)点击li输出索引号
// 闭包应用-点击li输出索引号
            // 1. 利用动态添加属性的方式
            var lis = document.querySelector('.nav').querySelectorAll('li');
            for (var i = 0; i < lis.length; i++) {
                lis[i].index = i;
                lis[i].onclick = function () {
                    console.log(this.index);
                }
            }
            // 2. 利用闭包得到当前li的索引号
            for (var i = 0; i < lis.length; i++) {
                // 利用for循环创建了4个立即执行函数
                // 立即执行函数也称为小闭包,因为立即执行函数中任何一个函数都可以使用它的i变量
                (function (i) {
                    // console.log(i);
                    lis[i].onclick = function () {
                        // 这里的i使用了立即执行函数中的i
                        console.log(i);
                    }
                })(i);
            }

但其实,闭包会创建多个函数,会造成内存泄漏的问题,反而效率比较低。

(2)定时器中的闭包

依然是利用立即执行函数来产生闭包。

// 闭包应用-定时器中的闭包
        var lis = document.querySelector('.nav').querySelectorAll('li');
        for (var i = 0; i < lis.length; i++) {
            (function (i) {
                setTimeout(function () {
                    console.log(lis[i].innerHTML);
                }, 3000);
            })(i)
        }
(3)计算打车价格

立即执行函数形成闭包,返回一个对象传递给car,对象中包含正常计算的总价和计算拥堵费用的总价。price和yd函数中都是用到了外部立即执行函数的局部变量start和total。

// 闭包应用-计算打车价格
        // 打车起步价13(3公里内),之后每多1公里增加5块钱。用户输入公里数就可以计算打车价格
        // 如果有拥堵情况,总价格多收取10块钱拥堵费
        var car = (function () {
            var start = 13;  // 起步价 局部变量
            var total = 0;  // 总价 局部变量
            return {
                // 正常的总价
                price: function (n) {
                    // 这里使用了外部立即执行函数的局部变量start和total
                    if (n <= 3) total = start;
                    else {
                        total = start + (n - 3) * 5;
                    }
                    return total;
                },
                // 拥堵的费用
                yd: function (flag) {
                    return flag ? total + 10 : total;
                }
            }
        })();
        console.log(car.price(5));  //23
        console.log(car.yd(true));  //33
        console.log(car.price(1));  //13
        console.log(car.yd(false)); //13
5、思考题
(1)思考题1
// 思考题1:
        var name = "The Window";
        var object = {
            name: "My Object",
            getNameFunc: function () {
                return function () {
                    return this.name;
                };
            }
        };
        console.log(object.getNameFunc()());

        // 解析:
        var f = object.getNameFunc();
        var f = function () {
            // 立即执行函数中的this指向的是window,window下有一个name属性
            return this.name;   // The Window
        }
        f();

如解析所示,可以将object.getNameFunc()()进行拆解,也就是将getNameFunc的返回值赋值给f,然后执行f方法。相当于是一个立即执行函数,所以this指向window,window全局作用域下又有一个name属性,所以最终打印的是The Window。

这里并没有闭包的产生,因为并没有访问另一个作用域中的局部变量。

(2)思考题2
// 思考题2:
        var name = "The Window";
        var object = {
            name: "My Object",
            getNameFunc: function () {
                var that = this;
                return function () {
                    return that.name;
                };
            }
        };
        console.log(object.getNameFunc()());

        // 解析:
        var f = object.getNameFunc();
        f = function () {
            return that.name;  // that指向的是object
        };
        f();

这里打印的是that.name,getNameFunc方法中的this指向调用者object,所以打印My Object。

并且该题中有闭包,getNameFunc中的that是局部变量,而该方法中return的那个函数又使用到了that变量,所以产生了闭包。

六、递归
1、什么是递归

如果一个函数在内部可以调用其本身,那么这个函数就是递归函数。简单理解就是,函数内部自己调用自己,这个函数就是递归函数。

由于递归很容易发生“栈溢出”错误(stack overflow),所以必须要加退出条件return

2、递归应用
(1)利用递归计算1-n的阶乘
// 利用递归求1-n的阶乘
        function fn(n) {
            if (n == 1) return 1;
            return n * fn(n - 1);
        }
        console.log(fn(5));

退出条件是当n等于1的时候,不然就是n与前一项的阶乘相乘。

(2)斐波那契数列
// 利用递归求斐波那契数列
        function fn(n) {
            if (n === 0 || n === 1) return 1;
            return fn(n - 1) + fn(n - 2);
        }
        console.log(fn(5));
(3)利用递归遍历数据

data数组中有两个对象,但第一个对象中的goods里面还有更深一层的数据。此时可以利用递归进行查找。递归函数中传入的参数是json对象和id号,在递归中,如果想要得到里层的数据,则里面应该有goods并且数组长度不为0,此时递归查找goods数组。

		var data = [{
            id: 1,
            name: '家电',
            goods: [{
                id: 11,
                gname: '冰箱'
            }, {
                id: 12,
                gname: '洗衣机'
            }]
        }, {
            id: 2,
            name: '服饰'
        }];
        // 输入id号,就可以返回数据对象
        // 1、利用forEach遍历里面的每一个对象
        function getID(json, id) {
            json.forEach(function (item) {
                // console.log(item);
                if (item.id === id) {
                    console.log(item);
                    // 2、想要得到里层的数据 利用递归函数
                    // 里面应该有goods这个数组并且数组长度不为0
                } else if (item.goods && item.goods.length > 0) {
                    getID(item.goods, id);
                }
            })
        }
        getID(data, 1);  // 家电对象
        getID(data, 11);  // 冰箱对象

直接将对象作为函数的返回值返回。

		function getID(json, id) {
            var o = {};
            json.forEach(function (item) {
                if (item.id === id) {
                    o = item;
                } else if (item.goods && item.goods.length > 0) {
                    o = getID(item.goods, id);
                }
            });
            return o;
        }
3、浅拷贝和深拷贝
  1. 浅拷贝只拷贝一层,更深层次对象级别的只拷贝引用
  2. 深拷贝拷贝多层,每一级别的数据都会拷贝
(1)浅拷贝
		var obj = {
            id: 1,
            name: 'andy',
            msg: {
                age: 18
            }
        };
        var o = {};
        // 1.利用for循环进行浅拷贝
        for (var k in obj) {
            o[k] = obj[k];
        }

        // 2.利用es6新方法assign进行浅拷贝
        Object.assign(o, obj);

        console.log(o);
        o.msg.age = 20;
        console.log(obj);

浅拷贝对于复杂数据类型来说,只是将引用的地址进行拷贝,所以修改拷贝对象的值,原对象的值也会发生改变。

es6还给我们新增了浅拷贝的语法糖:Object.assign(target,...sources)

(2)深拷贝
// 3.递归实现深拷贝
        function deepClone(target, source) {
            for (var k in source) {
                // 判断属性值属于哪种数据类型
                var item = source[k];
                // 判断是否是数组
                if (item instanceof Array) {
                    target[k] = [];
                    deepClone(target[k], item);
                }
                // 判断是否是对象
                else if (item instanceof Object) {
                    target[k] = {};
                    deepClone(target[k], item);
                }
                // 否则属于简单数据类型
                else {
                    target[k] = item;
                }
            }
        }
        deepClone(o, obj);
        console.log(o);

利用递归实现深拷贝,首先取出该属性值,然后判断其数据类型。如果是数组或者对象类型,则利用递归进行拷贝;如果是普通类型数据,则直接赋值。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值