JS疑难点总结

120 阅读8分钟

1.闭包

1.1什么是闭包?

一个函数和它使用的函数外部的变量就组成了闭包

举例:

function f1(){
  let a=1
  function f2(){
    let a=2
    function f3(){
      console.log(a)
    }
    a=22
    f3()
  }
  f2()
}
f1()

1.2.闭包的用途是什么?

  • 闭包可以让你从内部函数访问外部函数作用域

  • 能够维持变量,不被垃圾回收

1.3.闭包的缺点是什么?

常驻内存 会增大内存的使用量 使用不当会造成内存泄露

2.call、apply、bind 的用法分别是什么?

2.1call:

call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。

fn.call(xxx, 1, 2, 3)

第一个参数是this, 后面所有的参数是arguments

Array.prototype.forEach2 = function(fn) {
    for (let i = 0; i < this.length; i++) {
        fn(this[i], i)
    }
}

let arr = [1,2,3]
arr.forEach2.call(arr, (item)=>console.log(item))

2.2apply:

该方法的语法和作用与call方法类似,只有一个区别,就是 call() 方法接受的是一个参数列表,而 apply() 方法接受的是一个包含多个参数的数组。

求数组里的的最大值和最小值:

const numbers = [5, 6, 2, 3, 7];

const max = Math.max.apply(null, numbers);

console.log(max);
// expected output: 7

const min = Math.min.apply(null, numbers);

console.log(min);
// expected output: 2

2.3bind:

bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

语法:function.bind(thisArg[, arg1[, arg2[, ...]]])

3. 请说出至少 10 个 HTTP 状态码,并描述各状态码的意义。

HTTP 状态码(英语:HTTP Status Code)是用以表示 HTTP 响应状态的 3 位数字代码

  • 100~199 消息
  • 200-299 成功
  • 300 -399 重定向
  • 400-499 客户端出错
  • 500-599 服务器出错

100 ~ 199

服务器已经收到请求头,请求者应当继续提出请求。服务器返回此代码表示已收到请求的第一部分,正在等待其余部分

101:服务端已经理解了客户的请求,并通过 Ugrade 消息头通知客户端采用不同的协议来完成这个请求

200~299

200: 已成功处理了请求。出现此状态码是表示正常状态

201: 请求成功并且服务器创建了新的资源

202:服务器已接受请求,但尚未处理

204:服务器成功处理了请求,但没有返回内容

206:服务器成功处理了部分 GET 请求

300~399

300:针对请求,服务器可执行多种操作。服务器可根据请求者选择一项操作,或提供操作列表供请求者选择

301:请求的资源已永久移动到新位置。服务器返回此响应(对 GET 或 HEAD 请求的响应)时,会自动将请求者转到新位置。

302:请求的资源临时从不同的 URI 响应请求。由于这样的重定向是临时的,客户端应当继续向原有地址发送以后的请求。只有在 Cache-Control 或 Expires 中进行了指定的情况下,这个响应才是可缓存的。

303:对应当前请求的响应可以在另一个 URL 上被找到,而且客户端应当采用 GET 的方式访问那个资源。这个方法的存在主要是为了允许由脚本激活的 POST 请求输出重定向到一个新的资源。这个新的 URI 不是原始资源的替代引用。同时,303响应禁止被缓存。当然,第二个请求(重定向)可能被缓存。

304:自从上次请求后,请求的资源未修改过。服务器返回此响应时,不会返回资源的内容,因此可节省带宽和开销。

400~499

400:服务器不理解请求的语法。

401:请求要求身份验证。 对于需要登录的网页,服务器可能返回此响应。

403:服务器拒绝请求。

404:服务器找不到请求的资源。 例如,对于服务器上不存在的资源经常会返回此代码。

408:服务器等候请求时发生超时

409:由于和被请求的资源的当前状态之间存在冲突,请求无法完成。

429:用户在给定的时间内发送了太多的请求。旨在用于网络限速。

500~599

500:服务器遇到错误,无法完成请求

502:服务器作为网关或代理,从上游服务器收到无效响应。

503:服务器目前无法使用(由于超载或停机维护)。 通常,这只是暂时状态。

4.数组去重

假设有数组 array = [1,5,2,3,4,2,3,1,3,4]

4.1使用set

const unique=Array.from(new Set(array));

缺点:他无法去重引用类型的数据,比如对象数组。

4.2不使用set

4.2.1双重for循环

 arr = [1,5,2,3,4,2,3,1,3,4]
    for(var i=0;i<arr.length;i++){
       var cur=arr[i];
       for(var j=i+1;j<arr.length;){
           if(cur===arr[j]){
              arr.splice(j,1)
           }else{
               j++
           }
       }
    }
    console.log(arr);

缺点:

比较麻烦 占内存

4.2.2reduce+indexOf

array = [1,5,2,3,4,2,3,1,3,4]
  var unique = array.reduce(function (accumulator, currentValue) {
  if (accumulator.indexOf(currentValue) === -1) {
    accumulator.push(currentValue);
  }
  return accumulator
}, [])

console.log(unique);

4.2.3Map

arr = [1,5,2,3,4,2,3,1,3,4]
function arrayNoRepeat(arr) { 
  let map = new Map();
    let result = new Array();  // 数组用于返回结果
    for (let i = 0; i < arr.length; i++) {
        if(map.has(arr[i])) { // 判断 map 中是否已有该 key 值
            map.set(arr[i], true);  // 后面的true 代表该 key 值在原始数组中重复了,false反之
        } else {  // 如果 map 中没有该 key 值,添加
            map.set(arr[i], false);  
            result.push(arr[i]);
        }
    } 
    return result;
}
console.log(arrayNoRepeat(arr));

4.2.4计数排序

unique = (array) => {
    const hash = []
    for(let i=0;i<array.length; i++){
        hash[array[i]] = true
    }
    const result = []
    for(let k in hash){
        result.push(k)
    }
    return result
}

缺点:只支持数字或者字符串数组,如果数组里面有对象,比如 array = [{number:1}, 2],就会出错。

5.Dom事件相关

5.1什么是事件委托

由于冒泡阶段,浏览器从用户点击的内容从下往上遍历至 window,逐个触发事件处理函数,

因此可以监听一个祖先节点(例如爸爸节点、爷爷节点)来同时处理多个子节点的事件

具体实现:给祖先元素绑定监听函数,函数内部检查事件源event.target,如果事件发生在我们感兴趣的元素上,就处理该事件

优点:减少监听器数量,节省内存占用;可以监听动态元素

缺点:在调试的时候不容易确定监听者

5.2怎么阻止默认动作?

e.preventDefault()

如果 Event 对象的 cancelable 属性是 fasle,那么就没有默认动作,或者不能阻止默认动作。无论哪种情况,调用该方法都没有作用。

5.3怎么阻止冒泡?

捕获不可以取消,但是冒泡可以取消,e.propagation()

6.JS继承

6.1基于原型的继承

function Parent(name) {
   this.name = name
   this.colors = ["red", "blue", "yellow"]
}
Parent.prototype.sayName = function () {
   console.log(this.name);
}

function Child(name, age) {
   Parent.call(this, name) // 继承父类属性
   this.age = age;
}

Child.prototype = new Parent(); // 子类原型为父类的实例对象 
// 也可以写成
// Child.prototype.__proto__ = Parent.prototype

Child.prototype.sayAge = function () {
   console.log(this.age);
}

let child1 = new Child("lili", 19);
child1.colors.push("pink");
console.log(child1.colors); // ["red", "blue", "yellow", "pink"]
child1.sayAge(); // 19
child1.sayName(); // "lili"

let child2 = new Child("yue", 22);
console.log(child2.colors);  // ["red", "blue", "yellow"]
child2.sayAge(); // 22
child2.sayName(); // "yue"

6.2class继承

class Person{
    constructor(name,age){
        this.name = name
        this.age = age
    }
}


class Child extends Person{
    constructor(name,age,gender){
        super(name,age)//通过super调用父类的构造方法
        this.gender = gender
    }
}

let child1 = new Child('lili',24,'female')
console.log(child1) // {name: 'lili', age: 24, gender: 'female'}

7.数组排序

array = [2,1,5,3,8,4,9,5]

7.1冒泡排序

  • 在一次循环中相邻的两个元素进行循环比较,如果前者大(小)于后者,则交换位置,这样做的结果就是第一次循环最大(小)的排到了最后;
  • 重复这样的动作,重复多少次,由数组的长度决定,比如length为2,重复一次就行,length为3,就重复两次..依次类推,要重复length-1次;
 function sort(arr){
   //外层循环控制比较的轮数
    for(var i=0;i<arr.length-1;i++){
         for(var j=0;j<arr.length-i-1;j++){
           //内层循环控制交换的次数
              if(arr[j]>arr[j+1]){
                var temp=arr[j];
                arr[j]=arr[j+1];
                arr[j+1]=temp;
                    }
                }
            }
            return arr;
        }
let array=sort([2,1,5,3,8,4,9,5])
console.log(array)

7.2插入排序

对于一个数组,先从中取出第一个项形成新的数组,其后每次往新的数组中插入新的值,每次插完后都要进行排序,直到插完为止;

// 插入排序
    function insertSort(arr) {
        let arr1= [arr[0]];
        for (let i = 1; i < arr.length; i++) {
            // 插入多少次
            arr1.push(arr[i]);
            // 排序只需要循环length-1次
            for (let j = 0;j < arr1.length - 1;j++){
                // 每次插入要做的事情
                if (arr1[arr1.length-j-1] < arr1[arr1.length-j-2]){
                    // 交换位置
                    var arrTem = arr1[arr1.length-j-2];
                    arr1[arr1.length-j-2] = arr1[arr1.length-j-1];
                    arr1[arr1.length-j-1] = arrTem;
                }
            }
        }
        return arr1
    }
    var arr = [2,1,5,3,8,4,9,5] 
    console.log(insertSort(arr))

7.3选择排序

选择法排序是指每次选择所要排序的数组中值最小的数组元素(按从小到大的顺序排序,如果按从大到小的顺序排序,则选择值最大的数组元素),将这个数组元素的值与最前面没有进行排序的数组元素的值互换。

let sort = (numbers) => {
    for(let i=0; i<numbers.length -1; i++){
        let index = minIndex(numbers.slice(i))+ i
          // index对应的数应该放到i处
        if(index!==i){swap(numbers, index, i)
        }
    }
    return numbers
}
let swap = (array, i ,j) =>{
    let temp = array[i]
    array[i] = array[j]
    array[j] = temp
}
let minIndex = (numbers) =>{
    let index = 0
    for(let i=1; i<numbers.length; i++){
        if(numbers[i] < numbers[index]){
            index = i
        }
    }
    return index
}
let array = [2,1,5,3,8,4,9,5]
sort(array) 

7.4快速排序

Quick sort 以某某为基准,小的去前,大的去后。

let quickSort = (arr) => {
  // 终止条件
  if (arr.length <= 1) return arr
  // 指定一个基准
  let index = Math.floor(arr.length / 2)
  // 数组排除基准数
  let num = arr.splice(index, 1)[0]
  let left = []
  let right = []
  // 剩余项和基准数比较,小的放左边,大的放右边
  for (let i = 0; i < arr.length; i++) {
    if (arr[i] < num) {
      left.push(arr[i])
    } else {
      right.push(arr[i])
    }
  }
  return quickSort(left).concat([num], quickSort(right))
}
let arr = [2,1,5,3,8,4,9,5]
quickSort(arr);

7.5合并排序

let mergeSort = (arr) => {
  // 把数组拆分到一个数组只有一项(已排序),再合并
  let k = arr.length
  // 递进的终止条件
  if (k === 1) {
    return arr
  }
  let left = arr.slice(0, Math.floor(k / 2))
  let right = arr.slice(Math.floor(k / 2))
  return merge(mergeSort(left), mergeSort(right))
}

// 核心 合并
let merge = (arrA, arrB) => {
  if (arrA.length === 0) return arrB
  if (arrB.length === 0) return arrA
  return arrA[0] < arrB[0]
    ? [arrA[0]].concat(merge(arrA.slice(1), arrB))
    : [arrB[0]].concat(merge(arrA, arrB.slice(1)))
}
arr = [2,1,5,3,8,4,9,5]
mergeSort(arr);

8.promise

在没有Promise之前,如果我们想要操作多个异步函数,会有回调地狱

8.1创建一个Promise实例

Promise 对象是由关键字 new 及其构造函数来创建的。该构造函数接受两个函数——resolve 和 reject ——作为其参数。当异步任务顺利完成且返回结果值时,会调用 resolve 函数;而当异步任务失败且返回失败原因(通常是一个错误对象)时,会调用reject 函数。

resolve函数和reject函数都只接受一个参数

let myFirstPromise = new Promise(function(resolve, reject){
    //当异步代码执行成功时,我们才会调用resolve(...), 当异步代码失败时就会调用reject(...)
    //在本例中,我们使用setTimeout(...)来模拟异步代码,实际编码时可能是XHR请求或是HTML5的一些API方法.
    setTimeout(function(){
        resolve("成功!"); //代码正常执行!
    }, 250);
});

myFirstPromise.then(function(successMessage){
    //successMessage的值是上面调用resolve(...)方法传入的值.
    //successMessage参数不一定非要是字符串类型,这里只是举个例子
    console.log("Yay! " + successMessage);
}, function(errorMessage){
   // reject会调用这个函数
});

8.2Promise.prototype.then()

then() 方法返回一个 Promise 。它最多需要有两个参数:Promise 的成功和失败情况的回调函数。

const promise1 = new Promise((resolve, reject) => {
  resolve('Success!');
});

promise1.then((value) => {
  console.log(value);
  // expected output: "Success!"
});

then() 方法会返回一个新的promise对象,实现链式调用

const ajax = (method,url)=>{
  return new Promise((resolve,reject)=>{
    const xhr = new XMLHttpRequest()
    xhr.open(method,url)
    xhr.onreadystatechange = ()=>{
      if(xhr.readyState === 4){
        if(xhr.status >= 200 && xhr.status <300){
          resolve(xhr.response) // 请求成功
        }else{
          reject(new Error(xhr.statusText)) //请求失败
        }
      }
    }
    xhr.setRequestHeader("Accept", "application/json");
    xhr.send()
  })
}
// 第一个then返回一个值
ajax('get','/posts.json').then((json)=>{
  return json.post
},(error)=>{
  console.log('失败:'+error)
}).then((post)=>{ // 上一个回调函数返回结果作为参数,传入第二个回调函数
  //...
})

// 第一个then也有可能返回一个Promise对象
ajax('get','/posts.json').then(
   json => ajax('get',json.commentURL)
}).then(
  comments => console.log("resolved: ", comments), // 当第一个then方法指定的回调函数返回的Promise对象状态变为resolved,调用这个回调函数
  err => console.log("rejected: ", err)
});

8.3Promise.all()

Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

const p = Promise.all([p1, p2, p3]);

(1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。

(2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

var p = Promise.all([1,2,3]);
var p2 = Promise.all([1,2,3, Promise.resolve(444)]);
var p3 = Promise.all([1,2,3, Promise.reject(555)]);
setTimeout(function(){
    console.log(p);
    console.log(p2);
    console.log(p3);
});
// logs
// Promise { <state>: "fulfilled", <value>: Array[3] }
// Promise { <state>: "fulfilled", <value>: Array[4] }
// Promise { <state>: "rejected", <reason>: 555 }

8.4Promise.race()

Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

const p = Promise.race([p1, p2, p3]);

上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

var p1 = new Promise(function(resolve, reject) {
    setTimeout(resolve, 500, "one");
});
var p2 = new Promise(function(resolve, reject) {
    setTimeout(resolve, 100, "two");
});

Promise.race([p1, p2]).then(function(value) {
  console.log(value); // "two"
  // 两个都完成,但 p2 更快
});

var p3 = new Promise(function(resolve, reject) {
    setTimeout(resolve, 100, "three");
});
var p4 = new Promise(function(resolve, reject) {
    setTimeout(reject, 500, "four");
});

Promise.race([p3, p4]).then(function(value) {
  console.log(value); // "three"
  // p3 更快,所以它完成了
}, function(reason) {
  // 未被调用
});

var p5 = new Promise(function(resolve, reject) {
    setTimeout(resolve, 500, "five");
});
var p6 = new Promise(function(resolve, reject) {
    setTimeout(reject, 100, "six");
});

Promise.race([p5, p6]).then(function(value) {
  // 未被调用
}, function(reason) {
  console.log(reason); // "six"
  // p6 更快,所以它失败了
});

9.跨域

9.1同源

浏览器为了保护用户隐私和数据安全制定了同源策略,同源策略规定:如果两个URL的协议、域名和端口号都一致,这两个URL就是同源的

9.2跨域

当一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域。请求可以发出,但是响应会被浏览器阻止

9.3 JSONP 跨域

核心思想:利用script标签可以跨域的特性。

  1. 站点A声明一个回调函数,函数参数是想要获取的数据,创建script标签,src是跨域请求的url,并且将函数名以问号传参的方式发送给站点B;
  2. 站点B收到请求后,后端改造JS文件内容,将函数名和数据拼接成字符串返回给站点A
  3. 站点A这边接收到响应,执行回调函数,就可以拿到数据
  • 缺点:由于是script标签,只能发get请求,不支持post请求
  • 优点:兼容性好

9.4 CORS 跨域

  • 对于简单请求,浏览器发送的请求报文包含Origin字段,被请求站点在响应头添加 Access-Control-Allow-Origin: http://请求站点
  • 对于复杂请求,浏览器会先使用OPTIONS方法发起一次预检请求,服务器需要设置如下响应头 预检请求完成后,再发送实际请求,服务器设置响应头Access-Control-Allow-Origin: http://请求站点