10. JavaScript函数进阶-this、闭包、递归、深浅拷贝

声明:本人前端学习笔记的所有内容均为b站上pink老师课程的学习笔记,如果想详细了解的可以搜索以下网址:

2019全新javaScript进阶面向对象ES6


1. 函数的定义和调用

1.1 函数的定义方式

  1. 函数声明方式 function 关键字 (命名函数)

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

  3. new Function()

var fn = new Function('参数1','参数2'..., '函数体')
  • Function 里面参数都必须是字符串格式
  • 第三种方式执行效率低,也不方便书写,因此较少使用
  • 函数也属于对象,所有函数都是 Function 的实例(对象)
//  函数的定义方式

// 1. 自定义函数(命名函数) 
function fn() {};

// 2. 函数表达式 (匿名函数)
var fun = function() {};

// 3. 利用 new Function('参数1','参数2', '函数体');
var f = new Function('a', 'b', 'console.log(a + b)');
f(1, 2);

// 4. 所有函数都是 Function 的实例(对象)
console.dir(f);
// 5. 函数也属于对象
console.log(f instanceof Object);

在这里插入图片描述

1.2 函数的调用方式

// 函数的调用方式

// 1. 普通函数
function fn() {
    console.log('人生的巅峰');
}
// fn();   fn.call()

// 2. 对象的方法
var o = {
    sayHi: function() {
        console.log('人生的巅峰');
    }
}
o.sayHi();

// 3. 构造函数
function Star() {};
new Star();

// 4. 绑定事件函数
// btn.onclick = function() {};   // 点击了按钮就可以调用这个函数

// 5. 定时器函数
// setInterval(function() {}, 1000);  这个函数是定时器自动1秒钟调用一次

// 6. 立即执行函数
(function() {
    console.log('人生的巅峰');
})();
// 立即执行函数是自动调用

2. this

2.1 函数内this的指向

这些 this 的指向,是当我们调用函数的时候确定的。 调用方式的不同决定了this 的指向不同

一般指向我们的调用者.
在这里插入图片描述

2.2 改变函数内部this指向

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

2.2.1 call()方法

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

fun.call(thisArg, arg1, arg2, ...) 
  • thisArg:在 fun 函数运行时指定的 this 值

  • arg1,arg2:传递的其他参数

  • 返回值就是函数的返回值,因为它就是调用函数

  • 因此当我们想改变 this 指向,同时想调用这个函数的时候,可以使用 call,比如继承

 // 1. call()
var o = {
    name: 'andy'
}

function fn(a, b) {
    console.log(this);
    console.log(a + b);
};

fn.call(o, 1, 2);
// call 第一可以调用函数 第二可以改变函数内的this 指向
// 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); //改变父类构造函数this指向,使他指向子类的this
}
var son = new Son('刘德华', 18, '男');
console.log(son);
2.2.2 apply()方法

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

fun.apply(thisArg, [argsArray])
  • thisArg:在fun函数运行时指定的 this 值
  • argsArray:传递的值,必须包含在数组里面
  • 返回值就是函数的返回值,因为它就是调用函数
  • 因此 apply 主要跟数组有关系,比如使用 Math.max() 求数组的最大值(var max = Math.max.apply(Math, arr);)
// 2. apply()  应用 运用的意思
var o = {
    name: 'andy'
};

function fn(arr) {
    console.log(this);
    console.log(arr); // 'pink'
};
fn.apply(o, ['pink']);

// 1. 也是调用函数 第二个可以改变函数内部的this指向
// 2. 但是他的参数必须是数组(伪数组)
// 3. apply 的主要应用 比如说我们可以利用 apply 借助于数学内置对象求数组最大值 
// Math.max();
var arr = [1, 66, 3, 99, 4];
var arr1 = ['red', 'pink'];
// var max = Math.max.apply(null, arr);
var max = Math.max.apply(Math, arr);
var min = Math.min.apply(Math, arr);//将this指向Math
console.log(max, min);
2.2.3 bind()方法

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

fun.bind(thisArg, arg1, arg2, ...) 
  • thisArg:在 fun 函数运行时指定的 this 值
  • arg1,arg2:传递的其他参数
  • 返回由指定的 this 值和初始化参数改造的原函数拷贝
  • 因此当我们只是想改变 this 指向,并且不想调用这个函数的时候,可以使用 bind
// 3. bind()  绑定 捆绑的意思
var o = {
    name: 'andy'
};

function fn(a, b) {
    console.log(this);
    console.log(a + b);
};
var f = fn.bind(o, 1, 2);
f();
// 1. 不会调用原来的函数   可以改变原来函数内部的this 指向
// 2. 返回的是原函数改变this之后产生的新函数
// 3. 如果有的函数我们不需要立即调用,但是又想改变这个函数内部的this指向此时用bind

bind方法在实际开放中是最常用的方法,因为它不会调用函数,只是改变this的指向,如下面的案例

// 4. 我们有一个按钮,当我们点击了之后,就禁用这个按钮,3秒钟之后开启这个按钮
// var btn = document.querySelector('button');
// btn.onclick = function() {
//     this.disabled = true; // 这个this 指向的是 btn 这个按钮
//     // var that = this;
//     setTimeout(function() {
//         // that.disabled = false; // 定时器函数里面的this 指向的是window,可以采用全局变量改变它的指向,但是这样很麻烦
//         this.disabled = false; // 此时定时器函数里面的this 指向的是btn
//     }.bind(this), 3000); // 这个this 指向的是btn 这个对象
// }

//当有多个按钮的情况下更能体现出bind方法的方便
var btns = document.querySelectorAll('button');
for (var i = 0; i < btns.length; i++) {
    btns[i].onclick = function() {
        this.disabled = true;
        setTimeout(function() {
            this.disabled = false;
        }.bind(this), 2000);
    }
}

2.3 call apply bind 总结(重要)

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

区别点

1.call 和 apply 会调用函数, 并且改变函数内部this指向.

2.call 和 apply 传递的参数不一样, call 传递参数 aru1, aru2…形式 apply 必须数组形式[arg]

3.bind 不会调用函数, 可以改变函数内部this指向.

主要应用场景

1.call 经常做继承.

2.apply 经常跟数组有关系. 比如借助于数学对象实现数组最大值最小值

3.bind 不调用函数,但是还想改变this指向. 比如改变定时器内部的this指向.

3. 严格模式

JavaScript 除了提供正常模式外,还提供了严格模式(strict mode)。ES5 的严格模式是采用具有限制性 JavaScript 变体的一种方式,即在严格的条件下运行 JS 代码。

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

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

1.消除了 Javascript 语法的一些不合理、不严谨之处,减少了一些怪异行为。

2.消除代码运行的一些不安全之处,保证代码运行的安全。

3.提高编译器效率,增加运行速度。

4.禁用了在 ECMAScript 的未来版本中可能会定义的一些语法,为未来新版本的 Javascript 做好铺垫。比如一些保留字如:class, enum, export, extends, import, super 不能做变量名

3.1 开启严格模式

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

1. 为脚本开启严格模式

为整个脚本文件开启严格模式,需要在所有语句之前放一个特定语句“use strict”;(或‘use strict’;)。

<script>
  "use strict";
  console.log("这是严格模式。");
</script>

因为"use strict"加了引号,所以老版本的浏览器会把它当作一行普通字符串而忽略。

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

<script>
  (function (){
    "use strict";
       var num = 10;
    function fn() {}
   })();
</script>

2. 为函数开启严格模式

要给某个函数开启严格模式,需要把“use strict”; (或 ‘use strict’; ) 声明放在函数体所有语句之前。

function fn(){
  "use strict";
  return "这是严格模式。";
}

将 “use strict” 放在函数体的第一行,则整个函数以 “严格模式” 运行。

3.2 严格模式中的变化

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

1. 变量规定

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

②严禁删除已经声明变量。例如,delete x; 语法是错误的。

2. 严格模式下 this 指向问题

①以前在全局作用域函数中的 this 指向 window 对象,严格模式下全局作用域中函数中的 this 是 undefined。

②以前构造函数时不加 new也可以调用,当普通函数,this 指向全局对象。严格模式下,如果构造函数不加new调用, this 指向的是undefined 如果给他赋值则会报错

③new 实例化的构造函数指向创建的对象实例。

④定时器 this 还是指向 window 。

⑤事件、对象还是指向调用者。

3. 函数变化

①函数不能有重名的参数。

②函数必须声明在顶层.新版本的 JavaScript 会引入“块级作用域”( ES6 中已引入)。为了与新版本接轨,不允许在非函数的代码块内声明函数。

4. 高阶函数

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

<script>
    function fn(callback){
      callback&&callback(); //回调函数
    }
    fn(function(){alert('hi')}
</script>
<script>
    function fn(){
        return function() {}
    }
     fn();
</script>

此时fn 就是一个高阶函数

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

同理函数也可以作为返回值传递回来

5. 闭包

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

  1. 函数内部可以使用全局变量。

  2. 函数外部不可以使用局部变量。

  3. 当函数执行完毕,本作用域内的局部变量会销毁。

5.1 什么是闭包

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

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

<script>
 function fn1(){    // fn1 就是闭包函数
    var num = 10;
    function fn2(){
      console.log(num); // 10
    }
       fn2() //fn2访问了fn1的局部变量,所以局部变量所在的fn1就形成了闭包
 }
  fn1();
</script>

在 chrome 中调试闭包

  1. 打开浏览器,按 F12 键启动 chrome 调试工具。

  2. 设置断点。

  3. 找到 Scope 选项(Scope 作用域的意思)。

  4. 当我们重新刷新页面,会进入断点调试,Scope 里面会有两个参数(global 全局作用域、local 局部作用域)。

  5. 当执行到 fn2() 时,Scope 里面会多一个 Closure 参数 ,这就表明产生了闭包。

5.2 闭包的作用

上面的闭包是在fn1内部访问fn1的局部变量,那么:我们么能在fn()函数外面访问fn()里面的局部变量 num 呢?

<script>
    function fn() {
            var num = 10;

            // function fun() {
            //     console.log(num);

            // }
            // return fun;
            return function() {
                console.log(num);
            }
        }
        var f = fn();
        f(); //10

        // 类似于匿名函数的定义方式
        // var f = function() {
        //         console.log(num);
        //     }
</script>

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

5.3 闭包案例

1. 闭包应用 - 循环注册点击事件(重点!面试常考)

点击 li 输出当前 li 的索引号

<ul class="nav">
    <li>榴莲</li>
    <li>臭豆腐</li>
    <li>鲱鱼罐头</li>
    <li>大猪蹄子</li>
</ul>

<script>
    // 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(i);  由于点击事件的异步,该方法最后会输出4,无效的
            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() {
                console.log(i);
            }
        })(i);
    }
</script>

2. 闭包应用 - 定时器中的闭包

<ul class="nav">
    <li>榴莲</li>
    <li>臭豆腐</li>
    <li>鲱鱼罐头</li>
    <li>大猪蹄子</li>
</ul>
<script>
    // 闭包应用-3秒钟之后,打印所有li元素的内容
    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);
    }
</script>

3. 闭包应用 - 计算打车价格

<script>
    // 打车起步价13(3公里内),  之后每多一公里增加 5块钱.  用户输入公里数就可以计算打车价格
    // 如果有拥堵情况,总价格多收取10块钱拥堵费
    var car = (function() {
        var start = 13; // 起步价  局部变量
        var total = 0; // 总价  局部变量
        return {
            // 正常的总价
            price: function(n) {
                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
</script>

6. 递归

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

简单理解:函数内部自己调用自己, 这个函数就是递归函数

递归函数的作用和循环效果一样

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

6.1 利用递归求数学题

  1. 求 1 * 2 *3 … * n 阶乘。
function fn(n) {
    if (n == 1) {
        return 1;
    }
    return n * fn(n - 1);
}
console.log(fn(3));
// 详细思路 假如用户输入的是3
//return  3 * fn(2)
//return  3 * (2 * fn(1))
//return  3 * (2 * 1)
//return  3 * (2)
//return  6
  1. 求斐波那契数列 。
// 利用递归函数求斐波那契数列(兔子序列)  1、1、2、3、5、8、13、21...        
function fb(n) {
    if (n === 1 || n === 2) {
        return 1;
    }
    return fb(n - 1) + fb(n - 2);
}
console.log(fb(6));
  1. 根据id返回对应的数据对象
var data = [{
    id: 1,
    name: '家电',
    goods: [{
        id: 11,
        gname: '冰箱',
        goods: [{
            id: 111,
            gname: '海尔'
        }, {
            id: 112,
            gname: '美的'
        }, ]
    }, {
        id: 12,
        gname: '洗衣机'
    }]
}, {
    id: 2,
    name: '服饰'
}];
// 我们想要做输入id号,就可以返回的数据对象
// 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;
            // 2. 我们想要得里层的数据 11 12 可以利用递归函数
            // 里面应该有goods这个数组并且数组的长度不为 0 
        } else if (item.goods && item.goods.length > 0) {
            o = getID(item.goods, id);
        }

    });
    return o;
}
console.log(getID(data, 1));
console.log(getID(data, 2));
console.log(getID(data, 11));
console.log(getID(data, 12));
console.log(getID(data, 111));

6.2 浅拷贝和深拷贝

1.浅拷贝只是拷贝一层, 更深层次对象级别的只拷贝引用.

2.深拷贝拷贝多层, 每一级别的数据都会拷贝.

3.Object.assign(*target*, ...*sources*) es6 新增方法可以浅拷贝

6.2.1 浅拷贝和深拷贝的原理

浅拷贝是拷贝了对象的引用,或者说,就是把一个对象的地址给了另一个对象,他们指向相同,两个对象之间有共同的属性或者方法, 都可以使用,这样当原对象发生变化的时候,拷贝对象也跟着变化;

深拷贝是另外申请了一块内存,内容和原对象一样,更改原对象,拷贝对象不会发生变化。

  1. 如果是基本数据类型,名字和值都会储存在栈内存中
var a = 1;
b = a; // 栈内存会开辟一个新的内存空间,此时b和a都是相互独立的
b = 2;
console.log(a); // 1

当然,这也算不上深拷贝,因为深拷贝本身只针对较为复杂的object类型数据。

  1. 如果是引用数据类型,名字存在栈内存中,值存在堆内存中,但是栈内存会提供一个引用的地址指向堆内存中的值。比如浅拷贝:

img

当b=a进行拷贝时,其实复制的是a的引用地址,而并非堆里面的值。

img

而当我们a[0]=1时进行数组修改时,由于a与b指向的是同一个地址,所以自然b也受了影响,这就是所谓的浅拷贝了

img

那,要是在堆内存中也开辟一个新的内存专门为b存放值,就像基本类型那样,岂不就达到深拷贝的效果了

img

6.2.2 实现浅拷贝的方法

浅拷贝:有两种方式,一种是把一个对象里面的所有的属性值和方法都复制给另一个对象,另一种是直接把一个对象赋给另一个对象,使得两个都指向同一个对象。

// 浅拷贝只是拷贝一层, 更深层次对象级别的只拷贝引用.
// 深拷贝拷贝多层, 每一级别的数据都会拷贝.
var obj = {
    id: 1,
    name: 'andy',
    msg: {
        age: 18
    }
};
var o = {};
//1.把一个对象里面的所有的属性值和方法都复制给另一个对象
// for (var k in obj) {
//     // k 是属性名   obj[k] 属性值
//     o[k] = obj[k];
// }
// console.log(o);
// o.msg.age = 20;
// console.log(obj);

console.log('--------------');

//2.Object.assign方法
Object.assign(o, obj); //将obj拷贝给o这个新对象
console.log(o);
o.msg.age = 20;
console.log(obj); //改变o里面的age值后,obj原对象的age值也会变成20
6.2.3 实现深拷贝的方法

深拷贝:把一个对象的属性和方法一个个找出来,在另一个对象中开辟对应的空间,一个个存储到另一个对象中。

// 深拷贝拷贝多层, 每一级别的数据都会拷贝.
var obj = {
    id: 1,
    name: 'andy',
    msg: {
        age: 18
    },
    color: ['pink', 'red']
};
var o = {};
// 封装函数 
function deepCopy(newobj, oldobj) {
    for (var k in oldobj) {
        // 判断我们的属性值属于那种数据类型
        // 1. 获取属性值  oldobj[k]
        var item = oldobj[k];
        // 2. 判断这个值是否是数组
        if (item instanceof Array) {
            newobj[k] = [];
            deepCopy(newobj[k], item)
        } else if (item instanceof Object) {
            // 3. 判断这个值是否是对象
            newobj[k] = {};
            deepCopy(newobj[k], item)
        } else {
            // 4. 属于简单数据类型
            newobj[k] = item;
        }
    }
}

deepCopy(o, obj);
console.log(o);

var arr = [];
console.log(arr instanceof Object);
o.msg.age = 20;
console.log(obj);

两者区别就在于,浅拷贝只是简单的复制,对对象里面的属性和数组属性只是复制了地址,并没有创建新的相同对象或者数组。而深拷贝是完完全全的复制一份,空间大小占用一样但是位置不同!!

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
JavaScript中的闭包是指一个函数能够访问并使用其外部作用域中的变量,即使在该函数被调用之后,该外部作用域的上下文已经销毁。简单来说,闭包是指一个函数保留了对其词法作用域的引用,使得函数可以访问和操作外部作用域的变量。 在JavaScript中,闭包可以通过函数嵌套来创建。当一个函数在内部定义了另一个函数,并且内部函数引用了外部函数的变量,那么内部函数就形成了闭包闭包函数可以访问和修改外部函数的变量,即使在外部函数执行完毕后,这些变量依然存在于闭包函数的作用域中。 以下是一个闭包函数的例子: ```javascript function outerFunction() { var outerVariable = 'Hello'; function innerFunction() { console.log(outerVariable); } return innerFunction; } var closure = outerFunction(); closure(); // 输出 'Hello' ``` 在这个例子中,内部函数`innerFunction`形成了闭包,它可以访问和使用外部函数`outerFunction`中的变量`outerVariable`,即使`outerFunction`执行完毕后,闭包仍然可以访问和操作`outerVariable`的值。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [JavaScript中的闭包](https://blog.csdn.net/qq_44482048/article/details/128714553)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [轻松学习Javascript闭包函数](https://download.csdn.net/download/weixin_38628429/13018893)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [JavaScript闭包](https://blog.csdn.net/qq_57586976/article/details/127678306)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值