JavaScript:基础语法进阶

前言
上一篇“JavaScript语法”,我们学习了JavaScript的5种基本数据类型和引用类型。这个和Java基础类似。还有就是我们知道JavaScript除了ECMA标准外,还有Dom文档的操作Api和Bom浏览器的操作Api。还有我们知道JavaScript语法本身也有很多和Java不一样的地方,创建对象的方式可以使用字面量的方式,包括操作对象的属性使用obj.属性 = xxx,也和java不一样。
还有javascript语法本身有许多内置对象,比如时间对象Data、数组对象等有一系列的Api可以调用。比较特殊的就是数字数组对象的sort方法不能正确的给数字数组进行排序,原因就是存储是采用Unicode编码进行存储,1和11,2和22等这些相同的数其实都是按一个个单独的字符进行存储的,所以就会有数字11会排在数字1后面。
还有就是Dom文档模型的节点的获取等Api操作,Bom浏览器对鼠标事件的监听等都是需要掌握的Javascript基础知识点。
基础知识需要不断使用,然后加深记忆就可以熟练掌握。下面我们一起来学习JavaScript的语法进阶知识。

浅拷贝

对于对象或数组类型,当我们将a赋值给b,然后更改b中的属性,a也会随着变化。也就是说,a和b指向了同一块堆内存,所以修改其中任意的值,另一个值都会随之变化,这就是浅拷贝。

深拷贝

那么相应的,如果给b放到新的内存中,将a的各个属性都复制到新内存里,就是深拷贝。也就是说,当b中的属性有变化的时候,a内的属性不会发生变化。
参考链接:

这个浅拷贝和深拷贝的原理和所有编程语言一样,都是相通的。
然后我们需要了解Javascrip创建对象的4种方式:

1)通过先new Object()先创建一个对象,然后在使用obj.属性 = XXX来给属性设值;这种方式适合不知道对象的具体属性的情况下。缺点就是语句太多。
2)对象字面量,通过var关键字先声明一个对象,然后给对象赋值为字面量对象,属性固定但是也可以扩展。问题:如果创建多个对象,要重复创建对象。
3)工厂模式,通过定义一个方法,里面再通过字面量的方式创建对象,对象的属性可以通过参数动态改变。这样就可以通过调用方法返回对象。问题:对象都是obj类型。
4)自定义构造函数,适用于需要创建多个类型确定的对象。问题:每个对象都有相同的数据,浪费内存。

this

this指的是,调用函数的那个对象。this永远指向函数运行时所在的对象。

需要特别提醒的是:this的指向在函数定义时无法确认,只有函数执行时才能确定。

作用域的分类:

  • 全局作用域

  • 函数作用域

  • 没有块作用域(ES6有了)

if (true) {
    var name = 'smyhvae';
}
console.log(name);

上方代码中,并不会报错,因为:虽然 name 是在块里面定义的,但是 name 是全局变量。

直接编写在script标签中的JS代码,都在全局作用域。

调用函数时创建函数作用域,函数执行完毕以后,函数作用域销毁。

1)在函数作用域中可以访问到全局作用域的变量,在全局作用域中无法访问到函数作用域的变量。
2)在函数中要访问全局变量可以使用window对象。(比如说,全局作用域和函数作用域都定义了变量a,如果想访问全局变量,可以使用`window.a`)
3)在函数中,没有var声明的变量都会成为**全局变量**,而且并不会提前声明。

作用域链

当在函数作用域操作一个变量时,它会先在自身作用域中寻找,如果有就直接使用(就近原则)。如果没有则向上一级作用域中寻找,直到找到全局作用域;如果全局作用域中依然没有找到,则会报错ReferenceError。

外部函数定义的变量可以被内部函数所使用,反之则不行。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script>
        //只要是函数就可以创造作用域
        //函数中又可以再创建函数
        //函数内部的作用域可以访问函数外部的作用域
        //如果有多个函数嵌套,那么就会构成一个链式访问结构,这就是作用域链

        //f1--->全局
        function f1(){
            //f2--->f1--->全局
            function f2(){
                //f3---->f2--->f1--->全局
                function f3(){
                }
                //f4--->f2--->f1---->全局
                function f4(){
                }
            }
            //f5--->f1---->全局
            function f5(){
            }
        }

    </script>
</head>
<body>

</body>
</html>

理解:

  • 多个上下级关系的作用域形成的链, 它的方向是从下向上的(从内到外)

  • 查找变量时就是沿着作用域链来查找的

查找一个变量的查找规则:

    var a = 1

    function fn1() {
      var b = 2

      function fn2() {
        var c = 3
        console.log(c)
        console.log(b)
        console.log(a)
        console.log(d)
      }
      fn2()
    }
    fn1()
  • 在当前作用域下的执行上下文中查找对应的属性, 如果有直接返回, 否则进入2

  • 在上一级作用域的执行上下文中查找对应的属性, 如果有直接返回, 否则进入3

  • 再次执行2的相同操作, 直到全局作用域, 如果还找不到就抛出找不到的异常

闭包

闭包就是能够读取其他函数内部数据(变量/函数)的函数。

只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。

上面这两句话,是阮一峰的文章里的,你不一定能理解,来看下面的讲解和举例。

如何产生闭包

当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量或函数时, 就产生了闭包。

闭包到底是什么?

使用chrome调试查看

  • 理解一: 闭包是嵌套的内部函数(绝大部分人)

  • 理解二: 包含被引用变量 or 函数的对象(极少数人)

注意: 闭包存在于嵌套的内部函数中。

产生闭包的条件

  • 1.函数嵌套

  • 2.内部函数引用了外部函数的数据(变量/函数)。

来看看条件2:

    function fn1() {
        function fn2() {

        }

        return fn2;
    }

    fn1();

上面的代码不会产生闭包,因为内部函数fn2并没有引用外部函数fn1的变量。

PS:还有一个条件是外部函数被调用,内部函数被声明。比如:


    function fn1() {
        var a = 2
        var b = 'abc'

        function fn2() { //fn2内部函数被提前声明,就会产生闭包(不用调用内部函数)
            console.log(a)
        }

    }

    fn1();

    function fn3() {
        var a = 3
        var fun4 = function () {  //fun4采用的是“函数表达式”创建的函数,此时内部函数的声明并没有提前
            console.log(a)
        }
    }

    fn3();

常见的闭包

    1. 将一个函数作为另一个函数的返回值
    1. 将函数作为实参传递给另一个函数调用。

闭包1:将一个函数作为另一个函数的返回值

    function fn1() {
      var a = 2

      function fn2() {
        a++
        console.log(a)
      }
      return fn2
    }

    var f = fn1();   //执行外部函数fn1,返回的是内部函数fn2
    f() // 3       //执行fn2
    f() // 4       //再次执行fn2

当f()第二次执行的时候,a加1了,也就说明了:闭包里的数据没有消失,而是保存在了内存中。如果没有闭包,代码执行完倒数第三行后,变量a就消失了。

上面的代码中,虽然调用了内部函数两次,但是,闭包对象只创建了一个。

也就是说,要看闭包对象创建了一个,就看:外部函数执行了几次(与内部函数执行几次无关)。

闭包2. 将函数作为实参传递给另一个函数调用

    function showDelay(msg, time) {
      setTimeout(function() {  //这个function是闭包,因为是嵌套的子函数,而且引用了外部函数的变量msg
        alert(msg)
      }, time)
    }
    showDelay('atguigu', 2000)

上面的代码中,闭包是里面的funciton,因为它是嵌套的子函数,而且引用了外部函数的变量msg。

闭包的作用

  • 作用1. 使用函数内部的变量在函数执行完后, 仍然存活在内存中(延长了局部变量的生命周期)

  • 作用2. 让函数外部可以操作(读写)到函数内部的数据(变量/函数)

我们让然拿这段代码来分析:

    function fn1() {
      var a = 2

      function fn2() {
        a++
        console.log(a)
      }
      return fn2;
    }

    var f = fn1();   //执行外部函数fn1,返回的是内部函数fn2
    f() // 3       //执行fn2
    f() // 4       //再次执行fn2

作用1分析

上方代码中,外部函数fn1执行完毕后,变量a并没有立即消失,而是保存在内存当中。

作用2分析:

函数fn1中的变量a,是在fn1这个函数作用域内,因此外部无法访问。但是通过闭包,外部就可以操作到变量a。

达到的效果是:外界看不到变量a,但可以操作a

比如上面达到的效果是:我看不到变量a,但是每次执行函数后,让a加1。当然,如果我真想看到a,我可以在fn2中将a返回即可。

回答几个问题:

  • 问题1. 函数执行完后, 函数内部声明的局部变量是否还存在?

答案:一般是不存在, 存在于闭包中的变量才可能存在。

闭包能够一直存在的根本原因是f,因为f接收了fn1(),这个是闭包,闭包里有a。注意,此时,fn2并不存在了,但是里面的对象(即闭包)依然存在,因为用f接收了。

  • 问题2. 在函数外部能直接访问函数内部的局部变量吗?

不能,但我们可以通过闭包让外部操作它。

闭包的生命周期

  1. 产生: 嵌套内部函数fn2被声明时就产生了(不是在调用)

  2. 死亡: 嵌套的内部函数成为垃圾对象时。(比如f = null,就可以让f成为垃圾对象。意思是,此时f不再引用闭包这个对象了)

闭包的应用:定义具有特定功能的js模块

  • 将所有的数据和功能都封装在一个函数内部(私有的),只向外暴露一个包含n个方法的对象或函数。

  • 模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能。

方式一

(1)myModule.js:(定义一个模块,向外暴露多个函数,供外界调用)

function myModule() {
    //私有数据
    var msg = 'Smyhvae Haha'

    //操作私有数据的函数
    function doSomething() {
        console.log('doSomething() ' + msg.toUpperCase()); //字符串大写
    }

    function doOtherthing() {
        console.log('doOtherthing() ' + msg.toLowerCase()) //字符串小写
    }

    //通过【对象字面量】的形式进行包裹,向外暴露多个函数
    return {
        doSomething1: doSomething,
        doOtherthing2: doOtherthing
    }
}

上方代码中,外界可以通过doSomething1和doOtherthing2来操作里面的数据,但不让外界看到。

(2)index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>05_闭包的应用_自定义JS模块</title>
</head>
<body>
<!--
闭包的应用 : 定义JS模块
  * 具有特定功能的js文件
  * 将所有的数据和功能都封装在一个函数内部(私有的)
  * 【重要】只向外暴露一个包含n个方法的对象或函数
  * 模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能
-->
<script type="text/javascript" src="myModule.js"></script>
<script type="text/javascript">
    var module = myModule();
    module.doSomething1();
    module.doOtherthing2();
</script>
</body>
</html>

方式二

同样是实现方式一种的功能,这里我们采取另外一种方式。

(1)myModule2.js:(是一个立即执行的匿名函数)

(function () {
    //私有数据
    var msg = 'Smyhvae Haha'

    //操作私有数据的函数
    function doSomething() {
        console.log('doSomething() ' + msg.toUpperCase())
    }

    function doOtherthing() {
        console.log('doOtherthing() ' + msg.toLowerCase())
    }

    //外部函数是即使运行的匿名函数,我们可以把两个方法直接传给window对象
    window.myModule = {
        doSomething1: doSomething,
        doOtherthing2: doOtherthing
    }
})()

(2)index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>05_闭包的应用_自定义JS模块2</title>
</head>
<body>
<!--
闭包的应用2 : 定义JS模块
  * 具有特定功能的js文件
  * 将所有的数据和功能都封装在一个函数内部(私有的)
  * 只向外暴露一个包信n个方法的对象或函数
  * 模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能
-->

<!--引入myModule文件-->
<script type="text/javascript" src="myModule2.js"></script>
<script type="text/javascript">
    myModule.doSomething1()
    myModule.doOtherthing2()
</script>
</body>
</html>

上方两个文件中,我们在myModule2.js里直接把两个方法直接传递给window对象了。于是,在index.html中引入这个js文件后,会立即执行里面的匿名函数。在index.html中把myModule直接拿来用即可。

总结:

当然,方式一和方式二对比后,我们更建议采用方式二,因为很方便。

但无论如何,两种方式都采用了闭包。

闭包的缺点及解决

缺点:函数执行完后, 函数内的局部变量没有释放,占用内存时间会变长,容易造成内存泄露。

解决:能不用闭包就不用,及时释放。比如:

    f = null;  // 让内部函数成为垃圾对象 -->回收闭包

总而言之,你需要它,就是优点;你不需要它,就成了缺点。

内存溢出和内存泄露

内存溢出

内存溢出:一种程序运行出现的错误。当程序运行需要的内存超过了剩余的内存时, 就出抛出内存溢出的错误。

代码举例:

    var obj = {};
    for (var i = 0; i < 10000; i++) {
    obj[i] = new Array(10000000);  //把所有的数组内容都放到obj里保存,导致obj占用了很大的内存空间
    console.log("-----");
    }

内存泄漏

内存泄漏占用的内存没有及时释放。

注意,内存泄露的次数积累多了,就容易导致内存溢出。

常见的内存泄露

  • 1.意外的全局变量

  • 2.没有及时清理的计时器或回调函数

  • 3.闭包

情况1举例:

    // 意外的全局变量
    function fn() {
        a = new Array(10000000);
        console.log(a);
    }

    fn();

情况2举例:

    // 没有及时清理的计时器或回调函数
    var intervalId = setInterval(function () { //启动循环定时器后不清理
        console.log('----')
    }, 1000)

    // clearInterval(intervalId);  //清理定时器

情况3举例:

<script type="text/javascript">
  function fn1() {
    var a = 4;
    function fn2() {
      console.log(++a)
    }
    return fn2
  }
  var f = fn1()
  f()

  // f = null //让内部函数成为垃圾对象-->回收闭包
</script>

call()和apply()

介绍

这两个方法都是函数对象的方法,需要通过函数对象来调用。

当函数调用call()和apply()时,函数都会立即执行

  • 都可以用来改变函数的this对象的指向。

  • 第一个参数都是this要指向的对象(函数执行时,this将指向这个对象),后续参数用来传实参。

显式绑定this

JS提供的绝大多数函数以及我们自己创建的所有函数,都可以使用call 和apply方法。

它们的第一个参数是一个对象。因为你可以直接指定 this 绑定的对象,因此我们称之为显式绑定。

例1:

    function foo() {
        console.log(this.a);
    }

    var obj = {
        a: 2
    };

    // 将 this 指向 obj
    foo.apply(obj); //打印结果:2

第一个参数的传递

1、thisObj不传或者为null、undefined时,函数中的this会指向window对象(非严格模式)。

2、传递一个别的函数名时,函数中的this将指向这个函数的引用

3、传递的值为数字、布尔值、字符串时,this会指向这些基本类型的包装对象Number、Boolean、String。

4、传递一个对象时,函数中的this则指向传递的这个对象。

call()和apply()的区别

call()和apply()方法都可以将实参在对象之后依次传递,但是apply()方法需要将实参封装到一个数组中统一传递(即使只有实参只有一个,也要放到数组中)。

比如针对下面这样的代码:

    var persion1 = {
        name: "小王",
        gender: "男",
        age: 24,
        say: function (school, grade) {
            alert(this.name + " , " + this.gender + " ,今年" + this.age + " ,在" + school + "上" + grade);
        }
    }
    var person2 = {
        name: "小红",
        gender: "女",
        age: 18
    }

如果是通过call的参数进行传参,是这样的:

	persion1.say.call(persion2, "实验小学", "六年级");

如果是通过apply的参数进行传参,是这样的:

	persion1.say.apply(persion2, ["实验小学", "六年级"]);

看到区别了吗,call后面的实参与say方法中是一一对应的,而apply传实参时,要封装成一个数组,数组中的元素是和say方法中一一对应的,这就是两者最大的区别。

call()和apply()的作用

  • 改变this的指向

  • 实现继承。Father.call(this)

bind()

  • 都能改变this的指向

  • call()/apply()是立即调用函数

  • bind()是将函数返回,因此后面还需要加()才能调用。

bind()传参的方式与call()相同。

参考链接:

let和var在for循环中的表现

解决闭包中的this指向问题

内部函数是可以访问到外部函数的变量的。

方式一:直接通过父函数的名字访问

方式二:如果不知道父函数的名字,在父函数里加一句_this = this,此时_this相当于父函数的名字。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值