面试题 - Javascript

201 阅读22分钟

最近找工作整理了一些面试题,分享给大家一起来学习。如有问题,欢迎指正。

前端面试题系列文章:

javascript数据类型

javascript数据类型有哪些?

  • 原始类型:undefind、null、string、number、boolean、symbol、bigInt
  • 引用类型:object(Array、Function、Date、RegExp)

其中symbol和bigInt是es6新增的数据类型:

  • symbol:是通过 Symbol() 函数生成,每一个 symbol 的值都是唯一的。也被称作“原子类型”(atom)。symbol 的目的是去创建一个唯一属性键,保证不会与其他代码中的键产生冲突。

    let s1 = Symbol('sym');
    let s2 = Symbol('sym'); 
    s1 === s2 ; // false
    console.log(s1); // Symbol(sym)
    console.log(typeof s1); // symbol
    
  • bigInt:表示任意大小的整数.可以用在一个整数字面量后面加 n 的方式或者调用函数 BigInt() Number.MAX_SAFE_INTEGER表示最⼤安全数字,计算结果是9007199254740991,即在这个数范围内不会出现精度丢失(⼩数除外)。但是⼀旦超过这个范围,js就会出现计算不准确的情况。·因此官⽅提出了BigInt来解决此问题

    const theBiggestInt = 9007199254740991n;
    const alsoHuge = BigInt(9007199254740991); // 9007199254740991n
    const hugeString = BigInt("9007199254740991"); // 9007199254740991n
    

数据存储位置:

  • 栈(stack):存放原始数据类型,简单数据段,占据空间小、大小固定。先进后出。栈区内存由编译器自动分配释放

  • 堆(heap):存放引用数据类型,占据空间大、大小不固定。优先队列,是按优先级来进行排序的,优先级可以按照大小来规定。堆区内存一般由开发着分配释放,若开发者不释放,程序结束时可能由垃圾回收机制回收

    引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址

数据类型检测的方式有哪些?

  1. typeof

    数组、对象、null都会被判断为object,其他判断都正确

    console.log(typeof 'str'); // string 
    console.log(typeof 2); // number 
    console.log(typeof true); // boolean 
    console.log(typeof Symbol()); // symbol 
    console.log(typeof BigInt(123)); // bigint 
    console.log(typeof undefined); // undefined 
    console.log(typeof null); // object
    console.log(typeof {}); // object 
    console.log(typeof []); // object 
    console.log(typeof function(){}); // function 
    
  2. instanceof

    只能正确判断引用数据类型,而不能判断基本数据类型。其内部运行机制是判断在其原型链中能否找到该类型的原型

    console.log(2 instanceof Number); // false
    console.log(true instanceof Boolean); // false 
    console.log('str' instanceof String); // false 
    
    console.log([] instanceof Array); // true 
    console.log(function(){} instanceof Function); // true 
    console.log({} instanceof Object); // true
    
  3. constructor

    对象实例通过 constrcutor 对象访问它的构造函数来判断

    console.log((2).constructor === Number); // true
    console.log((true).constructor === Boolean); // true
    console.log(('str').constructor === String); // true
    console.log(([]).constructor === Array); // true
    console.log((function() {}).constructor === Function); // true
    console.log(({}).constructor === Object); // true
    

    注意:如果改变构造函数的原型,constructor就不能用来判断数据类型了

    function Fn() {}
    console.log(Fn.prototype)  // {constructor: ƒ Fn(),...}
    const f1 = new Fn()
    console.log(f1.constructor)  // ƒ Fn() {}
    console.log(f1.constructor===Fn); // true 
    
    Fn.prototype = new Array()
    console.log(Fn.prototype)  // []
    const f2 = new Fn()
    console.log(f2.constructor)  // ƒ Array() { [native code] }
    console.log(f2.constructor===Fn); // false 
    console.log(f2.constructor===Array); // true
    
  4. Object.prototype.toString.call()

    toString是Object的原型方法(返回对象的具体类型),Number、Array、function等类型作为Object的实例,都重写了toString方法(function类型返回内容为函数体的字符串,Array类型返回元素组成的字符串…)。

    const a = Object.prototype.toString
    console.log(a.call('str'));  // [object String]
    console.log(a.call(2));  // [object Number]
    console.log(a.call(true));  // [object Boolean]
    console.log(a.call(undefined));  // [object Undefined]
    console.log(a.call(null));  // [object Null]
    console.log(a.call([]));  // [object Array]
    console.log(a.call({}));  // [object Object]
    console.log(a.call(function(){}));  // [object Function]
    

instanceof 操作符的实现原理及实现

用于判断构造函数的prototype是否出现在对象的原型链上

function myInstanceof(obj, typeF) {
  let objProto = obj.__proto__;
  const typeFPrototype = typeF.prototype;
  while (true) {
    if (!objProto) {
      return false;
    }
    if (objProto === typeFPrototype) {
      return true;
    }
    objProto = objProto.__proto__;
  }
}

null和undefined区别

undefined 代表的含义是未定义,null是object类型,代表的含义是空对象

console.log(null==undefined)  // true console.log(null===undefined)  // false

typeof undefined // undefined 
typeof null // object

undefined典型用法:

  • 变量被声明但没有赋值时,就等于 undefined。
  • 对象的某个属性没有赋值时,该属性的值为 undefined。
  • 调用函数过程中,应该提供的参数没有提供时,该参数就等于 undefined。
  • 函数没有返回值时,默认返回 undefined。

null典型用法:

  • 作为函数的参数,表示该函数的参数为空。
  • 作为对象原型链的终点。

js浮点数运算为什么出现精度失真?如何解决?

原因:javascript的浮点数运算采用了IEEE 754的标准双精度(64位)浮点运算规则。十进制数会被转换成二进制进行计算,计算结果再转为十进制。64位双精度浮点数的小数部分最多支持53位二进制位,因位数限制会截断二进制数字,所以在进行算术计算时会产生误差。

解决方案:化整数运算

NaN是什么?

NaN表示“非数字”概念,NaN对于表示数字上的错误操作很有用。NaN 是一个特殊值,它和自身不相等

typeof NaN; // "number"
1*undefined; // NaN
NaN === NaN;  // false
NaN == NaN;  // false
NaN !== NaN;  // true

isNaN(1)  // false
isNaN('1')  // false
isNaN(true)  // false
isNaN('a')  // true
isNaN('true')  // true

Object.is() 、比较操作符 ===、== 的区别

  • == 会进行类型转换,比较两个值是否相等
  • === 不做类型转换,比较类型是否相同,值是否相等
  • Object.is() 不会做类型转换,比较类型是否相同,值是否相等。Object.is() 和 === 之间的唯一区别在于它们处理带符号的 0 和 NaN 值的时候不同
NaN === NaN; // false
Object.is(NaN, NaN) - // true

0 === +0; // true
-0 === +0; // true
Object.is(0, +0); // true
Object.is(-0, +0); // false

JavaScript使用

数组的方法

  • 数组和字符串的转换方法:toString()、toLocalString()、join()
  • push|unshift: 向数组的末尾|头部添加一个或多个元素
  • pop|shift:用于删除数组最后一个元素|第一个元素
  • splice:新增|修改|删除数组元素,即修改数组。arr.splice(index,num,item1,.....,itemX)
  • slice:截取数组
  • reverse:反转数组
  • sort:数组排序
  • concat:连接数组或单个元素,并返回新的数组
  • 数组遍历:forEach、map、every、some、find、findIndex、filter、reduce

for...in 和 for...of区别

for...in语句以任意顺序遍历一个对象的除 Symbol 以外的可枚举属性(包括自身属性和继承属性)。不能保证按照特定的顺序返回索引,所以一般不用做数组的遍历。 - for...of遍历的是数组元素值,只是数组内的元素,不包括原型属性和索引

区别:

  1. for in 和 for of 都可以遍历数组,for in 输出的是数组的index下标,而for of 输出的是数组的每一项的值
  2. for in 可以遍历对象,for of 不能遍历对象,只能遍历带有迭代器对象(iterator)的集合,例如Set,Map,String,Array

call、apply、bind 的用法以及区别

相同:改变函数体内 this 的指向。第一个参数都是this要指向的对象,如果如果没有这个参数或参数为undefined或null,则默认指向全局window。

不同: call、bind接收多个参数依次传入。apply第二个参数接收一个数组。 bind不立即执行。而apply、call 立即执行

fn.call(obj,a1,a2,a3)
fn.apply(obj,[a1,a2,a3])
fn.bind(obj,a1,a2,a3)()

异步编程的实现方式?

  • 回调函数 的方式,使用回调函数的方式有一个缺点是,多个回调函数嵌套的时候会造成回调函数地狱,上下两层的回调函数间的代码耦合度太高,不利于代码的可维护。
  • Promise 的方式,使用 Promise 的方式可以将嵌套的回调函数作为链式调用。但是使用这种方法,有时会造成多个 then 的链式调用,可能会造成代码的语义不够明确。
  • Generator/yeild 的方式,它可以在函数的执行过程中,将函数的执行权转移出去,在函数外部还可以将执行权转移回来。当遇到异步函数执行的时候,将函数执行权转移出去,当异步函数执行完毕时再将执行权给转移回来。因此在 generator 内部对于异步操作的方式,可以以同步的顺序来书写。使用这种方式需要考虑的问题是何时将函数的控制权转移回来,因此需要有一个自动执行 generator 的机制,比如说 co 模块等方式来实现 generator 的自动执行。
  • async/await 的方式,async 函数是 generator 和 promise 实现的一个自动执行的语法糖,它内部自带执行器,当函数内部执行到一个 await 语句的时候,如果语句返回一个 promise 对象,那么函数将会等待 promise 对象的状态变为 resolve 后再继续向下执行。因此可以将异步逻辑,转化为同步的顺序来书写,并且这个函数可以自动执行。

防抖和节流

  • 防抖 (Debouncing) :触发高频事件后,n秒内触发重新计算时间,直到n秒后触发执行。防止抖动,避免事件的重复触发。

    应用场景:

    • 点击按钮事件,用户在一定时间段内的点击事件,为了防止和服务端的多次交互,我们可以采用防抖。
    • 输入框的搜索
    • 浏览器窗口大小调整,window触发resize事件
    function debounce(fn, wait) {
     let timer = null;
     return (...args) => {
       clearTimeout(timer);
       timer = setTimeout(() => {
         fn(...args);
       }, wait);
     };
    }
    
  • 节流 (Throttling) :高频事件触发,但在n秒内只会执行一次。控制事件触发的频率。

    应用场景:

    • 监听scroll滚动事件,比如是否滑到底部自动加载更多
    function throttle(fn, wait) {
      let timer = null;
      return (...args) => {
        if (timer) {
          return;
        }
        timer = setTimeout(() => {
          fn(...args);
          clearTimeout(timer);
          timer = null
        }, wait);
      };
    }
    

JavaScript基础

强类型语言和弱类型语言的区别

  • 强类型:实参与形参类型必须相同,编译阶段判断,不允许隐式类型转换
  • 弱类型:不会限制实参类型,只能在运行时类型校验,允许隐式类型转换

构造函数:

在 JavaScript 中,用 new 关键字来调用的函数,称为构造函数。构造函数首字母一般大写

构造函数作用:在创建具有相似的特征(相同属性和方法)的多个对象时,会产生很多重复的代码,而使用构造函数就可以实现代码复用

静态成员和实例成员

  • 静态成员:给构造函数自身添加的成员
  • 实例成员:构造函数内部添加给this的成员,或在构造函数的prototype添加的成员
// 一个构造函数
function Box(value) {
  this.value = value;
}

// 使用 Box() 构造函数创建的所有盒子都将具有的属性
Box.prototype.getValue = function () {
  return this.value;
};

Box.isNull = (val) => {
    return val === null
};
console.log(Box.isNull(12));  // false  静态成员

const box = new Box(1);
console.log(box.value);  // 1  实例成员
box.getValue();  // 1  实例成员

构造函数的返回值

  • 没有手动添加,默认返回this
  • 返回基本类型的数据,最终返回还是this
  • 返回引用类型数据,最终返回该对象,this对象被丢失

构造函数的特点

  • 在创建实例对象时,构造函数内this成员可能未使用,会造成内存浪费,所以一般会在构造函数的prototype上定义共享的属性和方法。
  • new创建的实例可以访问到构造函数原型链上的属性和方法。

new执行过程:

  1. 新建一个对象obj
  2. 把obj的和构造函数通过原型链连接起来,即obj的__proto__ 指向构造函数prototype
  3. 将构造函数的this指向obj,执行构造函数添加属性和方法
  4. 判断构造函数返回类型,如果没有返回对象或返回基本类型数据,返回创建对象,若返回引用类型数据,则返回结果为该引用类型数据

new代码实现

function myNew(func, ...args){
    let obj = Object.create(func.prototype);
    const result = func.call(obj, ...args)
    return typeof result === "object" ? result : obj
}

function Fn(name) {
    this.name = name
}
const f = myNew(Fn, "jon")
console.log(f.name)   // jon

原型和原型链

原型对象:每一个JavaScript对象(null除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型对象,每一个对象都会从原型"继承"属性。

  • 函数的原型(显示原型):每个函数都有一个prototype属性,该属性指向函数的原型
  • 对象的原型(隐式原型):()每个对象都有一个__proto__属性,该属性指向对象的原型

原型链:当读取实例的属性时,自己和原型对象上都没有这个属性,就会往原型对象的原型对象上去找,一直找到最顶层为止。这条查找的路径就叫原型链。原型链的终点Object.prototype对象.proto 为null。 image.png

当原型修改、重写时

function Person(name) {
    this.name = name
}
// 修改原型
Person.prototype.getName = function() {}
var p = new Person('hello')
console.log(p.__proto__ === Person.prototype) // true
console.log(p.__proto__ === p.constructor.prototype) // true

// 重写原型
Person.prototype = {
    getName: function() {}
}
var p = new Person('hello')
console.log(p.__proto__ === Person.prototype)        // true
console.log(p.__proto__ === p.constructor.prototype) // false

// 修改构造函数指向
p.constructor = Person 
console.log(p.__proto__ === Person.prototype) // true console.log(p.__proto__ === p.constructor.prototype) // true

实现继承的方式

function Animal(name) {
    this.name = name
    this.eat = function () {
        console.log(this.name+"正在吃东西")
    }
}
Animal.prototype.getName = function() {
    return this.name
}
  1. 原型链继承

    function Dog(name) {
        this.name = name
    }
    Dog.prototype = new Animal()
    
    let dog = new Dog("Tom")
    console.log(dog.eat())  // Tom正在吃东西
    console.log(dog.getName())  // Tom
    
    Animal.prototype.getName = function() {
        return "123"
    }
    console.log(dog.getName())  // 123
    
    console.log(dog instanceof Animal)  // true
    console.log(dog instanceof Dog)  // true
    
    console.log(dog.constructor === Dog)  // false
    console.log(dog.constructor === Animal)  // true
    
  2. 共享原型

    function Dog(name) {
       this.name = name
    }
    Dog.prototype = Animal.prototype
    
    let dog = new Dog("Tom")
    console.log(dog.eat())  // ERROR dog.eat is not a function
    console.log(dog.getName())  // Tom
    
    console.log(dog instanceof Animal)  // true
    console.log(dog instanceof Dog)  // true
    
    console.log(dog.constructor === Dog)  // false
    console.log(dog.constructor === Animal)  // true
    
  3. 构造继承

    function Dog(name) {
        Animal.call(this,name)
    }
    
    let dog = new Dog("Tom")
    console.log(dog.eat())  // Tom正在吃东西
    console.log(dog.getName())  // error dog.getName is not a function
    
    console.log(dog instanceof Animal)  // false
    console.log(dog instanceof Dog)  // true
    
    console.log(dog.constructor === Dog)  // true
    console.log(dog.constructor === Animal)  // false
    
  4. 组合继承

    function Dog(name) {
        Animal.call(this,name)
    }
    Dog.prototype = new Animal()
    Dog.prototype.constructor = Dog
    
    let dog = new Dog("Tom")
    console.log(dog.eat())  // Tom正在吃东西
    console.log(dog.getName())  // Tom
    
    console.log(dog instanceof Animal)  // true
    console.log(dog instanceof Dog)  // true
    
    console.log(dog.constructor === Dog)  // true
    console.log(dog.constructor === Animal)  // false
    
  5. ES6的 class extend

    class Animal {
      constructor(name) {
        this.name = name;
      }
      getName() {
        return this.name;
      }
    }
    
    class Dog extends Animal {
      constructor(name) {
        super(name);
      }
      getName() {
        return this.name;
      }
    }
    
    let dog = new Dog("Tom");
    console.log(dog.getName()); // Tom
    
    console.log(dog instanceof Animal); // true
    console.log(dog instanceof Dog); // true
    
    console.log(dog.constructor === Dog); // true
    console.log(dog.constructor === Animal); // false
    
  6. Object.create(obj) 创建一个对象,并将对象的原型指向obj

    isPrototypeOf判断一个对象是否是另一个对象的原型

      let animal = {
          name: "animal",
          getName: function(){
              return this.name
          }
      }
      
      let dog = Object.create(animal)
      console.log(dog.getName())  // animal
      
      dog.name = "dog"
      console.log(dog.getName())  // dog
      
     animal.isPrototypeOf(dog)  // true
    

闭包

闭包是指有权访问另一个函数作用域中变量的函数

闭包有两个常用的用途;

  • 闭包的第一个用途是使我们在函数外部能够访问到函数内部的变量。通过使用闭包,可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
  • 闭包的另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。
function A() {
  let a = 1
  window.B = function () {
      console.log(a)
  }
}
A()
B() // 1

对作用域、作用域链的理解

作用域层级:

  1. 全局作用域
  2. 函数作用域
  3. 块级作用域

作用域链:  在当前作用域中查找所需变量,如果在自己作用域找不到该变量就去父级作用域查找,依次向上级作用域查找,直到访问到window对象就被终止,这一层层的关系就是作用域链。

对执行上下文的理解

  • 执行上下文类型

    • 全局执行上下文:任何不在函数内部的都是全局执行上下文,它首先会创建一个全局的window对象,并且设置this的值等于这个全局对象,一个程序中只有一个全局执行上下文。
    • 函数执行上下文:当一个函数被调用时,就会为该函数创建一个新的执行上下文,函数的上下文可以有任意多个。
    • eval函数执行上下文:eval函数中的代码会有属于他自己的执行上下文。
  • 执行上下文栈

    • JavaScript引擎使用执行上下文栈来管理执行上下文
    • 当JavaScript执行代码时,首先遇到全局代码,会创建一个全局执行上下文并且压入执行栈中,每当遇到一个函数调用,就会为该函数创建一个新的执行上下文并压入栈中,引擎会执行位于执行上下文栈顶的函数,当函数执行完成之后,执行上下文从栈中弹出,继续执行下一个上下文。当所有的代码都执行完毕之后,从栈中弹出全局执行上下文
  • 执行上下文:指在JavaScript引擎执行代码之前的准备工作,包括一些变量和函数的提升,this 的赋值等等

this指向

  • this总是指向函数的直接调用者
  • 没有明确调用者时,指向window
  • 做构造函数使用。指向实例对象
  • 箭头函数,this 指向外层的 this(即函数定义位置的上下文this)

事件循环机制

  • 同步任务是指在主线程上排队执行的任务,只有前一个任务执行完毕,才能继续执行下一个任务。new promise()、console.log()属于同步任务

  • 异步任务指的是,不进入主线程、而进入"任务队列"的任务;只有等主线程任务全部执行完毕,"任务队列"的任务才会进入主线程执行。异步任务分为宏任务和微任务

    • 宏任务:setTimeout setInterval ajax Dom事件
    • 微任务:promise.then/finally/catch async/await 隐式创建promise.then
  • 事件循环机制

    • 先执行同步代码,所有同步代码都在主线程上执行,形成一个执行栈。
    • 当遇到异步任务时,会将其挂起并添加到任务队列中,宏任务放入宏任务队列,微任务放进微任务队列。
    • 当同步任务执行完毕,就会执行微队列中的任务。
    • 加入到执行栈中执行,优先执行微任务。
    • 当微队列中的所有微任务执行结束,就会检查宏队列中有没有可执行的宏任务。如果有,则执行该宏任务,之后检查微队列并执行微任务,依次循环。
    • 一次事件循环只能处理一个宏任务,执行宏任务的时候会进行一次轮询,看有没有微任务,如果有微任务将会把微任务执行全部执行完毕,在执行宏任务。

垃圾回收与内存泄漏

垃圾回收:JavaScript代码运行时,需要分配内存空间来储存变量和值。当变量不再参与运行时,就需要系统收回被占用的内存空间,这就是垃圾回收。

回收机制

  • Javascript 具有自动垃圾回收机制,会定期对那些不再使用的变量、对象所占用的内存进行释放,原理就是找到不再使用的变量,然后释放掉其占用的内存。

  • JavaScript中存在两种变量:局部变量和全局变量。全局变量的生命周期会持续要页面卸载;而局部变量声明在函数中,它的生命周期从函数执行开始,直到函数执行结束,在这个过程中,局部变量会在堆或栈中存储它们的值,当函数执行结束后,这些局部变量不再被使用,它们所占有的空间就会被释放。

  • 不过,当局部变量被外部函数使用时,其中一种情况就是闭包,在函数执行结束后,函数外部的变量依然指向函数内部的局部变量,此时局部变量依然在被使用,所以不会回收。

  • 内存泄漏

    以下四种情况会造成内存的泄漏:

    • 全局变量未清除: 定义了全局变量,未将其删除或赋值为null
    • 定时器未清除:  设置了 setInterval 定时器,而忘记取消它,
    • 闭包:  不合理的使用闭包,从而导致某些变量一直被留在内存当中。
    • **DOM事件未正确解绑:**如果注册了事件监听器却没有正确解绑

JavaScript脚本加载的方式有哪些?

每当浏览器解析到

  1. 普通加载

    • script标签放在head标签内。

      浏览器的解析顺序是从上到下的,在解析到body之前不会渲染任何页面的部分,如果js文件很大,加载时间很长,那么就会造成堵塞。使得在该javaScript代码完全执行完之前,页面都是一片空白。

      <head>
         <script type="text/javascript" src="script.js"></script>
      </head>
      
      
    • script标签放在body标签内

      按照顺序先解析渲染了HTML再加载script文件(尽管脚本依然会互相阻塞,但是页面的大部分初始内容已经展示给了用户,这会让页面不会显得太慢.)

      <body>
          <div class="page" id="root"></div>
          <script type="text/javascript" src="script.js"></script>
      </body>
      
  2. 延迟加载

    script标签中写入defer或者async时,就会使JS文件异步加载,即html执行到script标签时,JS加载和文档解析同时进行。加载完成后JS文件的执行还是会阻塞文档解析。

    <script defer type="text/javascript" src="script.js"></script>
    <script async type="text/javascript" src="script.js"></script>
    

    defer 和 async 区别

    • async 脚本加载完成后立即执行,可以在DOM尚未完全下载完成就加载和执行;而defer脚本需要等到文档所有元素解析完成之后才执行
    • async 执行与文档顺序无关,先加载哪个就先执行哪个;defer会按照文档中的顺序执行
    • js代码创建的script标签默认async
  3. 动态脚本

    可以创建一个脚本并使用JavaScript将其动态添加到文档中。通过文档对象模型(DOM),我们可以几乎可以页面任意地方创建。

    <script type='text/javascript'> 
        var script = document.createElement('script'); 
        script.type = 'text/javaScript'; 
        script.src = 'file1.js';
        document.getElementsByTagName('head')[0].appendChild(script); 
    </script>
    

什么是 DOM 和 BOM?

  • DOM 指的是文档对象模型,它指的是把文档当做一个对象,这个对象主要定义了处理网页内容的方法和接口。

  • BOM 指的是浏览器对象模型,它指的是把浏览器当做一个对象来对待,这个对象主要定义了与浏览器进行交互的法和接口。BOM的核心是 window,而 window 对象具有双重角色,它既是通过 js 访问浏览器窗口的一个接口,又是一个 Global(全局)对象。这意味着在网页中定义的任何对象,变量和函数,都作为全局对象的一个属性或者方法存在。window 对象含有 location 对象、navigator 对象、screen 对象等子对象,并且 DOM 的最根本的对象 document 对象也是 BOM 的 window 对象的子对象。

对AJAX的理解,实现一个AJAX请求

AJAX是 Asynchronous JavaScript and XML 的缩写,指的是通过 JavaScript 的 异步通信,从服务器获取 XML 文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。 创建AJAX请求的步骤:

  • 创建一个 XMLHttpRequest 对象。
  • 在这个对象上使用 open 方法创建一个 HTTP 请求,open 方法所需要的参数是请求的方法、请求的地址、是否异步和用户的认证信息。
  • 在发起请求前,可以为这个对象添加一些信息和监听函数。比如说可以通过 setRequestHeader 方法来为请求添加头信息。还可以为这个对象添加一个状态监听函数。一个 XMLHttpRequest 对象一共有 5 个状态,当它的状态变化时会触发onreadystatechange 事件,可以通过设置监听函数,来处理请求成功后的结果。当对象的 readyState 变为 4 的时候,代表服务器返回的数据接收完成,这个时候可以通过判断请求的状态,如果状态是 2xx 或者 304 的话则代表返回正常。这个时候就可以通过 response 中的数据来对页面进行更新了。
  • 当对象的属性和监听函数设置完成后,最后调用 sent 方法来向服务器发起请求,可以传入参数作为发送的数据体。
const SERVER_URL = "/server";
let xhr = new XMLHttpRequest();
// 创建 Http 请求
xhr.open("GET", url, true);
// 设置状态监听函数
xhr.onreadystatechange = function() {
  if (this.readyState !== 4) return;
  // 当请求成功时
  if (this.status === 200) {
    handle(this.response);
  } else {
    console.error(this.statusText);
  }
};
// 设置请求失败时的监听函数
xhr.onerror = function() {
  console.error(this.statusText);
};
// 设置请求头信息
xhr.responseType = "json";
xhr.setRequestHeader("Accept", "application/json");
// 发送 Http 请求
xhr.send(null);

使用Promise封装AJAX:

// promise 封装实现:
function getJSON(url) {
  // 创建一个 promise 对象
  let promise = new Promise(function(resolve, reject) {
    let xhr = new XMLHttpRequest();
    // 新建一个 http 请求
    xhr.open("GET", url, true);
    // 设置状态的监听函数
    xhr.onreadystatechange = function() {
      if (this.readyState !== 4) return;
      // 当请求成功或失败时,改变 promise 的状态
      if (this.status === 200) {
        resolve(this.response);
      } else {
        reject(new Error(this.statusText));
      }
    };
    // 设置错误监听函数
    xhr.onerror = function() {
      reject(new Error(this.statusText));
    };
    // 设置响应的数据类型
    xhr.responseType = "json";
    // 设置请求头信息
    xhr.setRequestHeader("Accept", "application/json");
    // 发送 http 请求
    xhr.send(null);
  });
  return promise;
}

事件冒泡、事件捕获和事件委托

几个嵌套的元素处理同一个事件(click、focus等等这些事件),需要知道哪个元素的事件先触发,理解事件传播顺序就变得很有必要。

  • 事件流
    一个完整的JS事件流是从window开始,最后回到window的一个过程。事件流被分为三个阶段捕获过程、事件触发过程、冒泡过程

  • addEventListener() 方法

    target.addEventListener(type, listener, useCapture)
    
    • useCapture 默认fasle,表示事件冒泡阶段调用事件处理函数
    • useCapture 为true,表示在事件捕获阶段调用处理函数
  • 事件捕获:根节点 =>事件源(由外到内)进行事件传播。

    window.addEventListener("click", () => {
    console.log('window');
    },true);
    
    document.querySelector("outer").addEventListener("click", () => {
    console.log('outer');
    },true);
    
    document.querySelector("button").addEventListener("click", () => {
    console.log('button');
    },true);
    
    • 点击outer输出:window - outer
    • 点击button输出:window - outer - button
  • 事件冒泡:事件源 =>根节点(由内到外)进行事件传播

     window.addEventListener("click", () => {
     console.log('window');
     });
    
     document.querySelector("outer").addEventListener("click", () => {
     console.log('outer');
     });
    
     document.querySelector("button").addEventListener("click", () => {
     console.log('button');
     });
    
    • 点击outer输出:outer - button
    • 点击button输出:button - outer - window
  • 事件委托: 利用事件冒泡,把子元素的事件都绑定到父元素上。如果子元素阻止了事件冒泡,那么委托就无法实现。

    例如:在ul上代理所有liclick事件

    const ul = document.querySelector('ul');
    // 事件绑定到其公共的祖先元素ul上
    ul.addEventListener('click', function (event){
        // 这里this是ul,event.target 才是li
        alert(`${event.target.innerText}被点击了`);
    })
    
  • 阻止事件委托和事件冒泡

    • event.stopPropagation()

      stopPropagation()方法既可以阻止事件冒泡,也可以阻止事件捕获,但不会阻击默认行为(它就执行了超链接的跳转)

    • event.stopImmediatePropagation()

      stopImmediatePropagation阻止事件捕获,也可以阻止事件冒泡 另外也可以阻止监听同一事件的其他事件监听器被调用。