ES5的核心技术

2018-03-19  本文已影响0人  lj51

标签 : ES5


1.闭包

观察下一段代码在浏览器中的运行情况

    (function(){
        var a = 30;
    })();
    alert(a);   //  Uncaught ReferenceError: a is not defined

浏览器会报错,因为上面代码段1-3行形成了闭包,从而导致闭包外面无法访问到闭包内的变量。

我们再观察这么一段代码:

    (function(){
        alert(a);   //  undefined
        var a = 30;
    })();

上面这段代码结果是aundefined,和上面不同,undefined是指a声明了,但是没有赋值,而上面是报错是因为作用域中没有a这个变量。

接着看下面这段代码

    function f1() {
        var a = 20;
        function f2() {
            a += 1;
            console.log(a);
        }
        return f2;
    }

    var r = f1();
    r();    //  21
    r();    //  22
    r();    //  23

我们在函数作用域外面访问到了f1中的变量,这就是闭包的作用,让我们可以拿到本不该拿到的东西。

内存泄漏

我们可能经常听说闭包会导致内存泄漏,那么为什么闭包会导致内存泄漏呢?

一般函数在执行完的时候,js的垃圾回收机制就会回收函数内的变量,释放内存。但是,上面我们把f2函数给 return 出来了,里面使用到了a变量,而浏览器不知道我们执行了上面三次之后什么时候再使用个函数,所以a变量就一直在内存中存在了,从而可能会导致内存泄漏。那么,我们怎么防止发生内存泄露呢?一般情况下,我们在不使用f2的时候,把f2置为 null。

    f2 = null;

有些时候,我们想保护我们的变量,这时候可以用到闭包,不让外部直接访问和修改我们的变量。我们可以通过下面的方式实现:

    function Man() {
        var name;
        this.setName = function (value) {
            name = value;
        }
    }
    var s = new Man();
    s.setName('lemon');

现在,name 变量就被我们保护了,切记不用的时候一定要释放内存。

现在我们看一个我们经常看到的一个例子:

    var list_li = document.getElementsByTagName('li');  //  假设有6个li
    for(var i = 0;i<list_li.length;i++) {
        list_li[i].onclick = function () {
            console.log(i);
        }
    }

我们的目的是去给每一个li绑定一个事件,点击第 i 个我们就输出对应 i 的值,但是我们发现结果都是6。怎么去修改处我们想要的结果,也许都知道。

    var list_li = document.getElementsByTagName('li');
    for(var i = 0;i<list_li.length;i++) {
        (function (i) {
            list_li[i].onclick = function () {
                console.log(i);
            }
        })(i);
    }

为什么上面的代码执行结果和这次的不一样呢?

在js分为 同步队列异步队列,只有同步队列里面的代码执行完了才会去异步队列里面拉取执行。

异步队列只包含以下三个:

  • 事件绑定
  • Ajax
  • setTimeout

接下来,我们就清楚了,事件绑定是异步队列,而for循环是同步队列,当for结束的时候,i的值自然就是最后一个值。但是,我们加上闭包以后,将i传进去,但是函数没有执行完,因为事件绑定在异步队列中,此时i就会保存在内存中,一直到对应的事件绑定事件结束,i才会被垃圾回收机制给回收掉。

上面这一整句话都是因为js在es5中只有函数级作用域,没有块级作用域,而es6中引入的let则会产生块级作用域,这里不做赘述。


2. 模块化

立即执行函数实现模块化

    var module = (function () {
        var a =5;
        function add(x) {
            var b = a + x;
            console.log(b);
        }

        return {
            des:'这是一个模块',
            add:add
        }
    })();

    module.add(3)

加入你感觉这种写法过于函数时,你也可以写成这样:

    var module = {
        a:5,
        add:function (x) {
            console.log(this.a + x)
        }
    };

    module.add(30)

3. this

    var a = 20;
    var p = {
        a:30,
        test:function () {
            alert(this.a)
        }
    }
    
    p.test();    //  30
    var q = p.test();
    q();    //  20

p.test() 中 this 指向 p ,所以 a 的值为30。上面的 var q = p.test() 相当于下面的代码:

    var q = function () {
            alert(this.a)
        }

此时 this 指向的是全局,所以 a 的值为20。

观察下面的代码

    var a = 20;
    var p = {
        a:30,
        test:function () {
        
            function s() {
                alert(this.a)
            }
            s();
            
        }
    };
    p.test()    //20

在函数里面执行 s ,我们去找 s 的宿主,假如我们没有找到s的宿主,默认就会去找到window。


4. ES5中的类

在别的语言中,类的构造函数需要我们显式的声明:

    class c{
        constructor{
            this.a = 20;
        }
    }

但是在js中,构造函数和初始化这个类就是一个东西

    var People = function (sex) {
        this.sex = sex;  // 构造函数和初始化这个类就是一个东西
    };
    People.prototype.hobby = function () {
        console.log(this.sex + '喜欢女人!!')
    };

父类声明好了,继续写我们的子类

第一种写法

    var Lemon = function () {
        
    };
    Lemon.prototype = People.prototype;

乍一看没啥问题啊,子类的原型等于父类的原型。但是,我们继续做一件事情:

    Lemon.prototype.test = function () {
        
    }

我们给子类的原型上挂载一个方法,此时我们去consol.log 父类,发现父类的原型上也多了一个test方法。子类只是继承我们的父类,怎么可以修改呢!!!至于为什么呢?因为原型的传递是按引用传递。
所以,第一种方法是不可行的。

第二种写法

子类不是要把父类的方法和属性都继承过来么,那就这样写:

    var Lemon = function (sex) {
        People.call(this,sex);  //  把People的属性继承过来
    };
    Lemon.prototype = new People();  //  把People的方法继承过来
    var l = new Lemon('Man');
    console.log(l)

这样写看起来还不错,方法和属性都继承过来了。但是,我们修改我们的父类如下:

    var People = function (sex) {
        this.sex = sex;
        console.log('666')
    };

现在我们new Lemon的时候,会发现输出了 两次 666,也就是构造函数执行了次,而一旦我们构造函数里面的东西比较多,这时候就变得比较坑了。这么看来,这第二种方法也不怎么好,故舍弃。

通过上面两种方法,我们仔细观察,子类的构造函数的执行都是指向了父类。所以,我们需要了解一下我们现在的需求:

  • 拿到父类原型链上的方法
  • 不能让构造函数执行两次
  • 引用的原型链不能是按址引用
  • 修正子类的constructor

基于以上的需求:

    var Lemon = function (sex) {
        People.call(this,sex);  //  把People的属性继承过来
    };
    var __bak = Object.create(People.prototype);    //  创建一个副本
    __bak.constructor = Lemon;  //  修正构造函数的指向
    Lemon.prototype = __bak;  //  把People的方法继承过来
    var l = new Lemon('Man');
    console.log(l)

上面就是所谓的我们基于 js 实现面向对象的一个过程。


5. bind

    var user = {
        age:20,
        init:function () {
            console.log(this.age)
        }
    };

    var data = {
        age: 40
    };
    var s = user.init.bind(data);
    s();

这里我们要注意,bind之后返回的是一个新的对象。


总结

1.立即执行函数

2.闭包

  • 内部函数可以访问外部函数的变量,把内部函数返回出去就可以保护内部的变量。
  • 闭包容易造成内存泄漏,避免的方法就是 不用的时候将 return出去的变量置为 null。

3.原型链

  • 构造函数的属性比原型链上的属性的优先级要高;
  • 面向对象编程的时候,js没有类的概念,可以用函数来替代;
  • constructor 实际就是对应那个函数;
  • prototype 是按引用传递的,可以用 Object.create 创建原型链的副本

4. 数据传递类型

  • 数值 字符串 布尔类型 都是按传递;
  • 对象数组都是按引用传递;

5.改变this的指向,其中bind返回一个新对象

  • call
  • apply
  • bind

6.函数提升的优先级比变量优先级要

7.jq 内部有很多经典的写法 模块化编程的概念 闭包等等

上一篇下一篇

猜你喜欢

热点阅读