为什么一个好的前端‘基础扎实’更重要?

99 阅读28分钟

四、对象

1、对象的属性

给对象添加属性非常的简单如下所示:

var person={
    userName:'zhangsan'
}

如果想修改属性的特性,可以通过Object.defineProperty()来完成。

  var person = {
        userName: "zhangsan",
      };
      Object.defineProperty(person, "userName", {
        writable: false,
      });
      person.userName = "lisi"; //无法完成值的修改
      console.log(person.userName); //zhangsan

可以给Object.defineProperty添加getter()函数和setter( )函数,这两个函数可以实现对象的私有属性,私有属性不对外公布,如果想要对私有属性进行读取和写入,可以通过getter()函数和setter( )函数。

 var person = {
        _age: 20, // _age表示私有属性
      };
      Object.defineProperty(person, "age", {
        get: function () {
          return this._age;
        },
        //在给私有属性赋值的时候,完成对应的校验功能
        set: function (value) {
          if (value >= 18) {
            this._age = value;
            console.log("可以浏览该网站");
          } else {
            console.log("不可以浏览该网站");
          }
        },
      });
      console.log(person.age); //20
      person.age = 12;
      console.log(person.age); //20
      person.age = 30;
      console.log(person.age); // 30

关于Object.defineProperty更详细的内容,应该会在后面结合vue响应式原理的再深入记录。

2、属性访问方式的区别

访问对象中的属性,有两种方式。

第一种方式:通过‘.’来访问。

第二种方式:通过‘[ ]’来访问属性。

两种方式有什么区别呢?

第一:使用方括号来访问属性,可以借助于变量来实现。

    var person = {
        userName: "zhangsan",
      };
      var myName = "userName";
      console.log(person[myName]);

第二:使用方括号来访问属性,也可以通过数字来做属性。

  var person = {};
      person[1] = "hello";
      console.log(person[1]);

3、创建对象有哪几种方式

字面量方式创建对象

 var userInfo = {
        userName: "zhangsan",
        userAge: 18,
        getUserInfo: function () {
          console.log(this.userName + ":" + this.userAge);
        },
      };
      userInfo.getUserInfo();

字面量创建对象比较简单,但是问题也比较突出,每次只能创建一个对象,复用性比较差,如果需要创建多个对象,代码冗余比较高。

通过工厂模式创建对象

工厂模式是一个比较重要的设计模式,该模式提供了一个函数,在该函数中完成对象的创建。

 function createUser(userName, userAge) {
        var o = new Object();
        o.userName = userName;
        o.userAge = userAge;
        o.sayHi = function () {
          console.log(this.userName + ":" + this.userAge);
        };
        return o;
      }
      var user1 = createUser("wangwu", 20);
      var user2 = createUser("lisi", 20);
      console.log(user1.userName + ":" + user2.userName);

通过工厂模式创建对象,解决了字面量创建对象的问题,也就是当创建多个相似对象的时候代码重复的问题。

但是问题是,所创建的所有对象都是Object类型,无法进一步的区分对象的具体类型是什么。

通过构造函数创建对象

 function Person(userName, userAge) {
        this.userName = userName;
        this.userAge = userAge;
        this.sayHi = function () {
          console.log(this.userName + ":" + this.userAge);
        };
      }
      var p = new Person("zhangsan", 19);
      p.sayHi();

构造函数创建对象的优点:解决了工厂模式中对象类型无法识别的问题,也就是说通过构造函数创建的对象可以确定其所属的类型。

但是通过构造函数创建对象的问题:

在使用构造函数创建对象的时候,每个方法都会在创建对象时重新创建一遍,也就是说,根据Person构造函数每创建一个对象,我们就会创建一个sayHi方法,但它们做的事情是一样的,因此会造成内存的浪费。

通过原型模式创建对象

我们知道,每个函数都有一个prototype属性,这个属性指向函数的原型对象,而所谓的通过原型模式创建对象就是将属性和方法添加到prototype属性上。

  function Person() {}
      Person.prototype.userName = "wangwu";
      Person.prototype.userAge = 20;
      Person.prototype.sayHi = function () {
        console.log(this.userName + ":" + this.userAge);
      };
      var person1 = new Person();
      person1.sayHi();
      var person2 = new Person();
      console.log(person1.sayHi === person2.sayHi); // true

通过上面的代码,我们可以发现,使用基于原型模式创建的对象,它的属性和方法都是相等的,也就是说不同的对象会共享原型上的属性和方法,这样我们就解决了构造函数创建对象的问题。

但是这种方式创建的对象也是有问题的,因为所有的对象都是共享相同的属性,所以改变一个对象的属性值,会引起其他对象属性值的改变。而这种情况是我们不允许的,因为这样很容易造成数据的混乱。

   function Person() {}
      Person.prototype.userName = "wangwu";
      Person.prototype.userAge = 20;
      Person.prototype.arr = [1, 2];
      Person.prototype.sayHi = function () {
        console.log(this.userName + ":" + this.userAge);
      };
      var p1 = new Person();
      var p2 = new Person();
      console.log(p1.userName);
      p2.userName = "zhangsan";
      console.log(p1.userName); //wangwu,基本数据类型不受影响
      p1.arr.push(3);
      console.log(p1.arr); // [1,2,3]
      console.log(p2.arr); // [1,2,3]
      //引用类型受影响

组合使用构造函数模式和原型模式

通过构造函数和原型模式创建对象是比较常用的一种方式。

在构造函数中定义对象的属性,而在原型对象中定义对象共享的属性和方法。

//在构造函数中定义对象的属性
      function Person(userName, userAge) {
        this.userName = userName;
        this.userAge = userAge;
      }
      //在原型对象中添加共享的方法
      Person.prototype.sayHi = function () {
        return this.userName;
      };
      var p = new Person("zhangsan", 21);
      var p1 = new Person("lisi", 22);
      console.log(p1.sayHi());
      console.log(p.sayHi());
      // 不同对象共享相同的函数,所以经过比较发现是相等的。
      console.log(p.sayHi === p1.sayHi);
      //修改p对象的userName属性的值,但是不会影响到p1对象的userName属性的值
      p.userName = "admin";
      console.log(p.sayHi());
      console.log(p1.sayHi());

通过构造函数与原型模式组合创建对象的好处就是:每个对象都有自己的属性值,也就是拥有一份自己的实例属性的副本,同时又共享着方法的引用,最大限度的节省了内存。

使用动态原型模式创建对象

所谓的使用动态原型模式创建对象,其实就是将所有的内容都封装到构造函数中,而在构造函数中通过判断只初始化一次原型。

 function Person(userName, userAge) {
        this.userName = userName;
        this.userAge = userAge;
        if (typeof this.sayHi !== "function") {
          console.log("abc"); //只输出一次
          Person.prototype.sayHi = function () {
            console.log(this.userName);
          };
        }
      }
      var person = new Person("zhangsan", 21);
      var person1 = new Person("zhangsan", 21);
      person.sayHi();
      person1.sayHi();

通过上面的代码可以看出,我们将所有的内容写在了构造函数中,并且在构造函数中通过判断只初始化一次原型,而且只在第一次生成实例的时候进行原型的设置。这种方式创建的对象与构造函数和原型混合模式创建的对象功能上是相同的。

4、对象拷贝

拷贝指的就是将某个变量的值复制给另外一个变量的过程,关于拷贝可以分为浅拷贝深拷贝****。

针对不同的数据类型,浅拷贝与深拷贝会有不同的表现,主要表现于基本数据类型和引用数据类型在内存中存储的值不同。

对于基本数据类型,变量存储的是值本身,

对于引用数据类型,变量存储的是值在内存中的地址,如果有多个变量同时指向同一个内存地址,其中对一个变量的值进行修改以后,其它的变量也会受到影响。

var arr=[1,23,33]
var arr2=arr
arr2[0]=10;
console.log(arr) //  [10, 23, 33]

在上面的代码中,我们把arr赋值给了arr2,然后修改arr2的值,但是arr也受到了影响。

正是由于数据类型的不同,导致在进行浅拷贝与深拷贝的时候首先的效果是不一样的。

基本数据类型不管是浅拷贝还是深拷贝都是对值的本身的拷贝。对拷贝后值的修改不会影响到原始的值。

对于引用数据类型进行浅拷贝,拷贝后的值的修改会影响到原始的值,如果执行的是深拷贝,则拷贝的对象和原始对象之间相互独立,互不影响。

所以,这里我们可以总结出什么是浅拷贝,什么是深拷贝。

浅拷贝:如果一个对象中的属性是基本数据类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址,也就是拷贝后的内容与原始内容指向了同一个内存地址,这样拷贝后的值的修改会影响到原始的值。

深拷贝:如果一个对象中的属性是基本数据类型,拷贝的也是基本类型的值,如果属性是引用类型,就将其从内存中完整的拷贝一份出来,并且会在堆内存中开辟出一个新的区域存来进行存放,而且拷贝的对象和原始对象之间相互独立,互不影响。

浅拷贝

下面我们先来看一下浅拷贝的内容

 var obj = { a: 1, arr: [2, 3], o: { name: "zhangsan" } };
      var shallowObj = shallowCopy(obj);

      function shallowCopy(src) {
        var dst = {};
        for (var prop in src) {
          if (src.hasOwnProperty(prop)) {
            dst[prop] = src[prop];
          }
        }
        return dst;
      }
      obj.o.name = "lisi";
      console.log(shallowObj.o.name); //lisi,值受到了影响
      obj.arr[0] = 20;
      console.log(shallowObj.arr[0]); //20,值受到了影响
      obj.a = 10;
      console.log(shallowObj.a); // 1,值没有收到影响

除了以上方式实现浅拷贝以外,还可以通过ES6中的Object.assign()函数来实现,该函数可以将源对象中的可枚举的属性复制到目标对象中。

 var obj = { a: 1, arr: [2, 3], o: { name: "zhangsan" } };
      var result = {};
      //将obj对象拷贝给result对象
      Object.assign(result, obj);
      console.log(result);
      obj.a = 10;
      console.log(result.a); // 1,不受影响
      obj.arr[0] = 20;
      console.log(result.arr[0]); //20 受影响
      obj.o.name = "lisi";
      console.log(result.o.name); // lisi 受影响

深拷贝

下面,我们来看一下深拷贝内容

这里,我们可以使用

JSON.parse(JSON.stringify());

来实现深拷贝。

JSON.stringify()可以将对象转换为字符串

JSON.parse()可以将字符串反序列为一个对象

 var obj = { a: 1, arr: [2, 3], o: { name: "zhangsan" } };
      var str = JSON.stringify(obj);
      var resultObj = JSON.parse(str);
      obj.a = 10;
      console.log(resultObj.a); // 1 不受影响
      obj.arr[0] = 20;
      console.log(resultObj.arr[0]); // 2 不受影响
      obj.o.name = "lisi";
      console.log(resultObj.o.name); // zhangsan 不受影响

以上通过JSON对象,虽然能够实现深拷贝,但是还是有一定的问题的。

第一:无法实现对函数的拷贝

第二:如果对象中存在循环引用,会抛出异常

第三:对象中的构造函数会指向Object,原型链关系被破坏

 function Person(userName) {
        this.userName = userName;
      }
      var person = new Person("zhangsan");
      var obj = {
        fn: function () {
          console.log("abc");
        },
        // 属性o的值为某个对象
        o: person,
      };
      var str = JSON.stringify(obj);
      var resultObj = JSON.parse(str);
      console.log("resultObj=", resultObj); // 这里丢失了fn属性。因为该属性的值为函数
      console.log(resultObj.o.constructor); //指向了Object,导致了原型链关系的破坏。
      console.log(obj.o.constructor); // 这里指向Person构造函数,没有问题

下面我们再来看一下循环引用的情况:

  var obj = {
        userName: "zhangsan",
      };
      obj.a = obj;
      var result = JSON.parse(JSON.stringify(obj));

以上的内容会抛出异常。

自己模拟实现深拷贝

这里,我们实现一个简单的深拷贝,当然也可以使用第三方库中的方法来实现深拷贝,例如:可以使用jQuery中的$.extend()

在浅拷贝中,我们通过循环将源对象中属性依次添加到目标对象中,而在深拷贝中,需要考虑对象中的属性是否有嵌套的情况(属性的值是否还是一个对象),如果有嵌套可以通过递归的方式来实现,直到属性为基本类型,也就是说,我们需要将源对象各个属性所包含的对象依次采用递归的方式复制到新对象上。

 function clone(target) {
        if (typeof target === "object") {
          let objTarget = {};
          for (const key in target) {
              //通过递归完成拷贝
            objTarget[key] = clone(target[key]);
          }
          return objTarget;
        } else {
          return target;
        }
      }
      var obj = {
        userName: "zhangsan",
        a: {
          a1: "hello",
        },
      };
      var result = clone(obj);
      console.log(result);

以上就是一个最简单的深拷贝功能,但是在这段代码中我们只考虑了普通的object,还没有实现数组,所以将上面的代码修改一下,让其能够兼容到数组。

   function clone(target) {
        if (typeof target === "object") {
          //判断target是否为数组
          let objTarget = Array.isArray(target) ? [] : {};
          for (const key in target) {
            objTarget[key] = clone(target[key]);
          }
          return objTarget;
        } else {
          return target;
        }
      }
      var obj = {
        userName: "zhangsan",
        a: {
          a1: "hello",
        },
        //添加数组
        arr: [2, 3],
      };
      var result = clone(obj);
      console.log(result);

在上面的代码中,添加了 let objTarget = Array.isArray(target) ? [] : {};判断target是否为数组。

下面我们来看一下循环引用的情况:

   function clone(target) {
        if (typeof target === "object") {
          //判断target是否为数组
          let objTarget = Array.isArray(target) ? [] : {};
          for (const key in target) {
            objTarget[key] = clone(target[key]);
          }
          return objTarget;
        } else {
          return target;
        }
      }
      var obj = {
        userName: "zhangsan",
        a: {
          a1: "hello",
        },
        //添加数组
        arr: [2, 3],
      };
      obj.o = obj; //构成了循环引用
      var result = clone(obj);
      console.log(result);

在上面的代码中,添加了obj.o=obj.然后出现了Maximum call stack size exceeded

以上的错误表明了递归进入了死循环导致栈内存溢出。

原因是:对象存在循环引用的情况,也就是对象的属性间接或直接引用了自身的情况。

解决的方法:这里我们可以额外开辟一个存储空间,在这个存储空间中存储当前对象和拷贝对象之间的对应关系。

当需要拷贝当前的对象的时候,先去这个存储空间中进行查找,如果没有拷贝过这个对象,执行拷贝操作。如果已经拷贝过这个对象,直接返回,这样就可以解决循环引用的问题。

   let map = new WeakMap();
      function clone(target) {
        if (typeof target === "object") {
          //判断target是否为数组
          let objTarget = Array.isArray(target) ? [] : {};
          // 如果有直接返回
          if (map.get(target)) {
            return target;
          }
          //存储当前对象与拷贝对象的对应关系
          map.set(target, objTarget);
          for (const key in target) {
            objTarget[key] = clone(target[key]);
          }
          return objTarget;
        } else {
          return target;
        }
      }
      var obj = {
        userName: "zhangsan",
        a: {
          a1: "hello",
        },
        //添加数组
        arr: [2, 3],
      };
      obj.o = obj; //构成了循环引用
      var result = clone(obj);
      console.log(result);

以上就是一个基本的深拷贝的案例。

5、重写原型对象的问题

原型对象

constructor.png 我们知道每个函数在创建的时候都会有一个prototype属性,它指向函数的原型对象。

在这个对象中可以包含所有实例共享的属性和方法。例如上图中的sayName方法。

同时在每个原型对象上都会增加一个constructor属性,该属性指向prototype属性所在的构造函数,如上图所示。

proto.png

当我们通过new操作符创建一个实例的时候,该实例就有了一个__proto__属性,该属性指向了构造函数的原型对象,如上图所示:

所以说,__proto__属性可以看作是一个连接实例与构造函数的原型对象的桥梁。

所以三者的关系是,每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。通俗点说就是,实例通过内部指针可以访问到原型对象,原型对象通过constructor指针,又可以找到构造函数。也就是上图体现的内容。

下面我们来看一个问题:重写原型对象

我们在前面写代码的时候,每次都是为原型对象添加一个属性或者函数时,都是直接给Person.prototype上添加,这种写法比较冗余。

我们可以将属性和方法写成一个字面量对象的形式,然后在赋值给prototype属性。

如下代码所示:

   function Person() {}
      Person.prototype = {
        userName: "zhangsan",
        age: 20,
        sayHi: function () {
          console.log(this.userName);
        },
      };
      var person = new Person();
      person.sayHi(); //zhangsan

通过执行的结果,依然可以获取到原型对象上属性的值。

当我们把一个字面量对象赋值给prototype属性以后,实际上就是重写了原型对象。

但是这时候,我们打印Person.prototype.constructor的时候,发现不在指向Person这个构造函数,而是指向了Object构造函数。

    function Person() {}
      Person.prototype = {
        userName: "zhangsan",
        age: 20,
        sayHi: function () {
          console.log(this.userName);
        },
      };
      var person = new Person();
      person.sayHi();
      console.log(Person.prototype.constructor); // Object

原因是:在重写prototype的时候,我们使用字面量创建了一个新的对象,并且这个新的对象中少了constructor属性,

如下图所示

原型对象重写.png 而我们可以看到在字面量对象中有一个__proto__属性,指向了Object的原型对象,这时,只能去Object原型对象中查找是否有constructor属性,而Object原型对象中的constructor指向的还是Object.所以最终输出结果为Object.

我们怎样避免这种情况呢?

可以在重写原型对象的时候添加constructor属性。这样就不用在去新对象的原型对象中查找constructor属性了。

function Person() {}
      Person.prototype = {
        constructor: Person, //添加constructor
        userName: "zhangsan",
        age: 20,
        sayHi: function () {
          console.log(this.userName);
        },
      };
      var person = new Person();
      person.sayHi();
      console.log(Person.prototype.constructor);// Person

重写了原型对象以后,还需要注意一个问题,就是在重写原型对象之前,已经生成的对象的实例,无法获取新的原型对象中的属性和方法。

如下代码所示:

  <script>
      function Person() {}
      var person = new Person();//在重写原型对象之前,生成对象的实例
      Person.prototype = {
        constructor: Person, //添加constructor
        userName: "zhangsan",
        age: 20,
        sayHi: function () {
          console.log(this.userName);
        },
      };
      //   var person = new Person();
      person.sayHi(); //  person.sayHi is not a function,无法获取sayHi函数。
      console.log(Person.prototype.constructor);
    </script>

造成上面错误的原因是:person这个对象指向的是最初的原型对象,而最初的原型对象中是没有sayHi这个方法的。

所以在执行的时候会抛出异常。

6、继承的实现方式有哪些

关于继承的实现方式,这里做一个汇总。

原型链继承

代码如下:

      function Animal() {
        this.superType = "Animal";
        this.name = name || "动物";
        //实例方法
        this.sleep = function () {
          console.log(this.name + "正在睡觉!!");
        };
      }
//原型上的函数
      Animal.prototype.eat = function (food) {
        console.log(this.name + "正在吃:" + food);
      };
      function Dog(name) {
        this.name = name;
      }
      // 改变Dog的prototype指向,指向了一个Animal实例,实现了原型继承
      Dog.prototype = new Animal();
      var doggie = new Dog("wangcai");
      console.log(doggie.superType);
      doggie.sleep();
      doggie.eat("狗粮");

在上面的代码中,将Animal的实例赋值给了Dog的原型对象,这样就实现了原型的继承,所以Dog的实例可以获取父类Animal中的superType属性,调用父类中的实例方法,原型上的函数。

下面,可以通过一张图来理解一下:

原型继承1.png 原来的构造函数Dogprototype指向的是Dog的原型对象,但是现在指向了Animal的实例对象。也就是说构造函数Dog的原型对象为Animal的实例对象。

这样会出现什么样的效果呢?

原型继承2.png

注意:上面我们所写的代码还是有一个小的问题的。

Dog.prototype.constructor指向了Animal

Dog.prototype.constructor===Animal // true

这里,还是要求Dog.prototype.constructor指向Dog

 function Animal() {
        this.superType = "Animal";
        this.name = name || "动物";
        //实例方法
        this.sleep = function () {
          console.log(this.name + "正在睡觉!!");
        };
      }
      Animal.prototype.eat = function (food) {
        console.log(this.name + "正在吃:" + food);
      };
      function Dog(name) {
        this.name = name;
      }
      // 改变Dog的prototype指向,指向了一个Animal实例,实现了原型继承
      Dog.prototype = new Animal();
      // 将Dog的构造函数指向自身
      Dog.prototype.constructor = Dog;
      var doggie = new Dog("wangcai");
      console.log(doggie.superType);
      doggie.sleep();
      doggie.eat("狗粮");

原型继承的优点:

第一:实现起来非常简单

只要设置子类的portotype属性为父类的实例即可。

第二:可以通过子类的实例直接访问父类原型链中的属性和函数。

原型继承的缺点:

第一:我们知道子类的所有实例将共享父类的属性,这样就会导致一个问题:如果父类中的某个属性的值为引用类型,某个子类的实例去修改这个属性的值,就会影响到其它实例的值。

如下代码所示:

    function Person() {
        this.emotion = ["吃饭", "睡觉", "学习"]; // 爱好
      }
      function Studnet(id) {
        this.id = id; // 学号
      }
      Studnet.prototype = new Person();
      Studnet.prototype.constructor = Studnet;
      var stu1 = new Studnet(1001);
      console.log(stu1.emotion); // ["吃饭", "睡觉", "学习"]
      stu1.emotion.push("玩游戏");
      console.log(stu1.emotion); // ["吃饭", "睡觉", "学习", "玩游戏"]
      //创建 stu2对象
      var stu2 = new Studnet(1002);
      console.log(stu2.emotion); // ["吃饭", "睡觉", "学习", "玩游戏"]

通过上面的代码,我们可以看到stu1对象向数组emotion数组中添加了一项以后,stu2对象也收到了影响。

第二:在创建子类的实例的时候,无法向父类的构造函数中传递参数。

在通过new操作符创建子类的实例的时候,会调用子类的构造函数,而在子类的构造函数中并没有设置与父类的关联操作,所以无法向父类的构造函数中传递参数。

第三:在给子类的原型对象上添加属性或者是方法的时候,一定要放在Student.prototype=new Person()语句的后面。

如下代码:

   function Person() {
        this.emotion = ["吃饭", "睡觉", "学习"]; // 爱好
      }
      function Studnet(id) {
        this.id = id; // 学号
      }
//在Studnet.prototype = new Person();代码前给Student的prototype添加study方法。
      Studnet.prototype.study = function () {
        console.log("好好学习,天天向上");
      };
      Studnet.prototype = new Person();
      Studnet.prototype.constructor = Studnet;
      var stu1 = new Studnet(1001);
      stu1.study();

指向上面的代码,会出现stu1.study is not a function的错误。

原因:后面通过Studnet.prototype = new Person();这行代码对Student的原型对象进行了重写,所以导致study方法无效了。

修改后的代码:

      function Person() {
        this.emotion = ["吃饭", "睡觉", "学习"]; // 爱好
      }
      function Studnet(id) {
        this.id = id; // 学号
      }

      Studnet.prototype = new Person();
      Studnet.prototype.constructor = Studnet;
//放在了Studnet.prototype=new Person语句的后面
      Studnet.prototype.study = function () {
        console.log("好好学习,天天向上");
      };
      var stu1 = new Studnet(1001);
      stu1.study();

构造函数继承

在子类的构造函数中,通过apply()方法或者是call()方法,调用父类的构造函数,从而实现继承功能。

  function Person() {
        this.emotion = ["吃饭", "睡觉", "学习"]; // 爱好
      }
      function Studnet(id) {
        this.id = id; // 学号
        Person.call(this);
      }
      var stu1 = new Studnet(1001);
      console.log(stu1.emotion);

如下代码:

   function Person() {
        this.emotion = ["吃饭", "睡觉", "学习"]; // 爱好
      }
      function Studnet(id) {
        this.id = id; // 学号
        Person.call(this);
      }
      var stu1 = new Studnet(1001);
      var stu2 = new Studnet(1002);
      stu1.emotion.push("玩游戏");
      console.log(stu1.emotion); // ["吃饭", "睡觉", "学习", "玩游戏"]
      console.log(stu2.emotion); // ["吃饭", "睡觉", "学习"]

通过上面的代码,可以看到stu1对象向emotion数组中添加数据,并不会影响到stu2对象。

构造函数继承的优点

第一:由于在子类的构造中通过call改变了父类中的this指向,导致了在父类构造函数中定义的属性或者是方法都赋值给了子类,这样生成的每个子类的实例中都具有了这些属性和方法。而且它们之间是互不影响的,即使是引用类型。

第二:创建子类的实例的时候,可以向父类的构造函数中传递参数。

//传递age参数   
function Person(age) {
        this.emotion = ["吃饭", "睡觉", "学习"]; 
        this.age = age;
      }
//传递age参数
      function Studnet(id, age) {
        this.id = id; 
         // 传递age参数
        Person.call(this, age);
      }
      var stu1 = new Studnet(1001, 20);//传递年龄
      var stu2 = new Studnet(1002, 21);
      stu1.emotion.push("玩游戏");
      console.log(stu1.emotion); // ["吃饭", "睡觉", "学习", "玩游戏"]
      console.log(stu2.emotion); // ["吃饭", "睡觉", "学习"]
      console.log(stu1.age); // 20
      console.log(stu2.age); // 21

构造函数继承的缺点

第一:子类只能继承父类中实例的属性和方法,无法继承父类原型对象上的属性和方法。

   function Person(age) {
        this.emotion = ["吃饭", "睡觉", "学习"]; // 爱好
        this.age = age;
      }
      // 原型上的方法
      Person.prototype.study = function () {
        console.log("好好学习,天天向上");
      };
      function Studnet(id, age) {
        this.id = id; // 学号
        Person.call(this, age);
      }
      var stu = new Studnet(1001, 20);
      console.log(stu.age); // 20
      stu.study(); //stu.study is not a function

第二:在父类的构造函数中添加一个实例方法,对应的子类也就有了该实例方法,但是问题时,每创建一个子类的实例,都会有一个父类中的实例方法,这样导致的结果就是占用内存比较大。以前我们是定义在prototype原型上来解决这个问题的,但是在构造函数的继承中,又出现了这个。

  function Person(age) {
        this.emotion = ["吃饭", "睡觉", "学习"]; // 爱好
        this.age = age;
        this.study = function () {
          console.log(this.id + "号同学要努力学习");
        };
      }

      function Studnet(id, age) {
        this.id = id; // 学号
        Person.call(this, age);
      }
      var stu = new Studnet(1001, 20);
      stu.study();
      var stu1 = new Studnet(1002, 20);
      stu1.study();
//stu对象和stu1对象都单独有一个study方法。

拷贝继承

所谓的拷贝继承指的是先创建父类的实例,然后通过for...in的方式来遍历父类实例中的所有属性和方法,并依次赋值给子类的实例,同时原型上的属性和函数也赋给子类的实例。

 function Person(age) {
        this.emotion = ["吃饭", "睡觉", "学习"]; // 爱好
        this.age = age;
        this.study = function () {
          console.log(this.id + "号同学要努力学习");
        };
      }
      Person.prototype.run = function () {
        console.log(this.id + "号学生正在跑步,年龄是:" + this.age);
      };

      function Studnet(id, age) {
        var person = new Person(age);
        for (var key in person) {
          if (person.hasOwnProperty(key)) {
            this[key] = person[key];
          } else {
            Studnet.prototype[key] = person[key];
          }
        }
        // 子类自身的属性
        this.id = id;
      }
      var student = new Studnet(1001, 21);
      student.study();
      student.run();

在上面的代码中,创建了父类Person,并且在该类中指定了相应的实例属性和实例方法,同时为其原型对象中也添加了方法。

Studnet这个子类中,首先会创建父类Person的实例,然后通过for...in来进行遍历,获取父类中的属性和方法,获取以后进行判断,如果person.hasOwnProperty(key)返回值为false,表示获取到的是父类原型对象上的属性和方法,所以也要添加到子类的prototype属性上,成为子类的原生对象上的属性或者是方法。

最后创建子类的实例student,通过子类的实例student,可以访问继承到的属性或者是方法。

拷贝继承的优点

第一:可以实现向父类中的构造方法中传递参数。

第二:能够实现让子类继承父类中的实例属性,实例方法以及原型对象上的属性和方法。

拷贝继承的缺点

父类的所有属性和方法,子类都需要复制拷贝一遍,所以比较消耗内存。

组合继承

组合继承的核心思想是将构造函数继承与原型继承两种方式组合在一起。

  function Person(age) {
        this.emotion = ["吃饭", "睡觉", "学习"]; // 爱好
        this.age = age;
        this.study = function () {
          console.log(this.id + "号同学要努力学习");
        };
      }
      Person.prototype.run = function () {
        console.log(this.id + "号学生正在跑步,年龄是:" + this.age);
      };
      function Studnet(id, age) {
        Person.call(this, age);
        this.id = id; //子类独有的属性
      }
      Studnet.prototype = new Person();
      Studnet.prototype.constructor = Studnet;
      var student = new Studnet(1001, 21);
      student.run();
      console.log("爱好是:" + student.emotion);

组合继承的优点

第一:通过Person.call(this,ge)这个行代码,可以将父类中的实例属性和方法添加到子类Student中,另外通过Studnet.prototype = new Person(); 可以将父类的原型对象上的属性和函数绑定到Student的原型对象上。

第二:可以向父类的构造函数中传递参数。

组合继承的缺点

组合继承的主要缺点是父类的实例属性会绑定两次。

第一次是在子类的构造函数中通过call( )函数调用了一次父类的构造函数,完成实例属性和方法的绑定操作。

第二次是在改写子类prototype属性的时候,我们执行了一次new Person()的操作,这里又将父类的构造函数调用了一次,完成了属性的绑定操作。

所以在整个组合继承的过程中,父类实例的属性和方法会进行两次的绑定操作。当然这里需要你注意的一点是:通过call()函数完成父类中实例属性和方法的绑定的优先级要高于通过改写子类prototype的方式。也就是说第一种方式会覆盖第二种方式:

如下代码所示:

 function Person(age) {
        this.emotion = ["吃饭", "睡觉", "学习"]; // 爱好
        this.age = age;
     //实例方法
        this.study = function () {
          console.log(this.id + "号同学要努力学习");
        };
      }
      Person.prototype.run = function () {
        console.log(this.id + "号学生正在跑步,年龄是:" + this.age);
      };
    // 原型方法
      Person.prototype.study = function () {
        console.log(this.id + "号学生需要好好学习");
      };
      function Student(id, age) {
        Person.call(this, age);
        this.id = id; //子类独有的属性
      }
      Student.prototype = new Person();
      Student.prototype.constructor = Student;
      var student = new Student(1001, 21);
      student.run();
      console.log("爱好是:" + student.emotion);
      student.study(); //调用父类的实例方法study

在上面的代码中,在父类Person的构造函数中定义了实例方法study,同时在其原型对象上也定义了一个study方法。

通过子类的实例调用study方法的时候,调用的是父类的实例方法study.

寄生式组合继承

   function Person(age) {
        this.emotion = ["吃饭", "睡觉", "学习"]; // 爱好
        this.age = age;
        this.study = function () {
          console.log(this.id + "号同学要努力学习");
        };
      }
      Person.prototype.run = function () {
        console.log(this.id + "号学生正在跑步,年龄是:" + this.age);
      };
      Person.prototype.study = function () {
        console.log(this.id + "号学生需要好好学习");
      };
      function Studnet(id, age) {
        Person.call(this, age);
        this.id = id;
      }
// 定义Super构造函数
      function Super() {}
//Super.prototype原型对象指向了Person.prototype
      Super.prototype = Person.prototype;
//Student.prototype原型对象指向了Super的实例,这样就去掉了Person父类的实例属性。
      Studnet.prototype = new Super();
      Studnet.prototype.constructor = Studnet;
      var student = new Studnet(1001, 21);
      student.run();
      console.log("爱好是:" + student.emotion);
      student.study();

在上面的代码中,创建了一个Super构造函数,让Super.prototype的原型指向了Person.prototype,同时将Super的对象赋值给了Student.prototype,这样就去掉了Person父类的实例属性。

通过寄生式组合继承解决了组合继承的问题。

同时,在以后的应用中,可以使用组合继承,也可以使用寄生式组合继承。

7、模拟jQuery实现

下面我们通过模拟实现一个简单的jQuery,来巩固原型的应用。

  <script>
      // 为jQuery起一个别名,模仿jQuery的框架
      var $ = (jQuery = function () {});
      // 为jQuery原型起一个别名
      //这里没有直接赋值给fn,否则它属于window对象,容易造成全局污染
      //后面要访问jquery的原型,可以直接通过jQuery.fn来实现
      jQuery.fn = jQuery.prototype = {
        version: "6.1.1", //添加原型属性,表示jquery的版本
        //添加原型方法,表示返回jquery对象的长度
        size: function () {
          return this.length;
        },
      };
      
    </script>

下面,我们使用jQuery原型中的size方法和version属性。

   // 为jQuery起一个别名,模仿jQuery的框架
      var $ = (jQuery = function () {});
      // 为jQuery原型起一个别名
      //这里没有直接赋值给fn,否则它属于window对象,容易造成全局污染
      //后面要访问jquery的原型,可以直接通过jQuery.fn来实现
      jQuery.fn = jQuery.prototype = {
        version: "6.1.1", //添加原型属性,表示jquery的版本
        //添加原型方法,表示返回jquery对象的长度
        size: function () {
          return this.length;
        },
      };
      var jq = new $();
      console.log(jq.version); // 6.1.1
      console.log(jq.size()); // undefined

在上面的代码中,我们是创建了一个jquery的实例,然后通过该实例完成了原型属性和方法的调用。

但是在jquery库中,是采用如下的方式进行调用。

$().version;
$().size()

通过以上的两行代码,我们可以看到在jQuery库中,并没有使用new操作符,而是直接使用小括号运算符完成了对jQuery构造函数的调用。然后后面直接访问原型成员。

那应该怎样实现这种操作?

我们想到的就是,在jquery的构造函数中,直接创建jQuery类的实例。

   // 为jQuery起一个别名,模仿jQuery的框架
      var $ = (jQuery = function () {
        return new jQuery();
      });
      // 为jQuery原型起一个别名
      //这里没有直接赋值给fn,否则它属于window对象,容易造成全局污染
      //后面要访问jquery的原型,可以直接通过jQuery.fn来实现
      jQuery.fn = jQuery.prototype = {
        version: "6.1.1", //添加原型属性,表示jquery的版本
        //添加原型方法,表示返回jquery对象的长度
        size: function () {
          return this.length;
        },
      };
      $().version;
      //   var jq = new $();
      //   console.log(jq.version); // 6.1.1
      //   console.log(jq.size());

在上面的代码中,给jQuery构造函数直接返回了它的实例return new jQuery();

然后获取原型对象中的size属性的值:$().version.

但是,出现了如下的错误:

Uncaught RangeError: Maximum call stack size exceeded

以上错误的含义是栈内存溢出。

原因就是:当我们通过$()调用构造函数的时候,内部有执行了new操作,这时,又会重新执行jQuery的构造函数,这样就造成了死循环。

  
      var $ = (jQuery = function () {
        return jQuery.fn.init(); //调用原型中的`init方法`
      });
     
      jQuery.fn = jQuery.prototype = {
        init: function () {
          return this; //返回jquery的原型对象
        },
        version: "6.1.1",        
        size: function () {
          return this.length;
        },
      };
      console.log($().version);

在上面的代码中,在jQuery的构造方法中,调用的是原型中的init方法,在该方法中,返回了jquery的原型对象。

最后进行输出:cosnole.log($().version)

但是,以上的处理还是隐藏一个问题,具体看如下代码:

 var $ = (jQuery = function () {
        return jQuery.fn.init(); 
      });
      jQuery.fn = jQuery.prototype = {
        init: function () {
          this.length = 0; //原型属性length
          this._size = function () { //原型方法
            return this.length;
          };
          return this;
        },
        version: "6.1.1",
        length: 1, // 原型属性
        size: function () {
          return this.length;
        },
      };
      console.log($().version);
      console.log($()._size()); // 0
      console.log($().size()); // 0

在上面的代码中,在init这个原型方法中添加了lenght属性与_size方法,在该方法中打印length的值。

 var $ = (jQuery = function () {
        return new jQuery.fn.init(); //调用原型中的`init方法`
      });

jQuery的构造函数中,通过new操作符创建了一个实例对象,这样init()方法中的this指向的就是init方法的实例,而不是jQuery.prototype这个原型对象了。

  console.log($().version); // 返回undefined
      console.log($()._size()); // 0
      console.log($().size()); // 抛出异常:Uncaught TypeError: $(...).size is not a function

下面,我们来看一下怎样解决现在面临的问题。

  var $ = (jQuery = function () {
        return new jQuery.fn.init(); //调用原型中的`init方法`
      });
      jQuery.fn = jQuery.prototype = {
        init: function () {
          this.length = 0;
          this._size = function () {
            return this.length;
          };
          return this;
        },
        version: "6.1.1",
        length: 1,
        size: function () {
          return this.length;
        },
      };
		// 将`jQuery`的原型对象覆盖掉init的原型对象。
      jQuery.fn.init.prototype = jQuery.fn;
      console.log($().version); //6.1.1
      console.log($()._size()); // 0
      console.log($().size()); // 0

在上面的代码中,我们添加了一行代码:

 jQuery.fn.init.prototype = jQuery.fn;
 console.log($().version); 

下面,要实现的是选择器功能

jQuery构造函数包括两个参数,分别是selectorcontext,selector表示的是选择器,context表示匹配的上下文,也就是可选择的访问,一般表示的是一个DOM元素。这里我们只考虑标签选择器。

<script>
      // 给构造函数传递selector,context两个参数
      var $ = (jQuery = function (selector, context) {
        return new jQuery.fn.init(selector, context); //调用原型中的`init方法`
      });
      jQuery.fn = jQuery.prototype = {
        init: function (selector, context) {
          selector = selector || document; //初始化选择器,默认值为document
          context = context || document; // 初始化上下文对象,默认值为document
          if (selector.nodeType) {
            // 如果是DOM元素
            // 把该DOM元素赋值给实例对象
            this[0] = selector;
            this.length = 1; //表示包含了1个元素
            this.context = selector; //重新设置上下文对象
            return this; //返回当前实例
          }
          if (typeof selector === "string") {
            //如果选择器是一个字符串
            var e = context.getElementsByTagName(selector); // 获取指定名称的元素
            //通过for循环将所有元素存储到当前的实例中
            for (var i = 0; i < e.length; i++) {
              this[i] = e[i];
            }
            this.length = e.length; //存储元素的个数
            this.context = context; //保存上下文对象
            return this; //返回当前的实例
          } else {
            this.length = 0;
            this.context = context;
            return this;
          }
          //   this.length = 0;
          //   console.log("init==", this);
          //   this._size = function () {
          //     return this.length;
          //   };
          //   return this;
        },

        // version: "6.1.1",
        // length: 1,
        // size: function () {
        //   return this.length;
        // },
      };
      jQuery.fn.init.prototype = jQuery.fn;
      window.onload = function () {
        console.log($("div").length);
      };
      //   console.log($().version);
      //   console.log($()._size()); // 0
      //   console.log($().size()); // 0
      //   var jq = new $();
      //   console.log(jq.version); // 6.1.1
      //   console.log(jq.size());
    </script>
    <div></div>
    <div></div>
  </body>

在上面的代码中,当页面加载完以后,这时会触发onload事件,在该事件对应的处理函数中,通过$("div"),传递的是字符串,

selector参数表示的就是div这个字符串,这里没有传递context参数,表示的就是document对象。

最后打印元素的个数。

在使用jQuery库的时候,我们经常可以看到如下的操作:

$('div').html()

以上代码的含义就是直接在jQuery对象上调用html( )方法来操作jQuery包含所有的DOM元素。

html()方法的实现如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script>
      // 给构造函数传递selector,context两个参数
      var $ = (jQuery = function (selector, context) {
        return new jQuery.fn.init(selector, context); //调用原型中的`init方法`
      });
      jQuery.fn = jQuery.prototype = {
        init: function (selector, context) {
          selector = selector || document; //初始化选择器,默认值为document
          context = context || document; // 初始化上下文对象,默认值为document
          if (selector.nodeType) {
            // 如果是DOM元素
            // 把该DOM元素赋值给实例对象
            this[0] = selector;
            this.length = 1; //表示包含了1个元素
            this.context = selector; //重新设置上下文对象
            return this; //返回当前实例
          }
          if (typeof selector === "string") {
            //如果选择器是一个字符串
            var e = context.getElementsByTagName(selector); // 获取指定名称的元素
            //通过for循环将所有元素存储到当前的实例中
            for (var i = 0; i < e.length; i++) {
              this[i] = e[i];
            }
            this.length = e.length; //存储元素的个数
            this.context = context; //保存上下文对象
            return this; //返回当前的实例
          } else {
            this.length = 0;
            this.context = context;
            return this;
          }
          //   this.length = 0;
          //   console.log("init==", this);
          //   this._size = function () {
          //     return this.length;
          //   };
          //   return this;
        },
        html: function (val) {
          jQuery.each(
            this,
            function (val) {
              this.innerHTML = val;
            },
            val
          );
        },

        // version: "6.1.1",
        // length: 1,
        // size: function () {
        //   return this.length;
        // },
      };
      jQuery.fn.init.prototype = jQuery.fn;

      //提供each扩展方法
      jQuery.each = function (object, callback, args) {
        //通过for循环的方式来遍历jQuery对象中的每个DOM元素。
        for (var i = 0; i < object.length; i++) {
          // 在每个DOM元素上调用回调函数
          callback.call(object[i], args);
        }
        return object; //返回jQuery对象。
      };
      window.onload = function () {
        // console.log($("div").length);
        $("div").html("<h2>hello<h2>");
      };
      //   console.log($().version);
      //   console.log($()._size()); // 0
      //   console.log($().size()); // 0
      //   var jq = new $();
      //   console.log(jq.version); // 6.1.1
      //   console.log(jq.size());
    </script>
    <div></div>
    <div></div>
  </body>
</html>

在上面的代码中,首先添加了jQuery.each方法。

    //提供each扩展方法
      jQuery.each = function (object, callback, args) {
        //通过for循环的方式来遍历jQuery对象中的每个DOM元素。
        for (var i = 0; i < object.length; i++) {
          // 在每个DOM元素上调用回调函数
            //这里的让回调函数中的this指向了dom元素。
          callback.call(object[i], args);
        }
        return object; //返回jQuery对象。
      };

在上面的代码中,通过for循环遍历jQuery对象中的每个DOM元素。然后执行回调函数callback

jQuery的原型对象上,添加html方法

  html: function (val) {
          jQuery.each(
            this, //表示jQuery原型对象
            function (val) {
                //this表示的是dom元素,这里是div元素
              this.innerHTML = val;
            },
            val //表示传递过来的`<h2>hello<h2>`
          );
        },

html方法中完成对jQuery.each方法的调用。

window.onload的方法修改成如下的形式:

    window.onload = function () {
        // console.log($("div").length);
        $("div").html("<h2>hello<h2>");
      };

下面我们实现jQuery的扩展功能

jQuery 提供了良好的扩展接口,方便用户自定义 jQuery 方法。根据设计习惯,如果为 jQuery 或者 jQuery.prototype 新增方法时,我们可以直接通过点语法来实现,例如上面我们扩展的html方法,或者在 jQuery.prototype 对象结构内增加。但是,如果分析 jQuery 源码,会发现它是通过 extend() 函数来实现功能扩展的。

通过extend()方法来实现扩展的好处是:方便用户快速的扩展jQuery功能,但不会破坏jQuery框架的结构。如果直接在jQuery源码中添加方法,这样就破坏了Jquery框架的结构,不方便后期的代码维护。

如果后期不需要某个功能,可以直接使用Jquery提供的方法删除,而不需要从源码中在对该功能进行删除。

extend() 函数的功能很简单,它只是把指定对象的方法复制给 jQuery 对象或者 jQuery.prototype

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script>
      // 给构造函数传递selector,context两个参数
      var $ = (jQuery = function (selector, context) {
        return new jQuery.fn.init(selector, context); //调用原型中的`init方法`
      });
      jQuery.fn = jQuery.prototype = {
        init: function (selector, context) {
          selector = selector || document; //初始化选择器,默认值为document
          context = context || document; // 初始化上下文对象,默认值为document
          if (selector.nodeType) {
            // 如果是DOM元素
            // 把该DOM元素赋值给实例对象
            this[0] = selector;
            this.length = 1; //表示包含了1个元素
            this.context = selector; //重新设置上下文对象
            return this; //返回当前实例
          }
          if (typeof selector === "string") {
            //如果选择器是一个字符串
            var e = context.getElementsByTagName(selector); // 获取指定名称的元素
            //通过for循环将所有元素存储到当前的实例中
            for (var i = 0; i < e.length; i++) {
              this[i] = e[i];
            }
            this.length = e.length; //存储元素的个数
            this.context = context; //保存上下文对象
            return this; //返回当前的实例
          } else {
            this.length = 0;
            this.context = context;
            return this;
          }
          //   this.length = 0;
          //   console.log("init==", this);
          //   this._size = function () {
          //     return this.length;
          //   };
          //   return this;
        },
        // html: function (val) {
        //   jQuery.each(
        //     this,
        //     function (val) {
        //       this.innerHTML = val;
        //     },
        //     val
        //   );
        // },

        // version: "6.1.1",
        // length: 1,
        // size: function () {
        //   return this.length;
        // },
      };
      jQuery.fn.init.prototype = jQuery.fn;

      //提供each扩展方法
      jQuery.each = function (object, callback, args) {
        //通过for循环的方式来遍历jQuery对象中的每个DOM元素。
        for (var i = 0; i < object.length; i++) {
          // 在每个DOM元素上调用回调函数
          callback.call(object[i], args);
        }
        return object; //返回jQuery对象。
      };

      jQuery.extend = jQuery.fn.extend = function (obj) {
        for (var prop in obj) {
          this[prop] = obj[prop];
        }
        return this;
      };
      jQuery.fn.extend({
        html: function (val) {
          jQuery.each(
            this,
            function (val) {
              this.innerHTML = val;
            },
            val
          );
        },
      });
      window.onload = function () {
        // console.log($("div").length);
        $("div").html("<h2>hello<h2>");
      };
      //   console.log($().version);
      //   console.log($()._size()); // 0
      //   console.log($().size()); // 0
      //   var jq = new $();
      //   console.log(jq.version); // 6.1.1
      //   console.log(jq.size());
    </script>
    <div></div>
    <div></div>
  </body>
</html>

在上面的代码中,我们为jQuery的原型对象添加了extend方法

     jQuery.extend = jQuery.fn.extend = function (obj) {
        for (var prop in obj) {
          this[prop] = obj[prop];
        }
        return this;
      };

obj对象中的属性添加到jQuery原型对象上。

下面调用extend方法,同时设置html属性

  jQuery.fn.extend({
        html: function (val) {
          jQuery.each(
            this,
            function (val) {
              this.innerHTML = val;
            },
            val
          );
        },
      });

这样jQuery原型对象上就有了html方法。

而把原来的html方法的代码注释掉。

刷新浏览器,查看对应的效果。

参数传递

我们在使用jquery的方法的时候,需要进行参数的传递,而且一般都要求传递的参数都是对象。

使用对象作为参数进行传递的好处,就是方便参数的管理,例如参数个数不受限制。

如果使用对象作为参数进行传递,需要解决的问题:如何解决并提取参数,如何处理默认值等问题。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script>
      // 给构造函数传递selector,context两个参数
      var $ = (jQuery = function (selector, context) {
        return new jQuery.fn.init(selector, context); //调用原型中的`init方法`
      });
      jQuery.fn = jQuery.prototype = {
        init: function (selector, context) {
          selector = selector || document; //初始化选择器,默认值为document
          context = context || document; // 初始化上下文对象,默认值为document
          if (selector.nodeType) {
            // 如果是DOM元素
            // 把该DOM元素赋值给实例对象
            this[0] = selector;
            this.length = 1; //表示包含了1个元素
            this.context = selector; //重新设置上下文对象
            return this; //返回当前实例
          }
          if (typeof selector === "string") {
            //如果选择器是一个字符串
            var e = context.getElementsByTagName(selector); // 获取指定名称的元素
            //通过for循环将所有元素存储到当前的实例中
            for (var i = 0; i < e.length; i++) {
              this[i] = e[i];
            }
            this.length = e.length; //存储元素的个数
            this.context = context; //保存上下文对象
            return this; //返回当前的实例
          } else {
            this.length = 0;
            this.context = context;
            return this;
          }
          //   this.length = 0;
          //   console.log("init==", this);
          //   this._size = function () {
          //     return this.length;
          //   };
          //   return this;
        },
        // html: function (val) {
        //   jQuery.each(
        //     this,
        //     function (val) {
        //       this.innerHTML = val;
        //     },
        //     val
        //   );
        // },

        // version: "6.1.1",
        // length: 1,
        // size: function () {
        //   return this.length;
        // },
      };
      jQuery.fn.init.prototype = jQuery.fn;

      //提供each扩展方法
      jQuery.each = function (object, callback, args) {
        console.log("args=", args);
        //通过for循环的方式来遍历jQuery对象中的每个DOM元素。
        for (var i = 0; i < object.length; i++) {
          // 在每个DOM元素上调用回调函数
          callback.call(object[i], args);
        }

        return object; //返回jQuery对象。
      };

      // jQuery.extend = jQuery.fn.extend = function (obj) {
      //   for (var prop in obj) {
      //     this[prop] = obj[prop];
      //   }
      //   return this;
      // };
      jQuery.extend = jQuery.fn.extend = function () {
        var destination = arguments[0],
          source = arguments[1];
        //如果存在两个参数,并且都是对象
        if (typeof destination === "object" && typeof source === "object") {
          //把第二个对象合并到第一个参数对象中,并返回合并后的对象
          for (var property in source) {
            destination[property] = source[property];
          }
          return destination;
        } else {
          for (var prop in destination) {
            this[prop] = destination[prop];
          }
          return this;
        }
      };
      jQuery.fn.extend({
        html: function (val) {
          jQuery.each(
            this,
            function (val) {
              this.innerHTML = val;
            },
            val
          );
        },
      });
      jQuery.fn.extend({
        fontStyle: function (obj) {
          var defaults = {
            color: "#ccc",
            size: "16px",
          };
          //如果有参数,会覆盖掉默认的参数
          defaults = jQuery.extend(defaults, obj || {});
          //为每个DOM元素执设置样式.
          jQuery.each(this, function () {
            this.style.color = defaults.color;
            this.style.fontSize = defaults.size;
          });
        },
      });
      window.onload = function () {
        // console.log($("div").length);
        $("div").html("<h2>hello<h2>");
        $("p").fontStyle({
          color: "red",
          size: "30px",
        });
      };
      //   console.log($().version);
      //   console.log($()._size()); // 0
      //   console.log($().size()); // 0
      //   var jq = new $();
      //   console.log(jq.version); // 6.1.1
      //   console.log(jq.size());
    </script>
    <div></div>
    <div></div>
    <p>学习前端</p>
    <p>学习前端</p>
  </body>
</html>

在上面的代码中,重新改造extend方法。

     jQuery.extend = jQuery.fn.extend = function () {
        var destination = arguments[0],
          source = arguments[1];
        //如果存在两个参数,并且都是对象
        if (typeof destination === "object" && typeof source === "object") {
          //把第二个对象合并到第一个参数对象中,并返回合并后的对象
          for (var property in source) {
            destination[property] = source[property];
          }
          return destination;
        } else {
          for (var prop in destination) {
            this[prop] = destination[prop];
          }
          return this;
        }
      };

extend方法中,首先获取两个参数,然后判断这两个参数是否都是对象,如果都是对象,把第二个参数对象合并到第一个参数对象中,并返回合并后的对象。

否则,将第一个参数对象复制到jquery的原型对象上。

     jQuery.fn.extend({
        fontStyle: function (obj) {
          var defaults = {
            color: "#ccc",
            size: "16px",
          };
          //如果有参数,会覆盖掉默认的参数
          defaults = jQuery.extend(defaults, obj || {});
            // console.log("this==", this);//init {0: p, 1: p, length: 2, context: document}
          //为每个DOM元素执设置样式.
          jQuery.each(this, function () {
               //这里的this表示的是p标签,因为在each方法内部通过call改变了this指向,让this指向了每个遍历得到的p元素
            this.style.color = defaults.color;
            this.style.fontSize = defaults.size;
          });
        },
      });

在上面的代码中, 调用了extend方法,然后传递了fontStyle,这个fontStyle可以用来设置文本的颜色与字体大小。

当我们第一次调用extend方法的时候,只是传递了fontStyle这个对象,这时,会将该对象添加到jQuery原型对象上。

    window.onload = function () {
        // console.log($("div").length);
        $("div").html("<h2>hello<h2>");
        $("p").fontStyle({
          color: "red",
          size: "30px",
        });
      };

 <div></div>
    <div></div>
    <p>学习前端</p>
    <p>学习前端</p>

onload事件中,调用fontStyle方法,并且传递了一个对象,这时在fontStyle方法的内部,首先会创建一个defaults默认的对象,然后再次调用extend方法,将传递的对象合并到默认对象上,当然完成了值的覆盖。

下面调用each方法,在each方法中遍历每个元素,执行回调函数,并且改变this的指向。

封装成独立的命名空间

以上已经实现了一个简单的jQuery库,

但是这里还有一个问题,需要解决:当编写了大量的javascript代码以后,引入该jquery库就很容易出现代码冲突的问题,所以这里需要将jquery库的代码与其他的javascript代码进行隔离,这里使用闭包。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script>
      (function (window) {
        // 给构造函数传递selector,context两个参数
        var $ = (jQuery = function (selector, context) {
          return new jQuery.fn.init(selector, context); //调用原型中的`init方法`
        });
        jQuery.fn = jQuery.prototype = {
          init: function (selector, context) {
            selector = selector || document; //初始化选择器,默认值为document
            context = context || document; // 初始化上下文对象,默认值为document
            if (selector.nodeType) {
              // 如果是DOM元素
              // 把该DOM元素赋值给实例对象
              this[0] = selector;
              this.length = 1; //表示包含了1个元素
              this.context = selector; //重新设置上下文对象
              return this; //返回当前实例
            }
            if (typeof selector === "string") {
              //如果选择器是一个字符串
              var e = context.getElementsByTagName(selector); // 获取指定名称的元素
              //通过for循环将所有元素存储到当前的实例中
              for (var i = 0; i < e.length; i++) {
                this[i] = e[i];
              }
              this.length = e.length; //存储元素的个数
              this.context = context; //保存上下文对象
              return this; //返回当前的实例
            } else {
              this.length = 0;
              this.context = context;
              return this;
            }
            //   this.length = 0;
            //   console.log("init==", this);
            //   this._size = function () {
            //     return this.length;
            //   };
            //   return this;
          },
          // html: function (val) {
          //   jQuery.each(
          //     this,
          //     function (val) {
          //       this.innerHTML = val;
          //     },
          //     val
          //   );
          // },

          // version: "6.1.1",
          // length: 1,
          // size: function () {
          //   return this.length;
          // },
        };
        jQuery.fn.init.prototype = jQuery.fn;

        //提供each扩展方法
        jQuery.each = function (object, callback, args) {
          //通过for循环的方式来遍历jQuery对象中的每个DOM元素。
          for (var i = 0; i < object.length; i++) {
            // 在每个DOM元素上调用回调函数
            callback.call(object[i], args);
          }

          return object; //返回jQuery对象。
        };

        // jQuery.extend = jQuery.fn.extend = function (obj) {
        //   for (var prop in obj) {
        //     this[prop] = obj[prop];
        //   }
        //   return this;
        // };
        jQuery.extend = jQuery.fn.extend = function () {
          var destination = arguments[0],
            source = arguments[1];
          //如果存在两个参数,并且都是对象
          if (typeof destination === "object" && typeof source === "object") {
            //把第二个对象合并到第一个参数对象中,并返回合并后的对象
            for (var property in source) {
              destination[property] = source[property];
            }
            return destination;
          } else {
            for (var prop in destination) {
              this[prop] = destination[prop];
            }
            return this;
          }
        };
        // 开发jqueyr
        window.jQuery = window.$ = jQuery;
      })(window);

      jQuery.fn.extend({
        html: function (val) {
          jQuery.each(
            this,
            function (val) {
              this.innerHTML = val;
            },
            val
          );
        },
      });
      jQuery.fn.extend({
        fontStyle: function (obj) {
          var defaults = {
            color: "#ccc",
            size: "16px",
          };
          //如果有参数,会覆盖掉默认的参数
          defaults = jQuery.extend(defaults, obj || {});

          // console.log("this==", this);//init {0: p, 1: p, length: 2, context: document}
          //为每个DOM元素执设置样式.
          jQuery.each(this, function () {
            //这里的this表示的是p标签,因为在each方法内部通过call改变了this指向,让this指向了每个遍历得到的p元素

            this.style.color = defaults.color;
            this.style.fontSize = defaults.size;
          });
        },
      });
      window.onload = function () {
        // console.log($("div").length);
        $("div").html("<h2>hello<h2>");
        $("p").fontStyle({
          color: "red",
          size: "30px",
        });
      };
      //   console.log($().version);
      //   console.log($()._size()); // 0
      //   console.log($().size()); // 0
      //   var jq = new $();
      //   console.log(jq.version); // 6.1.1
      //   console.log(jq.size());
    </script>
    <div></div>
    <div></div>
    <p>学习前端</p>
    <p>学习前端</p>
  </body>
</html>

在上面的代码中,将jQuery库放在匿名函数中,然后进行自调用,并且传入window对象。

在上面所添加的代码中还要注意如下语句:

window.jQuery = window.$ = jQuery;

以上语句的作用:把闭包中的私有变量jQuery传递给window对象的jQuery属性。这样就可以在全局作用域中通过jQuery变量来访问闭包体内的jQuery框架了。

以上就是我们模拟的jQuery库。

五、DOM与事件

1、选择器

getElementById():通过id来查找对应的元素。

getElementsByClassName():通过类名来查找对应的元素,返回的是一个HTMLCollection对象。

getElementsByName():通过元素的name属性查找对应的元素,返回的是NodeList对象,它是一个类似于数组的结构。

getElementsByTagName(): 通过标签的名称来查找对应的元素,返回的是HTMLCollection对象。

querySelector:该选择器返回的是在基准元素下,选择器匹配到的元素集合中的第一个元素。该选择器的参数接收的是一个css选择

 <body>
    <div>
      <h4>标题内容</h4>
      <span>span标签内容</span>
      <p>
        段落内容
        <span>段落中的第一个span标签</span><br />
        <span>段落中的第二个span标签</span>
      </p>
    </div>
  </body>
  <script>
    console.log(document.querySelector("p span").innerHTML);// 获取p标签中第一个span标签中的内容,所以输出结果为:段落中的第一个span标签
       console.log(document.querySelector("h4,span").innerHTML);//获取第一个h4或者是span元素的内容:所以输出结果为:标题内容
        var ele = document.querySelector("p");
    console.log(ele.querySelector("div span").innerHTML);//段落中的第一个span标签。
      // 首先先找到`p`元素,然后看一下p元素下面有没有div,我们发现没有,但是依然能够匹配到span元素。
      //原因是:在匹配的过程中会优先找出最外层div元素下的span元素的集合,然后在判断span元素是否属于p元素的子元素,最后返回
      //第一个匹配到的span元素的值。
  </script>

下面,把HTML文档的结构修改成如下的形式:

<div>
      <h4>标题内容</h4>
      <span>span标签内容</span>
    <!--这里增加了一个p标签-->
      <p>第一个段落</p>
      <p>
        段落内容
        <span>段落中的第一个span标签</span><br />
        <span>段落中的第二个span标签</span>
      </p>
    </div>

执行如下代码会出现异常:

   var ele = document.querySelector("p");
    console.log(ele.querySelector("div span").innerHTML);//Cannot read property 'innerHTML' of null

原因:会找到第一个p元素,然后看一下p标签中是否有div,发现没有,但是会找出最外层div下的所有span元素的集合,看一下span元素是否属于p元素,而第一个p元素中没有span元素,所以抛出异常。

querySelectorAll()选择器:

querySelectorAll选择器与querySelector选择器的区别是:querySelectAll选择器会获取到基准元素下匹配到所有子元素的集合。返回的是一个NodeList集合。

 <div>
      <h4>标题内容</h4>
      <span>span标签内容</span>
      <p>
        段落内容
        <span>段落中的第一个span标签</span><br />
        <span>段落中的第二个span标签</span>
      </p>
    </div>
<script>
	 console.log(document.querySelectorAll("span"));//返回所有的span标签。
</script>

下面,再来看一段代码:

 <div id="container">
      <div class="bar"></div>
      <div class="foo">
        <div class="inner"></div>
      </div>
    </div>
<script>
    // 获取container下的所有div元素。
 var div1 = document.querySelectorAll("#container div");
    console.log(div1);// NodeList(3) [div.bar, div.foo, div.inner]
   
</script>

2、HTMLCollection对象与NodeList对象区别

在介绍前面的选择器的时候,它们返回的值有些是HTMLCollection对象,有些是NodeList对象,它们有什么区别?

HTMLCollection对象具有1ength属性,返回集合的长度,可以通过item()namedItem()函数来访问特定的元素。

item()函数:通过序号索引值来获取特定的某个节点,超过索引则返回null.

  <div id="container">
      <div class="bar"></div>
      <div class="foo">
        <div class="inner"></div>
      </div>
    </div>
<script>
 var main = document.getElementById("container").children;
    console.log(main); //HTMLCollection
    console.log(main.item(0)); //输出:<div class="bar"></div>
    console.log(main.item(1)); // 输出:foo元素
</script>

namedItem()函数:该函数用来返回一个节点,首先通过id属性去匹配,然后如果没有匹配到则使用name属性匹配,如果还没有匹配到则返回null. 当出现重复的id或者name属性时,只返回匹配到的第一个值。

  <form id="form1">
      <input type="text" id="userName" />
      <input type="password" id="password" name="userPwd" />
    </form>
<script>
 var form1 = document.getElementById("form1").children;
    console.log(form1.namedItem("userPwd"));//   <input type="password" id="password" name="userPwd" />
</script>

NodeList对象也具有length属性,返回集合的长度,同样也有item函数,也是通过索引定位子元素的位置。但是NodeList对象没有namedItem方法。

HTMLCollection对象与NodeList对象都是类似数组的结构,如果想调用数组中的方法,需要通过call()函数或者是apply()函数,转换为真正的数组后,可以使用数组中的函数。

同时,当我们对DOM树新增或者是删除一个节点的时候,都会立即的反映在HTMLCollection对象与NodeList对象中。

  <form id="form1">
      <input type="text" id="userName" />
      <input type="password" id="password" name="userPwd" />
    </form>
<script>
 //获取HTMLCollection集合
    var form1Children = document.getElementById("form1").children;
    // 获取form元素
    var form1 = document.getElementById("form1");
    console.log(form1Children.length); // 2 HTMLCollection中有两个子元素
    var input = document.createElement("input"); //创建input元素
    form1.appendChild(input); // 把创建的input元素添加到form元素中
    console.log(form1Children.length); // 3 可以看到HTMLCollection立即受到了影响

最后,总结一下HTMLCollection对象与NodeList对象的相同点与不同点

相同点:

第一:都是类似数组的结构,有length属性,可以通过call()函数或者是apply()函数转换成数组,使用数组中的函数。

第二:都用item函数,通过索引值获取相应的元素。

第三:都是实时的,当在DOM树上添加元素或者是删除元素,都会立即反应到HTMLCollection对象和NodeList对象上。

不同点:

第一:HTMLCollection对象中,有namedItem()函数,而NodeList对象中没有.

第二:NodeList对象中存储的是元素节点的集合,包括元素,以及节点,例如text文本节点,而HTMLCollection对象中只包含了元素的集合。

  <form id="form1">
      用户名<input type="text" id="userName" /> <br />
      用户密码<input type="password" id="password" name="userPwd" />
    </form>
<script>
 //获取HTMLCollection集合
    var form1Children = document.getElementById("form1").children;
    console.log(form1Children);
    //获取NodeList对象
    var formNodes = document.getElementById("form1").childNodes;
    console.log(formNodes);

</script>

通过查看浏览器控制台输出的结果,可以看出HTMLCollection对象与NodeList对象的区别。

3、常见的DOM操作有哪些?

添加节点

  <form id="form1">
      用户名<input type="text" id="userName" /> <br />
      用户密码<input type="password" id="password" name="userPwd" />
    </form>
<script>
	var form1 = document.getElementById("form1");
    //创建一个input元素
    var newInput = document.createElement("input");
    //创建属性
    var newAttr = document.createAttribute("type");
    newAttr.value = "password";
    //将属性绑定到元素上
    newInput.setAttributeNode(newAttr);

    //创建一个文本节点
    var newTextNode = document.createTextNode("用户密码");
    form1.appendChild(newTextNode); //添加文本节点
    form1.appendChild(newInput);
</script>

删除节点

  <form id="form1">
      用户名<input type="text" id="userName" /> <br />
      用户密码<input type="password" id="password" name="userPwd" />
    </form>
<script>
  var form1 = document.getElementById("form1");
    var nodeChilds = form1.childNodes;
    console.log(nodeChilds);
    form1.removeChild(nodeChilds[0]);
    form1.removeChild(nodeChilds[0]);
</script>

在上面的代码中,我们想将表单中的用户名这一项内容删除掉。

首先获取form表单,然后在获取对应的子元素。

通过执行removeChild方法删除第一个元素,而第一个元素是用户名这个文本字符串,

下面还要删除文本框,所以再次调用了removeChild函数,注意由于前面已经删除了用户名这个文本元素了,所以文本框成为了第一个元素,所以这里写到索引值也是0.

删除文本框的id属性

  <form id="form1">
      用户名<input type="text" id="userName" /> <br />
      用户密码<input type="password" id="password" name="userPwd" />
    </form>
  <script>
	 var input = document.querySelector("#userName");
    input.removeAttribute("id");
</script>

修改节点

修改元素节点

修改元素的节点的操作,一般是直接用新的节点替换旧的节点。关于节点的替换可以使用,replaceChild函数来实现,该函数的调用是通过父元素来调用的,例如:把div1中的内容替换掉,这里就需要通过container.replaceChild方法来完成,replaceChild方法需要两个参数,第一个参数表示的是新元素,第二个参数表示的是旧元素。

   <div id="container">
      <div id="div1">hello</div>
    </div>
   <script>
  var container = document.getElementById("container"); //获取父元素container
      var div1 = document.getElementById("div1"); //获取子元素
      var newDiv = document.createElement("div"); // 创建一个新的div元素
      var newText = document.createTextNode("nihao"); //创建一个文本内容
      newDiv.appendChild(newText); //把创建的文本内容添加到新的div中
      container.replaceChild(newDiv, div1); //用新的div替换旧的div,完成节点的修改操作。
</script>

修改属性节点

修改属性的节点,我们可以通过setAttribute()函数来完成,如果想获取属性节点可以通过getAttribute()函数来完成。

  <div id="container">
      <div id="div1" style="color: red">hello</div>
    </div>
<script>
	   var div1 = document.getElementById("div1");
      div1.setAttribute("style", "color:blue"); //设置style属性
      console.log(div1.getAttribute("style")); // 获取style属性的值
</script>

修改属性节点除了通过setAttribute()方法完成以外,还可以通过属性名直接进行修改

 <div id="container">
      <div id="div1" style="color: red">hello</div>
    </div>
<script>
 var div1 = document.getElementById("div1");
      div1.style.color = "blue";
</script>

但是通过这种方式进行修改,还需要注意一个问题:直接修改的属性名与元素节点中的属性名不一定是一致的。例如class这个属性,在javascript中是关键字,不能直接作为属性使用,这时需要通过className来完成。

    <div id="container">
      <div id="div1" style="color: red">hello</div>
      <div id="div2" class="foo">前端学习</div>
    </div>
	<script>
  var div2 = document.getElementById("div2");
      div2.className = "bar"; //注意这里使用的是className
</script>

通过查看浏览器控制台,可以看到对应的样式发生了修改。

修改文本节点

文本节点的修改,可以通过innerHTML属性来完成。

 <div id="container">
      <div id="div1" style="color: red">hello</div>
      <div id="div2" class="foo">前端学习</div>
    </div>
<script>
	  var div2 = document.getElementById("div2");
      div2.innerHTML = "Vue 学习";
</script?>

4、DOM性能问题

Dom操作非常消耗性能,应该尽量避免频繁的操作DOM.

导致浏览器重绘,重新渲染,比较消耗cpu资源,比较消耗性能。

提升性能的方案:

第一:对DOM查询操作进行缓存

第二:将频繁操作修改为一次性操作

首先看第一种情况:

这里需要对页面中所有p标签内文字调整大小(单击按钮完成)

 //不缓存的结果
      for (let i = 0; i < document.getElementsByTagName("p").length; i++) {
        //每次循环,都会计算lenght,频繁进行DOM查询
      }
 const pList = document.getElementsByTagName("p");
      const length = pList.length;
      for (let i = 0; i < length; i++) {
        //缓存length,只进行一次DOM查询
      }

下面看一下第二种情况:

需求:页面中有一个ul列表,需要单击按钮一次性插入10个或者100个li?

传统的做法:

 <ul id="list"></ul>
const listNode = document.getElementById("list");
      for (let i = 0; i < 10; i++) {
        const li = document.createElement("li");
        li.innerHTML = `item${i}`;
        list.appendChild(li);
      }

执行上面的代码,可以实现对应的需求,但是问题是上面的操作是频繁操作dom,性能比较低。

const listNode = document.getElementById("list");
      //创建一个文档片段,文档片段存在于内存中,并不在DOM树中,所以此时还没有插入到DOM中
      //也就是先将dom插入到临时区域中
      const frag = document.createDocumentFragment();
      //执行插入

      for (let i = 0; i < 10; i++) {
        const li = document.createElement("li");
        li.innerHTML = `item${i}`;
        frag.appendChild(li);
      }
      //都完成后,再插入到DOM树中
      listNode.appendChild(frag);

5、什么是事件传播

在浏览器中,JavaScriptHTML之间的交互是通过事件实现的,常用的事件包括了鼠标点击的事件,鼠标移动事件等等。

当事件发生以后,会触发绑定在元素上的事件处理程序,执行相应的操作。

问题是当事件发生后,事件是怎样传播的呢?

事件发生后会在目标节点和根节点之间按照特定的顺序进行传播,路径经过的节点都会接收到事件。

这里的特定顺序是怎样的顺序呢?

第一种:事件传递的顺序是先触发最外层的元素,然后依次向内传播,这样的传递顺序我们称之为事件的捕获阶段

第二种:事件传递的顺序是先触发最内层的元素,然后依次向外进行传播,这样的传递顺序我们称之为事件冒泡阶段

当然,一个完整的事件传播包含了三个阶段

首先就是事件的捕获阶段

然后是事件的目标阶段,目标阶段指的就是事件已经到达目标元素。

最后是事件的冒泡阶段

以上就是关于事件传播的描述

6、什么是事件的捕获

关于事件捕获,在上一小节,我们已经介绍过:事件的传递是从最外层开始,依次向内传播,在捕获阶段,事件从window开始,一直到触发事件的元素。

window----> document----> html----> body ---->目标元素

如下代码所示:

<body>
    <table border="1">
      <tbody>
        <tr>
          <td>单元格内容</td>
        </tr>
      </tbody>
    </table>
    <script>
      var table = document.querySelector("table");
      var tbody = document.querySelector("tbody");
      var tr = document.querySelector("tr");
      var td = document.querySelector("td");
      table.addEventListener(
        "click",
        function () {
          console.log("table");
        },
        true
      );
      tbody.addEventListener(
        "click",
        function () {
          console.log("tbody");
        },
        true
      );
      tr.addEventListener(
        "click",
        function () {
          console.log("tr");
        },
        true
      );
      td.addEventListener(
        "click",
        function () {
          console.log("td");
        },
        true
      );
    </script>
  </body>

在上面的代码中,有一个表格,给表格中的每个元素通过addEventListener方法绑定了单击事件,同时该方法的第三个参数,设置为了true,这样就表明事件将在捕获阶段发生。

所以当我们单击td单元格的时候,事件的执行结果是:table,tbody,tr,td.也就是说事件从table开始,依次向下传播。这个传播的过程就是事件捕获。

7、什么是事件冒泡

关于事件的冒泡,在前面也已经提到过:事件传递的顺序是先触发最内层的元素,然后依次向外进行传播,这样的传递顺序我们称之为事件冒泡阶段。

如下代码所示:

<body>
    <table border="1">
      <tbody>
        <tr>
          <td>单元格内容</td>
        </tr>
      </tbody>
    </table>
    <script>
      var table = document.querySelector("table");
      var tbody = document.querySelector("tbody");
      var tr = document.querySelector("tr");
      var td = document.querySelector("td");
      table.addEventListener("click", function () {
        console.log("table");
      });
      tbody.addEventListener("click", function () {
        console.log("tbody");
      });
      tr.addEventListener("click", function () {
        console.log("tr");
      });
      td.addEventListener("click", function () {
        console.log("td");
      });
    </script>
  </body>

上面的代码,我们将addEventListener方法的第三个参数true去掉了,这时就有事件的捕获变成了事件的冒泡。默认值为(false).

但单击单元格的时候,执行的结果为:td,tr,tbody,table, 这个过程就是事件的冒泡。

8、阻止事件冒泡

现在,我们已经了解了事件冒泡的过程,但是在很多的情况下,我们需要阻止事件冒泡的发生。

例如:在上一小节的案例中,当我们单击了单元格后,不仅触发单元格元素的事件,同时也会触发其它元素的事件,而这里我们只希望触发单元格的事件。所以这里需要阻止事件的冒泡。

阻止事件的冒泡需要使用:event.stopPropagation()函数

如下案例:

  <script>
      var table = document.querySelector("table");
      var tbody = document.querySelector("tbody");
      var tr = document.querySelector("tr");
      var td = document.querySelector("td");
      table.addEventListener("click", function () {
        console.log("table");
      });
      tbody.addEventListener("click", function () {
        console.log("tbody");
      });
      tr.addEventListener("click", function () {
        console.log("tr");
      });
      td.addEventListener("click", function (event) {
        //阻止了事件的冒泡操作
        event.stopPropagation();
        console.log("td");
      });
    </script>

在单元格的事件处理函数中,通过event.stopPropagation()方法阻止了事件的冒泡。

stopPropagation()函数相对的还有一个stopImmediatePropagation函数,它们两者之间有什么区别呢?

stopPropagation():函数会阻止事件冒泡,其它事件处理程序仍然可以调用

stopImmediatePropagation函数不仅可以阻止事件冒泡,也会阻止其它事件处理程序的调用。

如下代码所示:

 <body>
    <table border="1">
      <tbody>
        <tr>
          <td>单元格内容</td>
        </tr>
      </tbody>
    </table>
    <script>
      var table = document.querySelector("table");
      var tbody = document.querySelector("tbody");
      var tr = document.querySelector("tr");
      var td = document.querySelector("td");
      table.addEventListener("click", function () {
        console.log("table");
      });
      tbody.addEventListener("click", function () {
        console.log("tbody");
      });
      tr.addEventListener("click", function () {
        console.log("tr");
      });
        //单元格第一个单击事件
      td.addEventListener("click", function (event) {
        console.log("td1");
      });
        //单元格第二个单击事件
      td.addEventListener("click", function (event) {
        //阻止了事件的冒泡操作
       // event.stopImmediatePropagation();
         event.stopPropagation();
        console.log("td2");
      });
     //   单元格第三个单击事件
      td.addEventListener("click", function (event) {
        console.log("td3");
      });
    </script>
  </body>

在上面的代码中,给单元格添加了三个单击的事件,同时第二个单击事件使用了stopPropagation方法来阻止冒泡行为。

执行结果如下:td1,td2,td3

通过执行结果,可以看到单元格的三个单击事件全部触发,并且阻止了冒泡的行为。

如果使用stopImmediatePropagation方法,执行结果为:td1,td2

通过执行的结果可以看到,阻止了冒泡的行为,但是没有触发单元格的第三个单击的事件,也就是说会阻止其它事件的执行。

9、事件冒泡与事件捕获问题

下面我们来看一段代码:

<body>
    <table border="1">
      <tbody>
        <tr>
          <td>单元格内容</td>
        </tr>
      </tbody>
    </table>
    <script>
      var table = document.querySelector("table");
      var tbody = document.querySelector("tbody");
      var tr = document.querySelector("tr");
      var td = document.querySelector("td");
      //事件捕获
      table.addEventListener(
        "click",
        function () {
          console.log("table");
        },
        true
      );
      //事件冒泡
      tbody.addEventListener("click", function () {
        console.log("tbody");
      });
      //事件捕获
      tr.addEventListener(
        "click",
        function () {
          console.log("tr");
        },
        true
      );
      //事件冒泡
      td.addEventListener("click", function (event) {
        console.log("td");
      });
   
    </script>
  </body>

在上面的代码中,既有事件捕获又有事件冒泡,那么执行的结果是怎样的呢?

当单击td单元格后

执行结果:table,tr,td,tbody

分析:事件传播的循序是:先事件捕获阶段,然后事件的目标阶段,最后是事件冒泡阶段

所以说,在一个程序中有事件的捕获阶段,又有事件的冒泡阶段,会优先执行捕获阶段的事件。

所以上面代码整个执行的流程:

先执行table这个捕获阶段,输出table这个字符串

下面执行tbody,但是tbody绑定的是冒泡类的事件,所以不执行,跳过。

下面是tr,而tr是捕获类型的事件,所以会执行,输出字符串tr

下面是td,由于我们单击的是td元素,所以该元素就是事件目标元素,则会执行,输出td字符串。

当单击了td元素以后,就开始进入了事件冒泡阶段。这时会冒泡到tr元素,但是tr元素绑定的是捕获阶段的事件,所以不执行,直接跳过,下面继续冒泡到了tbody元素,该元素绑定的是冒泡类型的事件,所以执行,输出字符串tbody.

下面继续冒泡,执行到table元素,该原生是捕获类型的事件,所以直接跳过,没有输出。

10、Event对象使用

JavaScrip中,每触发一个事件,就会产生一个Event对象,在该对象中包含了所有与事件相关的内容,包括事件的元素,事件类型等。

当给某个元素绑定了事件处理程序后,就可以获取到Event对象,但是在不同的浏览器下,Event对象的实现还是有一定的差异的。

关于获取Event对象的方式有两种:

第一种:在事件的处理程序中,可以通过参数来获取Event对象。

第二种:在事件的处理程序中,可以通过window.event属性获取Event对象。

具体的示例代码如下:

 <body>
    <button id="btn">单击</button>

    <script>
      var btn = document.getElementById("btn");
      btn.addEventListener("click", function (event) {
        //通过参数获取Event对象
        console.log("event=", event);
        //通过window.event的方式来获取Event对象
        var windEvent = window.event;
        console.log("windEvent=", windEvent);
        //判断两种方式是否相等
        console.log(event === windEvent);
      });
    </script>
  </body>

在谷歌浏览器中,测试上面的代码,可以发现两种获取Event对象的方式是相等的。

但是注意,在其它的浏览中进行测试可能会出现不相等的情况,也就是有的浏览器会出现不支持window.event这种方式来获取Event对象,这里可以自行进行测试。

为了能够在获取事件对象的时候,支持不同的浏览器,我们可以做兼容性的处理。

 <body>
    <button id="btn">单击</button>

    <script>
      var eventFn = {
        event: function (e) {
          return e || window.event;
        },
      };

      var btn = document.getElementById("btn");
      btn.addEventListener("click", function (event) {
        //通过参数获取Event对象
        console.log("event=", eventFn.event(event));
        //通过window.event的方式来获取Event对象
        var windEvent = eventFn.event(window.event);
        console.log("windEvent=", windEvent);
        //判断两种方式是否相等
        console.log(event === windEvent);
      });
    </script>

在上面的代码中定义了eventFn对象,在该对象中有一个属性event,在该属性中,判断返回Eevent对象的方式。

在对应的事件处理函数中,可以调用eventFn对象中的event方法来获取Event对象。

获取事件的目标元素

在事件的处理程序中,我们可能需要获取 事件的目标元素。

IE浏览器中,可以使用event对象中的srcElement属性来获取事件的目标元素,在非IE浏览器中可以通过event对象的target属性来获取事件的目标元素,当然在有的非IE浏览器下也支持event对象中的srcElement属性,目的是为了保持与ie保持一致,但是要注意的是并不是所有的非IE浏览器都支持srcElement属性。

<script>
     var eventFn = {
        event: function (e) {
          return e || window.event;
        },
      };

      var btn = document.getElementById("btn");
      btn.addEventListener("click", function (event) {
        var event = eventFn.event(event);
        console.log("target=", event.target);
        console.log("srcElement=", event.srcElement);
      });
 </script>   

在谷歌浏览器中进行测试,都可以获取target属性和srcElement属性的值。

关于其它浏览器的情况,可以自行测试。

当然为了能够兼容其它的浏览器,可以做一下兼容的处理。

   var eventFn = {
        event: function (e) {
          return e || window.event;
        },
        target: function (e) {
          return e.target || e.srcElement;
        },
      };

      var btn = document.getElementById("btn");
      btn.addEventListener("click", function (event) {
        var event = eventFn.event(event);
        console.log("target=", eventFn.target(event));
        console.log("srcElement=", eventFn.target(event));
      });

这里在eventFn对象中封装了一个target属性。

阻止默认行为

 <a href="https://www.baidu.com" id="a1">链接</a>
  <script>
      var a1 = document.getElementById("a1");
      a1.addEventListener("click", function (event) {
        event.preventDefault(); //阻止默认行为
        alert("你点击了链接");
      });
</script>  

关于Event对象中的其他内容,可以参考文档。

11、介绍一下三种事件模型

关于JavaScript的事件模型有三类,分别是DOM0DOM2,DOM3

DOM0事件模型

DOM0的事件模型指的是:将一个函数赋值给一个事件处理属性。

如下代码:

var btn=document.getElementById('btn')
btn.onclick=function(){}

或者也可以采用如下的方式:

<button onclick="fn()">
    单击
    </button>
<script>
	function fn(){
        console.log('hello')
    }
</script>

需要注意的是:DOM0事件模型的处理程序只支持冒泡阶段。

DOM0事件模型的优点与缺点:

优点:实现起来非常简单,并且可以跨浏览器。

缺点:一个事件处理程序只能绑定一个函数。

<body>
    <button id="btn" onclick="btnClick()">单击按钮</button>
    <script>
      var btn = document.getElementById("btn");
      btn.onclick = function () {
        console.log("hello");
      };
      function btnClick() {
        console.log("123");
      }
    </script>
  </body>

在上面的代码中,我们给按钮使用两种方法绑定了事件处理程序,但是DOM0这种事件模型只能绑定一个函数,并且在JavaScript中绑定事件处理程序的优先级高于在HTML元素中定义的事件处理程序,所以打印结果为hello.

如果删除元素绑定的事件,只需要将对应的事件处理程序设置为null即可

btn.onclick=null

DOM2事件模型

针对DOM2事件模型不同的浏览器厂商制定了不同的的实现方式,主要分为IE浏览器和非IE浏览器

IE10及以下版本中只支持事件的冒泡,在IE11中同时支持事件的捕获与事件冒泡。在IE10及以下版本中,可以通过attachEvent函数来添加事件处理程序,通过detachEvent函数删除事件处理程序。

element.attachEvent('on'+ eventName,handler) // 添加事件处理程序
element.detachEvent('on'+ eventName,handler) //  删除事件处理程序

IE11和非IE浏览器中,同时支持事件捕获和事件冒泡两个阶段,可以通过addEventListener()函数添加事件处理程序,可以通过removeEventListener() 函数删除事件处理程序。

addEventListener(eventName,handler,useCapture) //添加事件处理程序
removeEventListener(eventName,handler,useCapture) // 删除事件处理程序

其中useCapture如果为true表示支持事件捕获,为falseb表示支持事件冒泡,默认是为false

事件捕获与事件冒泡.png

通过上面的介绍,我们知道了DOM2的事件处理程序存在两种情况,那这两种实现的方式之间有没有相同点和不同点呢?

相同点

第一:在DOM2的事件处理中不管是IE浏览器还是非IE浏览器都支持对同一个事件绑定多个处理函数。

 <body>
    <button id="btn">单击按钮</button>
    <script>
      var btn = document.getElementById("btn");
      btn.addEventListener("click", function () {
        console.log("hello");
      });
      btn.addEventListener("click", function () {
        console.log("nihao");
      });

    </script>
  </body>

以上程序的输出结果为:hello,nihao

第二:在需要删除绑定的事件的时候,,不能删除匿名函数,因为添加的函数和删除的函数必须是同一个函数。

下面的代码中,同时绑定和删除了handler函数,这样做是完全可以的。

var btn=document.getElementById('btn')
var handle=function(){
    console.log('hello');
}
btn.addEventListener('click',handle,false);
btn.removeEventListener('click',handle)

但是如果采用如下的删除方式是无法取消绑定的事件的。因为它们都是匿名函数,而并不是同一个函数。

btn.addEventListener('click',function(){
    console.log('hello')
},false)
btn.removeEventListener('click',function(){})

不同点

第一:在IE浏览器中,使用attachEvent函数为同一个事件添加多个处理程序时,会按照添加的相反顺序执行。

<script>
     var btn = document.getElementById("btn");
      btn.attachEvent("onclick", function () {
        console.log("hello");
      });
      btn.attachEvent("onclick", function () {
        console.log("nihao");
      });
 </script>   

当单击按钮的时候,先输出nihao,再输出hello.

第二:在IE浏览中,attachEvent函数添加的事件处理程序会在全局作用域中运行,因此this指向的是window.

在非IE浏览器中,addEventListener()函数添加的处理程序在指定的元素内部执行,this指向所绑定的元素。

既然DOM2事件的处理有浏览器的兼容性问题,那应该怎样进行处理呢?

 var EventHandler = {
        addEventHandler: function (ele, type, handler) {
          if (ele.addEventListener) {
            ele.addEventListener(type, handler);
          } else if (ele.attachEvent) {
            ele.attachEvent("on" + type, handler);
          } else {
            ele["on" + type] = handler;
          }
        },
        removeEventHandler: function (ele, type, handler) {
          if (ele.addEventListener) {
            ele.removeEventHandler(type, handler);
          } else if (ele.detachEvent) {
            ele.detachEvent("on" + type, handler);
          } else {
            ele["on" + type] = null;
          }
        },
      };

DOM3事件模型

DOM3事件模型中允许自定义事件,自定义事件有createEvent("CustomEvent")函数来完成。返回的对象有一个initCustomEvent()方法接收如下四个参数。

1)type:字符串,触发的事件类型,自定义。例如 “keyDown”,“selectedChange”;
  2)bubble(布尔值):标示事件是否应该冒泡;
  3cancelable(布尔值):标示事件是否可以取消;
  4)detail(对象):任意值,保存在event对象的detail属性中;

具体的示例代码如下

 <body>
    <div id="div1">监听自定义事件</div>
    <button id="btn">单击</button>
    <script>
      var customeEvent;
      //在立即执行函数中创建自定义事件
      (function () {
        //判断浏览器是否支持DOM3事件处理程序,如果条件成立表示支持,固定写法
        if (document.implementation.hasFeature("CustomEvents", "3.0")) {
          var user = { userName: "zhangsan" };
          customeEvent = document.createEvent("CustomEvent"); //创建自定义事件
          customeEvent.initCustomEvent("myEvent", true, false, user);
        }
      })();

      //监听自定义事件
      //通过addEventListener()函数监听自定义的事件`myEvent`
      var div1 = document.getElementById("div1");
      div1.addEventListener("myEvent", function (e) {
        console.log("用户名是:", e.detail.userName);
      });
      //触发自定义事件
      var btn = document.getElementById("btn");
      btn.addEventListener("click", function () {
        div1.dispatchEvent(customeEvent);
      });
    </script>
  </body>

以上就是DOM3事件模型的案例。

12、介绍一下事件委托

事件冒泡的一个应用就是事件代理,也叫做事件委托

事件委托:利用事件冒泡的特性,将本应该注册在子元素上的处理事件注册在父元素上。

例如:

  <div id="div1">
      <a href="#">a1</a>
      <a href="#">a2</a>
      <a href="#">a3</a>
      <button>增加按钮</button>
    </div>

在上面的代码中有很多的a标签,如果给每个a标签添加单击事件比较麻烦,同时采用这种方式添加事件还会导致占用内存比较多,你可以想象一下,如果a标签比较多的话,是不是会占用更多的内存。

那应该怎样解决这个问题呢?

可以通过事件委托的机制。也就是将事件绑定到父元素上,然后通过事件冒泡的原理,来解决这个问题。

如下代码所示:

 <script>
      const div1 = document.getElementById("div1");
      div1.addEventListener("click", function (e) {
        var target = e.target;
        if (target.nodeName.toLowerCase() === "a") {
          console.log(target.innerHTML);
        }
      });
    </script>

在上面的代码中,我们通过事件委托的方式解决了对应的问题,当然,这里你可能问,在div标签下的a标签所做的操作都是一样的,那么能不能针对不同的元素所做的操作如果不一样,事件委托能否处理呢?

答案是可以处理的。

下面我们把上面的程序改造一下:

<body>
    <div id="div1">
      <a href="#" id="a1">a1</a>
      <a href="#" id="a2">a2</a>
      <a href="#" id="a3">a3</a>
      <button>增加按钮</button>
    </div>
    <script>
      const div1 = document.getElementById("div1");
      div1.addEventListener("click", function (e) {
        var target = e.target;
        switch (target.id) {
          case "a1":
            console.log("针对a1进行操作");
            break;
          case "a2":
            console.log("针对a2进行操作");
            break;
          case "a3":
            console.log("针对a3进行操作");
            break;
        }
      });
    </script>
  </body>

在上面的代码中,给每个a标签,添加了id属性,通过switch结构进行判断,然后执行不同的操作。

下面,再来看一个关于事件委托的应用问题:

在一个ul标签中,包含了5li,需要单击每个li标签,输出标签中的内容。同时在页面中添加一个按钮,单击按钮创建一个新的li元素,单击新创建的li元素也可以输出对应的内容。

实现方式如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <ul>
      <li>a1</li>
      <li>a2</li>
      <li>a3</li>
      <li>a4</li>
      <li>a5</li>
    </ul>
    <button id="btn">添加</button>
    <script>
        //通过querySelectorAll方法获取所有的li元素
        //通过for循环遍历的方式,给每个li添加单击事件
      var children = document.querySelectorAll("li");
      for (var i = 0; i < children.length; i++) {
        children[i].addEventListener("click", function () {
          console.log(this.innerHTML);
        });
      }
        //获取按钮
      var btn = document.querySelector("#btn");
        //获取ul标签
      var ul = document.querySelector("ul");
        //给按钮添加单击事件
      btn.addEventListener("click", function () {
        //创建li
        var newLi = document.createElement("li");
          //创建文本节点
        var newText = document.createTextNode("a6");
          //将文本添加到新创建的li标签上
        newLi.appendChild(newText);
        //把创建的li添加到ul上
        ul.appendChild(newLi);
      });
    </script>
  </body>
</html>

执行上面的代码:点击a1--a5都可以获取内容,单击按钮也可以添加一个新的li元素,但是问题是单击新创建的li元素,并没有输出元素中的内容。

原因是:我们通过querySelectorAll方法获取所有li元素,但是通过这个方法不能实时对增加的事件进行绑定。也就是说无法完成对新元素事件的绑定。

解决办法:先创建新元素,然后在进行事件的绑定。

 <script>
      function bindEvent() {
        var children = document.querySelectorAll("li");
        for (var i = 0; i < children.length; i++) {
          children[i].addEventListener("click", function () {
            console.log(this.innerHTML);
          });
        }
      }
      var btn = document.querySelector("#btn");
      var ul = document.querySelector("ul");
      btn.addEventListener("click", function () {
        //创建li
        var newLi = document.createElement("li");
        var newText = document.createTextNode("a6");
        newLi.appendChild(newText);
        //把创建的li添加到ul上
        ul.appendChild(newLi);
        //重新添加事件处理程序
        bindEvent();
      });
    </script>

在上面的代码中,我们创建了一个bindEvent函数,把对li元素添加事件的操作封装到了该方法中。

然后在按钮对应的事件处理函数中,先完成新元素的创建,然后在调用bindEvent方法,完成对li元素事件的绑定操作。

虽然以上完成了我们的需求,但是还有一个问题需要解决,就是我们前面所讲的:由于给每个li元素都添加了单击事件,导致占用内存比较多,性能比较低,所以可以使用事件委托的方式来改造上面的程序。

改造后的程序如下所示:

<script>

      var ul = document.querySelector("ul");
      //   var parent = document.querySelector("ul");
      ul.addEventListener("click", function (e) {
        var target = e.target;
        if (target.nodeName.toLowerCase() === "li") {
          console.log(target.innerHTML);
        }
      });

      var btn = document.querySelector("#btn");

      btn.addEventListener("click", function () {
        //创建li
        var newLi = document.createElement("li");
        var newText = document.createTextNode("a6");
        newLi.appendChild(newText);
        //把创建的li添加到ul上
        ul.appendChild(newLi);
        //重新添加事件处理程序
        // bindEvent();
      });
    </script>

在上面的代码中,我们通过querySelector方法获取ul这个父元素,然后给该元素添加单击事件,在对应的事件处理程序中,判断获取到的元素是否为li,如果是打印li元素中的内容。

由于这里我们没有在使用querySelectorAll函数,所以这里我们也没有必要在封装到一个函数中了。

关于按钮的单击事件的处理,没有任何的变化。

以上就是通过事件委托的方式,给父元素添加了事件,对应的子元素都具有了相应的事件,这样的处理方式性能更高。

JavaScript中还有一些其它的比较常用的事件,例如:

焦点相关的事件:focus,blur等事件
鼠标相关的事件:mouseover,mouseout等事件
键盘相关的事件:keydown,keyup,keypress等
其它事件...

13、介绍一下浏览器的重排与重绘

在前面我们也提到过对DOM的操作是比较消耗性能的,这是因为它会带来浏览器的重绘与重排。

在了解什么是重排与重绘之前,先来说一下浏览器渲染HTML的过程。

浏览器渲染HTML的过程大体上可以分为4步

第一:HTML代码被HTML解析器解析成对应的DOM树,CSS代码被CSS解析器解析成对应的样式规则集。

第二:DOM树与CSS解析完成后,附加在一起形成一个渲染树

第三:节点信息的计算,即根据渲染树计算出每个节点的几何信息(宽,高等信息)

第四:渲染绘制,根据计算完成的节点信息绘制整个页面。

而我们所要了解的重排与重绘就发生在第三步和第四步中。

什么是重排

当对一个DOM节点信息进行修改的时候,需要对该DOM结构进行重新的计算。并且该DOM结构的修改会决定周边DOM结构的更改范围,主要分为全局范围和局部范围。

全局范围就是从页面的根节点html标签开始,对整个渲染树进行重新计算,例如:我们修改窗口的尺寸或者修改了根元素的字体大小的时,都会导致对整个渲染树进行重新计算。

局部范围只会对渲染树的某部分进行重新计算。例如要改变页面中某个div的宽度,只需要重新计算渲染树中与该div相关的内容就可以了。

而重排的过程发生在DOM节点信息修改的时候,重排实际是根据渲染树中每个渲染对象的信息,计算出各自渲染对象的几何信息,例如DOM元素的位置,尺寸,大小等。然后将其放在页面中的正确的位置上。

综上所述,我们明白了重排其实就是一种改变页面布局的操作。那么常见的引起重排的操作有哪些呢?

(1)页面首次渲染

在页面首次渲染的时候,HTML页面中的各个元素位置,尺寸,大小等信息都是未知的,需要通过与css样式规则集才能够确定出各个元素的几何信息。这个过程中会产生很多元素集合信息的计算,所以会产生重排。

(2)浏览器窗口大小的改变

页面渲染完成后,就会得到一个固定的渲染树。如果此时对浏览器窗口进行缩放或者是拉伸操作,渲染树中从根元素html标签开始的所有元素,都会重新计算其几何信息,从而产生重排的操作。

(3)元素位置改变和尺寸的改变

(4)元素内容改变,例如,文本内容被另外一个不同尺寸的图片替换。

(5)添加或者删除可见的DOM元素

(6)获取某些特定的属性

当我们对javascript某些操作属性的修改也会导致重排的操作,而频繁的重排操作会对浏览器引擎产生很大的消耗。所以浏览器不会对js的每个操作都进行一次重排,而是维护一个会引起重排操作的队列,等到队列中的操作达到了一定的数量或者是到了一定的时间间隔的时候,浏览器才会去刷新一次队列,进行真正的重排操作。

虽然浏览器有这样的优化,但是我们写的一些代码还会导致浏览器提取刷新队列,例如以下的操作。

offsetLeft,offsetWidth,offsetHeight,offsetTop
scrollTop,scrollLeft,scrollWidth,scrollHeight
clientTop,clientLeft,clientWidth,clientHeight
widht,height

当我们进行以上属性操作的时候,浏览器为了返回最精确的信息,需要刷新队列,因为队列中的某些操作会影响到这些属性值的获取。

以上就是浏览器重排的介绍

浏览器重绘

浏览器的重绘指的就是改变元素在页面中的展示样式,而不会引起元素在文档中位置的改变。例如:改变元素的颜色,背景色,透明度等。

常见的引起重绘的操作如下:

color:颜色
border-style:边框样式
visibility: 元素是否可见
background:背景样式,包括背景颜色,背景图片等
text-decoration:文本下划线,上划线等
border-radius:边框圆角
box-shadow:元素的阴影

以上就是浏览器的重绘的介绍。

通过对浏览器重排与重绘的介绍,相信大家已经有所了解了,那么它们两者之间有什么关系呢?

简单的说,重排一定会引起重绘,而重绘却不一定会引起重排的操作

因为当元素在重排的过程中,元素的位置等几何信息会重新计算,并会引起元素的重新渲染,这样就会产生重绘的操作,而在重绘的时候,只是改变了元素的展示的样式,而不会引起元素在文档中位置的改变,所以一般不会引起重排的操作。

性能优化

浏览器的重排与重绘是比较消耗性能的,所以我们应该尽量减少重排与重绘的操作,这也是优化网页性能的一种方式。

常见的方法如下:

第一:将样式属性值的修改合并为一次。

例如,我们需要修改一个元素的样式,可以通过如下的代码实现:

  var mainDiv = document.getElementById("div1");
      mainDiv.style.width = "200px";
      mainDiv.style.height = "100px";
      mainDiv.style.background = "#ccc";

但是问题是,在上面的操作中多次修改了style属性,会引发多次的重排与重绘操作。

所以为了解决这个问题,可以将这些样式合并在一个class类中。

  <style>
      .changeStyle {
        width: 200px;
        height: 100px;
        background: "#ccc";
      }
    </style>

然后通过通过javascript直接修改元素的样式

  document.getElementById("div1").className = "changeStyle";

这样我们可以在最后一步完成样式的修改,从而只引起一次的重排与重绘的操作。

第二:

将需要多次重排的元素,position属性设为absolutefixed,这样此元素就脱离了文档流,它的变化不会影响到其他元素。例如有动画效果的元素就最好设置为绝对定位。

第三:在对多节点操作的时候,可以现在内测中完成,然后在添加到文档中。

如下代码所示:

  function createTable(list) {
        var table = $("#table");
        var rowHtml = "";
        list.forEach(function (item) {
          rowHtml += "<tr>";
          rowHtml += "<td>" + item.userName + "</td>";
          rowHtml += "<td>" + item.userPwd + "</td>";
          rowHtml += "</tr>";
          table.append(rowHtml);
          rowHtml = "";
        });
      }

当调用createTable方法的时候,会从list集合中取出一条数据,然后放在tr标签中,紧跟着添加到表格中,这样就会导致每添加一行数据,都会引发一次浏览器的重排和重绘的操作,如果数据很多,则会对渲染造成很大的影响。

修改后的代码

function createTabel(list) {
        var table = $("#table");
        var rowHtml = "";
        list.forEach(function (item) {
          rowHtml += "<tr>";
          rowHtml += "<td>" + item.userName + "</td>";
          rowHtml += "<td>" + item.userPwd + "</td>";
          rowHtml += "</tr>";
        });
    //将数据一次性追加到表格中,完成一次渲染
        table.append(rowHtml);
      }

通过上面的改造以后,只会引起一次浏览器的重绘与重排的操作,从而带来很大的新能提升。

第四:由于display属性为none的元素不在渲染树中,对隐藏的元素操作不会引发其他元素的重排。如果要对一个元素进行复杂的操作时,可以先隐藏它,操作完成后再显示。这样只在隐藏和显示时触发两次重排。

第五: 尽量减少table布局,随便修改一个单元格的高度或宽度都会让整个表格进行重排,性能非常差。

第六:在对多个同级元素做事件绑定的时候,推荐使用事件委托机制来处理。

第七:文档片段DocumentFragment的使用,关于这块内容,在前面已经使用过。

使用批量插入元素,例如:向页面的ul元素中添加100li元素,

const listNode = document.getElementById("list");
      //创建一个文档片段,文档片段存在于内存中,并不在DOM树中,所以此时还没有插入到DOM中
      //也就是先将dom插入到临时区域中
      const frag = document.createDocumentFragment();
      //执行插入

      for (let i = 0; i < 10; i++) {
        const li = document.createElement("li");
        li.innerHTML = `item${i}`;
        frag.appendChild(li);
      }
      //都完成后,再插入到DOM树中
      listNode.appendChild(frag);

以上就是关于浏览器重绘与重排的内容。

六、AJAX

1、什么是AJAX

Ajax是一种异步请求数据的web开发技术,对于改善用户的体验和页面性能很有帮助。简单地说,在不需要重新刷新页面的情况下,Ajax 通过异步请求加载后台数据,并在网页上呈现出来。

常见运用场景有表单验证是否登入成功、百度搜索下拉框提示和快递单号查询等等。

Ajax的目的是提高用户体验,较少网络数据的传输量。同时,由于AJAX请求获取的是数据而不是html文档,因此它也节省了网络带宽,让互联网用户的网络冲浪体验变得更加顺畅。

关于提高用户的体验,可以通过下面来进行体会

下图是普通的请求方式

ajax1.png

下图是ajax请求的方式

ajax2.png

2、AJAX原理是什么

Ajax相当于在用户和服务器之间加了一个中间层,使用户操作与服务器响应异步化。并不是所有的用户请求都提交给服务器,像一些数据验证和数据处理等都交给Ajax引擎自己来做,只有确定需要从服务器读取新数据时再由Ajax引擎代为向服务器提交请求。

Ajax的原理简单来说通过XmlHttpRequest对象来向服务器发送异步请求,从服务器获得数据,然后用JavaScript来操作DOM而更新页面。

XMLHttpRequestajax的核心机制,它是在IE5中首先引入的,是一种支持异步请求的技术。简单的说,也就是JavaScript可以及时向服务器提出请求和处理响应,而不阻塞用户。达到无刷新的效果。

3、AJAX基本的使用

这里主要掌握的是能够手动创建AJAX.

创建步骤:

AJAX创建过程.png 创建xhr对象

let xhr = null;
if (window.XMLHttpRequest) {// 兼容 IE7+, Firefox, Chrome, Opera, Safari  
    xhr = new XMLHttpRequest();  
} else {// 兼容 IE6, IE5 
    xhr = new ActiveXObject("Microsoft.XMLHTTP");  
} 

配置请求地址与发送请求

xhr.open(method, url, async);  
xhr.send(string);//`POST`请求时才使用字符串参数,否则不用带参数。
// method:请求的类型;GET 或 POST
// url:文件在服务器上的位置
// async:true(异步)或 false(同步)

注意:POST请求一定要设置请求头的格式内容

xhr.open("POST", "test.html", true);  
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");  
xhr.send("fname=Henry&lname=Ford");  //`POST`请求参数放在send里面,即请求体

处理响应

xhr.onreadystatechange = function() { 
    if (xhr.readyState == 4 && xhr.status == 200){    
    document.GetElementById("mydiv").innerHTML = xhr.responseText;  
    }
} 
什么是readyState?
readyState是XMLHttpRequest对象的一个属性,用来标识当前XMLHttpRequest对象处于什么状态。
readyState总共有5个状态值,分别为0~4,每个值代表了不同的含义:

0:未初始化 — 尚未调用.open()方法;
1:启动 — 已经调用.open()方法,但尚未调用.send()方法;
2:发送 — 已经调用.send()方法,但尚未接收到响应;
3:接收 — 已经接收到部分响应数据;
4:完成 — 已经接收到全部响应数据,而且已经可以在客户端使用了;
什么是status?
HTTP状态码(status)由三个十进制数字组成。HTTP状态码共分为5种类型:

1xx(临时响应):表示临时响应并需要请求者继续执行操作的状态码。
2xx(成功):表示成功处理了请求的状态码。
3xx(重定向):表示要完成请求,需要进一步操作。通常,这些状态代码用来重定向。
4xx(请求错误):这些状态码表示请求可能出错,妨碍了服务器的处理。
5xx(服务器错误):这些状态码表示服务器在尝试处理请求时发生内部错误。这些错误可能是服务器本身的错误,而不是请求出错。

4、AJAX优缺点分析

优点

(1)无刷新更新数据

AJAX最大的优点是在不需要刷新浏览器的情况下,能够与服务器保持通信,并且能够减少不必要的数据数据传输,降低网络数据流量,这样可以加快响应用户的请求,避免不必要的等待时间,提高用户的体验。

(2)前后端分离

前端人员只关注前端页面逻辑的开发,通过ajax获取后端数据然后进行展示处理,而后端专注于请求的获取,响应的处理,职责明确。

缺点:

(1) 破坏浏览器的后退功能

浏览器有一个比较重要的功能就是历史记录的功能,通过后退按钮可以后退到浏览器之前访问的页面,但是使用了ajax后无法后退,也就是破坏了浏览器的后退机制。

(2)不利于搜索引擎的优化

百度,谷歌在进行搜索引擎优化的时候(SEO),会获取页面中的内容,而通过ajax请求返回的数据是通过javascript动态添加到页面上的,而百度等搜索引擎无法抓取到这些通过javascript动态生成的内容,所以不利于SEO

(3) 破坏了URL统一资源定位的功能。

由于AJAX的请求并不会修改浏览器中地址栏的URL,因此对于相同的URL,不同的用户看到的内容是不一样的,例如,你访问某个电商网站,在该电商网站中搜索到一件非常好的商品,你现在把地址发给你的同学,而你的同学打开这个地址后,却看不到这件商品。

所以网站的搜索的实现,一般不是通过ajax来发送请求。

5、Get和Post请求数据的区别

(1)参数传递

get请求会将参数添加到URL地址的后面,在调用ajaxsend方法的时候,传递的参数是null,即xhr.send();

post请求的数据会放在请求体中,用户是无法通过URL地址直接看到的,调用send方法的时候,需要指定要发送到服务端的数据,即xhr.send(data)

(2)服务端的处理

针对get请求与post请求,在服务端的处理也是不一样的。如果以Express来作为服务端,get的请求需要通过Request.query来获取参数,而post请求的处理,需要通过Request.body来获取数据。

(3)传递的数据量

get请求的数据量小,对于不同的浏览器是有差异 ,谷歌浏览器限制8k.post请求传递的数据量比较大,一般默认不受限制。但是服务器一般会做限制。

(4)安全性

get请求的安全性比较低,因为请求的数据会出现在url上,通过浏览器的缓存或者是历史记录很容易获取到请求的数据。post请求是将数据放在请求体中进行传递,数据不会出现在URL,安全性比较高。

6、Get和Post请求的应用场景

在了解了get方式和post请求方式的区别以后,下面看一下它们的应用场景。

get的应用场景

(1)数据的搜索,单击搜索按钮,搜索网站中指定的数据。

(2) 传递的数据量小,适合用于url方式进行传递

(3) 数据安全性要求不高的情况

post请求的应用场景

(1) 传递数据量比较大的情况,例如上传文件

(2) 表单提交,例如用户登录,注册,要求数据安全性比较高的情况。

(3) 请求会修改数据库中数据的情况,例如,添加数据,修改数据等。

7、浏览器同源策略

浏览器同源策略是浏览器最基本也是最核心的安全功能,它规定客户端脚本在没有明确授权的情况下,不能读写不同源的目标资源。

所谓的同源指的是相同协议,域名和端口号,如果两个资源路径在协议,域名,端口号上有任何一点不同,则它们就不属于同源的资源,

另外在同源策略上,又分为两种表现形式:

第一:禁止对不同页面进行DOM操作

第二:禁止使用XMLHttpRequest向不是同源的服务器发送ajax请求。

8、为什么浏览器会有跨域限制的问题?

什么是跨域呢?

访问同源的资源是被浏览器允许的,但是如果访问不同源的资源,浏览器默认是不允许的。访问不同源的资源那就是我们所说的跨域。

如下表格所示:

跨域.png

从表中可以看出域名,子域名,端口号,协议不同都属于不同源,当脚本被认为是来自不同源时,均被浏览器拒绝请求。

浏览器对跨域访问的限制,可以在很大的程度上保护用户数据的安全

第一:假如没有Dom同源策略的限制,就有可能会出现如下的安全隐患

黑客做了一个假的的网站,通过iframe嵌套了一个银行的网站,然后把iframe的高度宽度调整到占据浏览器的可视区域 ,这样用户进入这个假的网站后,看到就是和真正的银行网站是一样的内容。如果用户输入了用户名和密码,这个假的网站就可以跨域访问到所嵌套的银行网站的DOM节点,从而黑客就可以获取到用户输入的用户名和密码了。

第二:如果浏览器没有XMLHttpRequest同源策略限制,黑客可以进行跨站请求伪造(CSRF)攻击,具体方式如下:

(1)用户登录了个人银行页面A,页面A会在Cookie中保存用户信息

(2)后来用户又访问了一个恶意的页面B,在该页面中执行了恶意Ajax请求的代码

(3)这时页面B会向页面A发送Ajax请求,该请求会默认发送用户Cookie信息。

(4)页面A会从请求的Cookie中获取用户信息,验证无误后,就会返回用户的一系列相关的数据,而这些数据就会被恶意的页面B所获取,从而造成用户数据的泄漏。

正是存在这些危险的场景存在,所以同源策略的限制就显得非常总要。

9、跨域问题演示

创建一个文件夹,在该文件夹中创建index.html文件,该文件中的代码如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script>
      window.onload = function () {
        var btn = document.getElementById("btnLogin");
        btn.addEventListener("click", function () {
          sendRequest();
        });
      };
      function sendRequest() {
        var userName = document.getElementById("userName").value;
        //这里为了简单,暂时不考虑浏览器兼容性问题
        var xhr = new XMLHttpRequest();
        let url = "http://localhost:3000/getUserNameInfo?name=" + userName;
        xhr.open("get", url, true);
        xhr.send();
        xhr.onreadystatechange = function () {
          if (xhr.readyState === 4 && xhr.status === 200) {
            console.log(xhr.responseText);
          }
        };
      }
    </script>
  </head>
  <body>
    用户名:<input type="text" id="userName" /> <br />
    <button id="btnLogin">登录</button>
  </body>
</html>

在该文件夹下面安装express

npm install express

同时创建server.js文件,该文件的代码如下:

var express = require('express')
var app = express();
app.get('/getUserNameInfo', function (req, res) {
    var userName = req.query.name;
    var result = {
        id: 10001,
        userName: userName,
        userAge:21
    };
    var data = JSON.stringify(result);
    res.writeHead(200, { 'Content-type': 'application/json' })
    res.write(data);
    res.end()
})
app.listen(3000, function () {
    console.log('服务端启动....')
})

下面启动服务端

同时index.html文件也通过vscode自带的服务器进行访问。

这时会出现如下错误:

Access to XMLHttpRequest at 'http://localhost:3000/getUserNameInfo?name=admin' from origin 'http://127.0.0.1:5500' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

通过以上错误可以发现,现在的程序出现 跨域的问题,

下面看一下具体的解决方案

10、CORS

通过上面的错误,我们明白了,客户端不能发送跨域请求是因为服务端并不接收跨域的请求,所以为了解决跨域请求的问题,我们可以将服务端设置为可以接收跨域请求。

这里我们需要使用CORS('跨域资源共享'),来解决跨域请求的问题。CORS主要的实现方式是服务端通过对响应头的设置,接收跨域请求的处理。

服务端修改后的代码如下:

var express = require('express')
var app = express();
app.all('*', function (req, res) {
    //设置可以接收请求的域名
    res.header('Access-Control-Allow-Origin', 'http://127.0.0.1:5500');
    res.header('Access-Control-Allow-Methods', 'GET, POST,PUT');
    res.header('Access-Control-Allow-Headers', 'Content-Type');
    res.header('Content-Type', 'application/json;charset=utf-8');
    req.next();
})
app.get('/getUserNameInfo', function (req, res) {
    var userName = req.query.name;
    console.log('userName=',userName)
    var result = {
        id: 10001,
        userName: userName,
        userAge:21
    };
    var data = JSON.stringify(result);
    res.writeHead(200, { 'Content-type': 'application/json' })
    res.write(data);
    res.end()
})
app.listen(3000, function () {
    console.log('服务端启动....')
})

在原有的代码中,我们主要是添加了如下的代码:

app.all('*', function (req, res) {
    //设置可以接收请求的域名
    res.header('Access-Control-Allow-Origin', 'http://127.0.0.1:5500');
    res.header('Access-Control-Allow-Methods', 'GET, POST,PUT');
    res.header('Access-Control-Allow-Headers', 'Content-Type');
    res.header('Content-Type', 'application/json;charset=utf-8');
    req.next();
})

在上面的代码中,最主要的是 res.header('Access-Control-Allow-Origin', 'http://127.0.0.1:5500');这行代码,

这行代码是必须的,表示服务器可以接收哪个域发送的请求,可以用通配符*,表示接收全部的域,但是为了安全,我们最好设置特定的域。我们这里测试的是http://127.0.0.1:5500(注意:如果客户端地址是127.0.0.1,这里不能写成localhost,同时还需要注意,这里地址最后没有/)

后面请求头信息可以根据情况进行选择设置,例如接收请求的方法,数据传输的格式等。

通过对服务端的处理不会对前端代码做任何的处理,但是由于不同系统服务端采用的语言与框架是不同的,所以导致服务端的处理方式不同。

11、JSONP

JSONP是客户端与服务端进行跨域通信比较常用的解决办法,它的特点是简单,兼容老式浏览器,并且对服务器影响小。

JSONP的实现的实现思想可以分为两步:

第一:在网页中动态添加一个script标签,通过script标签向服务器发送请求,在请求中会携带一个请求的callback回调函数名。

第二: 服务器在接收到请求后,会进行相应处理,然后将参数放在callback回调函数中对应的位置,并将callback回调函数通过json格式进行返回。

前端代码:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script>
      window.onload = function () {
        var btn = document.getElementById("btnLogin");
        btn.addEventListener("click", function () {
          sendRequest();
        });
      };
      function sendRequest() {
        var userName = document.getElementById("userName").value;
        //请求参数,其中包含回调函数
        var param = "name=" + userName + "&callback=successFn";
        //请求的url
        var url = "http://localhost:3000/getUserNameInfo?" + param;
        var script = document.createElement("script");
        script.src = url;
        document.body.appendChild(script);
      }
      function successFn(result) {
        console.log("result=", result);
      }
      //   function sendRequest() {
      //     var userName = document.getElementById("userName").value;
      //     //这里为了简单,暂时不考虑浏览器兼容性问题
      //     var xhr = new XMLHttpRequest();
      //     let url = "http://localhost:3000/getUserNameInfo?name=" + userName;
      //     xhr.open("get", url, true);
      //     xhr.send();
      //     xhr.onreadystatechange = function () {
      //       if (xhr.readyState === 4 && xhr.status === 200) {
      //         console.log(xhr.responseText);
      //       }
      //     };
      //   }
    </script>
  </head>
  <body>
    用户名:<input type="text" id="userName" /> <br />
    <button id="btnLogin">登录</button>
  </body>
</html>

在上面的代码中,我们重新改造了sendRequest方法,在该方法中构建了param参数,该参数的内容包括了用户输入的用户名以及回调函数名。下面构建好所要请求的服务端的url地址,将该url地址交给script标签的src属性,通过该属性向服务器发送请求。

同时定义回调函数successFn,接收服务端返回的数据。可以对服务端返回的数据做进一步的处理。

这里需要注意的一点就是:回调函数必须设置为全局的函数。因为服务端返回响应后,会在全局环境下查找回调函数。

下面看一下服务端的处理:

var express = require('express')
var app = express();
// app.all('*', function (req, res) {
//     //设置可以接收请求的域名
//     res.header('Access-Control-Allow-Origin', 'http://127.0.0.1:5500');
//     res.header('Access-Control-Allow-Methods', 'GET, POST,PUT');
//     res.header('Access-Control-Allow-Headers', 'Content-Type');
//     res.header('Content-Type', 'application/json;charset=utf-8');
//     req.next();
// })
app.get('/getUserNameInfo', function (req, res) {
    var userName = req.query.name;
    //获取请求的回调函数
    var callbackFn = req.query.callback
    console.log('callbackFn==',callbackFn)
    console.log('userName=',userName)
    var result = {
        id: 10001,
        userName: userName,
        userAge:21
    };
    var data = JSON.stringify(result);
    res.writeHead(200, { 'Content-type': 'application/json' })
    //返回值是对对回调函数的调用
    res.write(callbackFn+'('+data+')')
    // res.write(data);
    res.end()
})
app.listen(3000, function () {
    console.log('服务端启动....')
})

在服务的代码中,需要接收回调函数的名称。

同时返回的内容中,包含了回调函数的名称,以及传递给该回调函数的具体数据。

这样当回调函数返回给浏览器后,浏览器可以从全局的环境中查找该回调函数,并进行执行。

使用JSONP的优点与缺点:

优点:

简单,不存在浏览器兼容性的问题

缺点:

只能实现get请求,如果是post请求则无法进行跨域的处理。