javascript

336 阅读14分钟

从输入网址到渲染完成经历了哪些过程?

  1. 输入网址,先通过DNS解析域名为IP地址,
  2. 与WEB服务器建立TCP链接
  3. 向web服务器发送http请求,服务器响应请求,返回指定url数据
  4. 浏览器加载web服务器返回的数据及解析html源文件
  5. 生成DOM树,解析css和js,渲染页面。

数组去重

let array = [1,1,2,3,3,4,4,5,6];
let arrays = new Set(array); //es6的set语法
console.log(arrays) //1,2,3,4,5,6

排序

降序

let abc= [2,1,5,4,3,9,7,8,10,20]
function sorts(a,b){
	return b-a; 主要区别
}
abc.sort(sorts);
console.log(abc) // [20, 10, 9, 8, 7, 5, 4, 3, 2, 1]

升序

let abc= [2,1,5,4,3,9,7,8,10,20]
function sorts(a,b){
	return a-b; //主要区别
}
abc.sort(sorts);
console.log(abc) // [1, 2, 3, 4, 5, 7, 8, 9, 10, 20]

js原型和原型链

js原型

JS中每个函数都存在有一个原型对象属性prototype。并且所有函数的默认原型都是Object的实例。

js原型链

每个继承父函数的子函数的对象都包含一个内部属性_proto_。该属性包含一个指针,指向父函数的prototype。若父函数的原型对象的_proto_属性为再上一层函数。在此过程中就形成了原型链。

这个知识点,可以查看 juejin.cn/post/684490…

js数据类型

js基本数据类型

String、Number 、Boolean、Undefined、Null、Symbol

js引用数据类型

Object、Array、Function、Array、Data等

数组常用方法

join(): arr.join(separator) ,separator参数为可选。将数组的元素组起一个字符串,以separator为分隔符,省略的话则用默认用逗号为分隔符,该方法只接收一个参数:即分隔符。

//不带参数
let arr = ["掘金",6,"大地王者","奋斗"]
console.log(arr.join());//掘金,6,大地王者,奋斗
//带参数
let arr = ["掘金",6,"大地王者","奋斗"]
console.log(arr.join("-"));//掘金-6-大地王者-奋斗

push()和pop():push(): 可以接收任意数量的参数,把它们逐个添加到数组末尾,并返回修改后数组的长度。 pop():数组末尾移除最后一项,减少数组的 length 值,然后返回移除的项。

let arr = [1];
arr.push(2,3);
console.log(arr)//[1, 2, 3]

arr2.pop();
console.log(arr2) // [1, 2]

shift() 和 unshift():shift():删除原数组第一项,并返回删除元素的值;如果数组为空则返回undefined 。 unshift:将参数添加到原数组开头,并返回数组的长度。列子可参考push()和pop()

reverse():反转数组项的顺序。

concat():将参数添加到原数组中。这个方法会先创建当前数组一个副本,然后将接收到的参数添加到这个副本的末尾,最后返回新构建的数组。在没有给 concat()方法传递参数的情况下,它只是复制当前数组并返回副本。此方法也可以用于数组的合并

let arr = [1,2,3]
let arr1 = arr6.concat(5,6,7)
console.log(arr1) //[1, 2, 3, 5, 6, 7]

slice():返回从原数组中指定开始下标到结束下标之间的项组成的新数组。slice()方法可以接受一或两个参数,即要返回项的起始和结束位置。在只有一个参数的情况下, slice()方法返回从该参数指定位置开始到当前数组末尾的所有项。如果有两个参数,该方法返回起始和结束位置之间的项——但不包括结束位置的项。

splice():可以实现删除、插入和替换。

删除:可以删除任意数量的项,只需指定 2 个参数:要删除的第一项的位置和要删除的项数。例如, splice(0,2)会删除数组中的前两项。

插入:可以向指定位置插入任意数量的项,只需提供 3 个参数:起始位置、 0(要删除的项数)和要插入的项。例如,splice(2,0,4,6)会从当前数组的位置 2 开始插入4和6。


替换:可以向指定位置插入任意数量的项,且同时删除任意数量的项,只需指定 3 个参数:起始位置、要删除的项数和要插入的任意数量的项。插入的项数不必与删除的项数相等。例如,splice (2,1,4,6)会删除当前数组位置 2 的项,然后再从位置 2 开始插入4和6。

splice()方法始终都会返回一个数组,该数组中包含从原始数组中删除的项,如果没有删除任何项,则返回一个空数组。

map() (ES5新增):方法返回一个新数组,数组中的元素为原始数组元素调用函数处理后的值。不会对空数组进行检测。不会改变原始数组。参数接收的是一个方法,Array.map(callback);

const arr = [1, 3, 4, 5, 6, 7, 8, 10];
const res = arr.map((num)=>{
    return num * num; //[ 1, 9, 16, 25, 36, 49, 64, 100 ]
})

filter() (ES5新增):方法创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素。数组。

var words = ['1', '2', '3', '14', '34', '56'];

const result = words.filter(word => word > 6);

console.log(result); //["14", "34", "56"]

every() (ES5新增):判断数组中每一项都是否满足条件,只有所有项都满足条件,才会返回true。

some() (ES5新增):判断数组中是否存在满足条件的项,只要有一项满足条件,就会返回true。

有关数组的更多用法,可以查看: developer.mozilla.org/en-US/docs/…

闭包

什么是闭包

闭包是一个定义在一个函数(父函数)里面的函数,它拥有对父函数里面的变量的访问权。

闭包注意事项

通常,函数的作用域及其所有变量都会在函数执行结束后被销毁。但是,在创建了一个闭包以后,这个函数的作用域就会一直保存到闭包不存在为止。

function makeAdder(x) {
  return function(y) {
    return x + y;
  };
}

var add5 = makeAdder(5);
var add10 = makeAdder(10);

console.log(add5(2));  // 7
console.log(add10(2)); // 12

// 释放对闭包的引用
add5 = null;
add10 = null;

闭包中的 this 对象

var name = "The Window";

var obj = {
  name: "My Object",
  
  getName: function(){
    return function(){
      return this.name;
    };
  }
};

console.log(obj.getName()());  // The Window

obj.getName()()实际上是在全局作用域中调用了匿名函数,this指向了window。这里要理解函数名与函数功能(或者称函数值)是分割开的,不要认为函数在哪里,其内部的this就指向哪里。匿名函数的执行环境具有全局性,因此其 this 对象通常指向 window。

闭包的应用

应用闭包的主要场合是:设计私有的方法和变量。

闭包的缺陷

闭包的缺点就是常驻内存会增大内存使用量,并且使用不当很容易造成内存泄露

来一道常考闭包面试题

function fun(n,o){
  console.log(o);
  return {
    fun: function(m){
      return fun(m,n);
    }
  };
}

var a = fun(0);  // ?
a.fun(1);        // ?        
a.fun(2);        // ?
a.fun(3);        // ?

var b = fun(0).fun(1).fun(2).fun(3);  // ?

var c = fun(0).fun(1);  // ?
c.fun(2);        // ?
c.fun(3);        // ?

答案

undefined
0
0
0

undefined, 0, 1, 2

undefined, 0
1
1

Promise

promise有三种状态:

  1. 等待中(pending)
  2. 完成了 (resolved)
  3. 拒绝了(rejected)

状态一旦改变,就不能再次改变

new Promise((resolve, reject) => {
  resolve('success')
  reject('reject') // 无效
})

promise构造函数内部代码是 立即执行的

new Promise((resolve, reject) => {
  console.log('new Promise')
  resolve('success')
})
console.log('finifsh') // new Promise -> finifsh

如果在then中使用了return,那么return的值会被Promise.resolve()包装

Promise.resolve(1)
  .then(res => {
    console.log(res) // => 1
    return 2 // 包装成 Promise.resolve(2)
  })
  .then(res => {
    console.log(res) // => 2
  })

实现一个简单的Promise

function Promise(executor) {
    //executor
    this.status = 'pending'
    this.reason = null
    this.data = null
    this.onFulFilledList = []
    this.onRejectedList = []
    const _this = this;

    function resolve(data) {
        if(_this.status == 'pending') {
            _this.data = data
            _this.status = 'onFulfilled'
            _this.onFulFilledList.forEach(element => {
                element(_this.data)
            });
        }
    }

    function reject(e) {
        if(_this.status == 'pending') {
            _this.reason = e
            _this.status = 'rejected'
            _this.onRejectedList.forEach(element => {
                element(_this.reason)
            })
        }
    }

    executor(resolve, reject)
}

Promise.prototype.then = function(res, rej) {
    const _this = this
    if(_this.status=='onFulfilled') {
        res(_this.data)
        return
    }
    if(_this.status=='rejected') {
        res(_this.reason)
    }
    if(_this.status=='pending'){
        _this.onFulFilledList.push(res)
        _this.onRejectedList.push(rej)
    }
}

面试够用用版

function promise(constructor) {
    let self = this;
    self.status = "pending"; //定义状态改变前的初始状态
    self.value = undefined; //定义状态为resolved的时候的状态
    self.reason = undefined; //定义状态为rejected的时候的状态
    
    function resolve(value) {
        两个==="pending",保证了状态的改变是不可逆的
        if(self.status === 'pending') {
            self.value = value;
            self.status = "resolved"
        }
    }

    function reject(reason) {
        两个==="pending",保证了状态的改变是不可逆的
        if(self.status === "pending") {
            self.readon = reason;
            self.status = "rejected";
        }
    }

    //捕获构造异常
    try{
        constructor(resolve, reject)
    }catch(e){
        reject(e)
    }
}

同时,需要在 myPromise的原型上定义链式调用的 then方法:

promise.prototype.then = function(onFullfilled, onRejected) {
    let self = this;
    switch(self.status){
        case "resolved":
            onFullfilled(relf.value);
        brack;
        case "rejected":
             onRejected(self.reason);
        break;
        default:
    }
}

new的实现原理

new关键字会进行如下操作

  1. 创建一个空对象,构造函数中的this指向这个空对象
  2. 链接该对象到另一个对象(即设置该对象的__proto__为构造函数的prototype)
  3. 执行构造函数,将构造函数内的this作用域指向1步骤中创建的空对象{}
  4. 如果构造函数有返回值,则return返回值,否则return空对象obj

根据new的操作,实现代码

function new() {
    let obj = {} //创建一个空对象
    let [constructor, ...args] = [...arguments]
    obj.__proto__ == constructor.prototype //设置该对象的__proto__为构造函数的prototype
    let result = constructorr.apply(obj, args) //将构造函数的作用域指向创建的空对象上
    if(result && (typeof (result) === 'object' || typeof(result) ==='function')) {
       return result //如果构造函数执行的结构返回的是一个对象,那么返回这个对象
    }
    return obj //如果构造函数返回的不是一个对象,返回创建的新对象
}

写一个方法,实现sum(2,3)和sum(2)(3)能够正常工作

第一种方法

function sum(x) {
    if(arguments.length == 2) {
        return arguments[0] + arguments[1];
    }else{
        return function(y) {
            retunr x + y
        }
    }
}

第二种方法

function sum(x,y) {
    if(y !== undefined){
        return x + y;
    }else{
        return function (y) {
            return x + y;
        }
    }
}

防抖实现

典型例子:鼠标连击触发

实现原理:当一次事件发生后,事件处理器要等一定阀值的时间,如果这段时间过去后,再也没有事件发生,就处理最后一次发生的事件

function debounce(fn, wait=50, immediate) {
    let timer;
    return function() {
        if(immediate){
            fn.apply(this, arguments)
        }
        if(timer) clearTimeout(timer)
        timer = setTimeout(() => {
            fn.apply(this, arguments)
        }, wait)
    }
}

节流实现

可以理解为时间再一个管道中传输,加上这个节流阀以后,时间的流速就会减慢,实际上这个函数的就是如此,他可以将一个函数的调用频率在一定阀值内,例如1s,那么1s内这个函数一定不会被调用两次

function throttle(fn, wait) {
    let prev = new Date();
    return function() {
        const args = arguments;
        const now =-= new Date();
        if(now - prev > wait) {
            fn.apply(this.args)
            pre = new Date();
         }
    }
}

Session 和 Cookie 

Cookie

服务器通过Set-Cookie头给客户端一串字符串

客户端每次访问相同域名的网页时,必须带上这段字符串

客户端要在一段时间内保存这个Cookie

Cookie默认在用户关闭页面后就失效,后台代码可以任意设置Cookie的过期时间

大小大概在4kb

Session

将SessionID(随机数)通过Cookie发给客户端

客户端访问服务器时,服务器读取SessionID

服务器有一块内存(哈希表)保存了所有session

通过SessionID可以得到对应用户的隐私信息,如:id email

这块内存(哈希表)就是服务器上的所有session

一般来说,Session是基于Cookie来实现的

null和undefined的区别

null是一个被分配的值,设置为null的变量意味着其无值。而undefined则是某个变量虽然声明了变量,但是还没进行过任何赋值。

继承的方式

原型继承(组合继承)

function parent(value){
    this.val = value
}
parent.prototype.getValue = function(){
    console.log(this.val)
}
function child(){
    parent.call(this,value)
}
child.prototyppe = new parent()
const child = new child(1)
child.getValue() //1
child instanceof parent

此继承方式是在子函数中通过parent.call(this)继承父类属性,然后改变自雷的原型为new parent来继承父类的函数

这种继承方式优点在于构造函数可以传参,不会与父类引用属性共享,可以复用父类的函数,但是也存在一个缺点就是在继承父类函数的时候调用了父类构造函数,导致子类的原型上多了不需要的父类属性,存在内存上的浪费。

寄生继承

function Parent(value) {
  this.val = value
}
Parent.prototype.getValue = function() {
  console.log(this.val)
}

function Child(value) {
  Parent.call(this, value)
}
Child.prototype = Object.create(Parent.prototype, {
  constructor: {
    value: Child,
    enumerable: false,
    writable: true,
    configurable: true
  }
})

const child = new Child(1)

child.getValue() // 1
child instanceof Parent // true

继承实现的核心就是将父类的原型赋值给了子类,并且将构造函数设置为子类,这样既解决了无用的父类属性问题,还能正确的找到子类的构造函数。

Class 继承

class Parent {
  constructor(value) {
    this.val = value
  }
  getValue() {
    console.log(this.val)
  }
}
class Child extends Parent {
  constructor(value) {
    super(value)
  }
}
let child = new Child(1)
child.getValue() // 1
child instanceof Parent // true

class 实现继承的核心在于使用 extends 表明继承自哪个父类,并且在子类构造函数中必须调用 super,因为这段代码可以看成 Parent.call(this, value)。 class的本质就是函数

map ,filter,reduce

map

map 作用是遍历原数组,将每个元素拿出来做一些变换然后放入到新的数组中,生成一个新数组。

[1, 2, 3].map(v => v + 1) // -> [2, 3, 4]
map 的回调函数接受三个参数,分别是当前索引元素,索引,原数组

filter

filter 的作用也是生成一个新数组,在遍历数组的时候将返回值为 true 的元素放入新数组,我们可以利用这个函数删除一些不需要的元素

let array = [1, 2, 4, 6]
let newArray = array.filter(item => item !== 6)
console.log(newArray) // [1, 2, 4]

reduce

reduce 可以将数组中的元素通过回调函数最终转换为一个值。

array.reduce(function(total, currentValue, currentIndex, arr), initialValue)
callback (执行数组中每个值的函数,包含四个参数)

    1、total(上一次调用回调返回的值,或者是提供的初始值(initialValue))    2、currentValue (数组中当前被处理的元素)
    3、currentIndex(当前元素在数组中的索引)    4、arr(调用 reduce 的数组)
initialValue (作为第一次调用 callback 的第一个参数。)

var arr = [1, 2, 3, 4];
var sum = arr.reduce(function(prev, cur, index, arr) {
    console.log(prev, cur, index);
    return prev + cur;
})
console.log(arr, sum);

打印结果:1 2 13 3 26 4 3[1, 2, 3, 4] 10

可以 看出 index是从1开始的,第一次的prev的值是数组的第一个值。数组长度是4,但是reduce函数循环3次

var  arr = [1, 2, 3, 4];
var sum = arr.reduce(function(prev, cur, index, arr) {
    console.log(prev, cur, index);
    return prev + cur;
},0) //注意这里设置了初始值
console.log(arr, sum);

0 1 01 2 13 3 26 4 3[1, 2, 3, 4] 10

这个例子index是从0开始的,第一次的prev的值是我们设置的初始值0,数组长度是4,reduce函数循环4次。

结论:如果没有提供initialValue,reduce 会从索引1的地方开始执行 callback 方法,跳过第一个索引。如果提供initialValue,从索引0开始。

如何数组为空就会报错

var  arr = [];
var sum = arr.reduce(function(prev, cur, index, arr) {
    console.log(prev, cur, index);
    return prev + cur;
})
//报错,"TypeError: Reduce of empty array with no initial value"

但是要是我们设置了初始值就不会报错

var  arr = [];
var sum = arr.reduce(function(prev, cur, index, arr) {
    console.log(prev, cur, index);
    return prev + cur;
},0)
console.log(arr, sum); // [] 0

reduce的简单用法

1、数组求和,乘积

var  arr = [1, 2, 3, 4];
var sum = arr.reduce((x,y)=>x+y)
var mul = arr.reduce((x,y)=>x*y)
console.log( sum ); //求和,10
console.log( mul ); //求乘积,24

reduce的高级用法

1、计算数组中每个元素出现的次数

let names = ['Alice', 'Bob', 'Tiff', 'Bruce', 'Alice'];

let nameNum = names.reduce((pre,cur)=>{
  if(cur in pre){
    pre[cur]++
  }else{
    pre[cur] = 1 
  }
  return pre
},{})
console.log(nameNum); //{Alice: 2, Bob: 1, Tiff: 1, Bruce: 1}

2、数组去重

let arr = [1,2,3,4,4,1]
let newArr = arr.reduce((pre,cur)=>{
    if(!pre.includes(cur)){
      return pre.concat(cur)
    }else{
      return pre
    }
},[])
console.log(newArr);// [1, 2, 3, 4]

3、将二维数组转化为一维

let arr = [[0, 1], [2, 3], [4, 5]]
let newArr = arr.reduce((pre,cur)=>{
    return pre.concat(cur)
},[])
console.log(newArr); // [0, 1, 2, 3, 4, 5]

4、将多维数组转化为一维

let arr = [[0, 1], [2, 3], [4,[5,6,7]]]
const newArr = function(arr){
   return arr.reduce((pre,cur)=>pre.concat(Array.isArray(cur)?newArr(cur):cur),[])
}
console.log(newArr(arr)); //[0, 1, 2, 3, 4, 5, 6, 7]

5、对象里的属性求和

var result = [
    {
        subject: 'math',
        score: 10
    },
    {
        subject: 'chinese',
        score: 20
    },
    {
        subject: 'english',
        score: 30
    }
];

var sum = result.reduce(function(prev, cur) {
    return prev + cur.score;
}, 0);console.log(sum) //60

Event Loop 执行顺序

  • 首先执行同步代码,这属于宏任务
  • 当执行完所有同步代码后,执行栈为空,查询是否有异步代码需要执行
  • 执行所有微任务
  • 当执行完所有微任务后,如有必要会渲染页面
  • 然后开始下一轮 Event Loop,执行宏任务中的异步代码,也就是 setTimeout 中的回调函数

微任务包括 process.nextTickpromiseMutationObserver,其中 process.nextTick 为 Node 独有。

宏任务包括 scriptsetTimeoutsetIntervalsetImmediateI/OUI rendering