总结一下最近前端面试被问到的题目吧

2022-09-24 08:40:20 浏览数 (1)

浏览器乱码的原因是什么?如何解决?

产生乱码的原因:

  • 网页源代码是gbk的编码,而内容中的中文字是utf-8编码的,这样浏览器打开即会出现html乱码,反之也会出现乱码;
  • html网页编码是gbk,而程序从数据库中调出呈现是utf-8编码的内容也会造成编码乱码;
  • 浏览器不能自动检测网页编码,造成网页乱码。

解决办法:

  • 使用软件编辑HTML网页内容;
  • 如果网页设置编码是gbk,而数据库储存数据编码格式是UTF-8,此时需要程序查询数据库数据显示数据前进程序转码;
  • 如果浏览器浏览时候出现网页乱码,在浏览器中找到转换编码的菜单进行转换。

前端进阶面试题详细解答

AJAX

代码语言:javascript复制
const getJSON = function(url) {
    return new Promise((resolve, reject) => {
        const xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP');
        xhr.open('GET', url, false);
        xhr.setRequestHeader('Accept', 'application/json');
        xhr.onreadystatechange = function() {
            if (xhr.readyState !== 4) return;
            if (xhr.status === 200 || xhr.status === 304) {
                resolve(xhr.responseText);
            } else {
                reject(new Error(xhr.responseText));
            }
        }
        xhr.send();
    })
}

实现数组原型方法

forEach

代码语言:javascript复制
Array.prototype.forEach2 = function(callback, thisArg) {
    if (this == null) {
        throw new TypeError('this is null or not defined')
    }
    if (typeof callback !== "function") {
        throw new TypeError(callback   ' is not a function')
    }
    const O = Object(this)  // this 就是当前的数组
    const len = O.length >>> 0  // 后面有解释
    let k = 0
    while (k < len) {
        if (k in O) {
            callback.call(thisArg, O[k], k, O);
        }
        k  ;
    }
}

O.length >>> 0 是什么操作?就是无符号右移 0 位,那有什么意义嘛?就是为了保证转换后的值为正整数。其实底层做了 2 层转换,第一是非 number 转成 number 类型,第二是将 number 转成 Uint32 类型

map

基于 forEach 的实现能够很容易写出 map 的实现:

代码语言:shell复制
- Array.prototype.forEach2 = function(callback, thisArg) {
  Array.prototype.map2 = function(callback, thisArg) {
    if (this == null) {
        throw new TypeError('this is null or not defined')
    }
    if (typeof callback !== "function") {
        throw new TypeError(callback   ' is not a function')
    }
    const O = Object(this)
    const len = O.length >>> 0
-   let k = 0
    let k = 0, res = []
    while (k < len) {
        if (k in O) {
-           callback.call(thisArg, O[k], k, O);
            res[k] = callback.call(thisArg, O[k], k, O);
        }
        k  ;
    }
    return res
}

filter

同样,基于 forEach 的实现能够很容易写出 filter 的实现:

代码语言:shell复制
- Array.prototype.forEach2 = function(callback, thisArg) {
  Array.prototype.filter2 = function(callback, thisArg) {
    if (this == null) {
        throw new TypeError('this is null or not defined')
    }
    if (typeof callback !== "function") {
        throw new TypeError(callback   ' is not a function')
    }
    const O = Object(this)
    const len = O.length >>> 0
-   let k = 0
    let k = 0, res = []
    while (k < len) {
        if (k in O) {
-           callback.call(thisArg, O[k], k, O);
            if (callback.call(thisArg, O[k], k, O)) {
                res.push(O[k])                
            }
        }
        k  ;
    }
    return res
}

some

同样,基于 forEach 的实现能够很容易写出 some 的实现:

代码语言:shell复制
- Array.prototype.forEach2 = function(callback, thisArg) {
  Array.prototype.some2 = function(callback, thisArg) {
    if (this == null) {
        throw new TypeError('this is null or not defined')
    }
    if (typeof callback !== "function") {
        throw new TypeError(callback   ' is not a function')
    }
    const O = Object(this)
    const len = O.length >>> 0
    let k = 0
    while (k < len) {
        if (k in O) {
-           callback.call(thisArg, O[k], k, O);
            if (callback.call(thisArg, O[k], k, O)) {
                return true
            }
        }
        k  ;
    }
    return false
}

reduce

代码语言:javascript复制
Array.prototype.reduce2 = function(callback, initialValue) {
    if (this == null) {
        throw new TypeError('this is null or not defined')
    }
    if (typeof callback !== "function") {
        throw new TypeError(callback   ' is not a function')
    }
    const O = Object(this)
    const len = O.length >>> 0
    let k = 0, acc

    if (arguments.length > 1) {
        acc = initialValue
    } else {
        // 没传入初始值的时候,取数组中第一个非 empty 的值为初始值
        while (k < len && !(k in O)) {
            k  
        }
        if (k > len) {
            throw new TypeError( 'Reduce of empty array with no initial value' );
        }
        acc = O[k  ]
    }
    while (k < len) {
        if (k in O) {
            acc = callback(acc, O[k], k, O)
        }
        k  
    }
    return acc
}

代码输出结果

代码语言:javascript复制
function Person(name) {
    this.name = name
}
var p2 = new Person('king');
console.log(p2.__proto__) //Person.prototype
console.log(p2.__proto__.__proto__) //Object.prototype
console.log(p2.__proto__.__proto__.__proto__) // null
console.log(p2.__proto__.__proto__.__proto__.__proto__)//null后面没有了,报错
console.log(p2.__proto__.__proto__.__proto__.__proto__.__proto__)//null后面没有了,报错
console.log(p2.constructor)//Person
console.log(p2.prototype)//undefined p2是实例,没有prototype属性
console.log(Person.constructor)//Function 一个空函数
console.log(Person.prototype)//打印出Person.prototype这个对象里所有的方法和属性
console.log(Person.prototype.constructor)//Person
console.log(Person.prototype.__proto__)// Object.prototype
console.log(Person.__proto__) //Function.prototype
console.log(Function.prototype.__proto__)//Object.prototype
console.log(Function.__proto__)//Function.prototype
console.log(Object.__proto__)//Function.prototype
console.log(Object.prototype.__proto__)//null

这道义题目考察原型、原型链的基础,记住就可以了。

事件委托的使用场景

场景:给页面的所有的a标签添加click事件,代码如下:

代码语言:javascript复制
document.addEventListener("click", function(e) {
    if (e.target.nodeName == "A")
        console.log("a");
}, false);

但是这些a标签可能包含一些像span、img等元素,如果点击到了这些a标签中的元素,就不会触发click事件,因为事件绑定上在a标签元素上,而触发这些内部的元素时,e.target指向的是触发click事件的元素(span、img等其他元素)。

这种情况下就可以使用事件委托来处理,将事件绑定在a标签的内部元素上,当点击它的时候,就会逐级向上查找,知道找到a标签为止,代码如下:

代码语言:javascript复制
document.addEventListener("click", function(e) {
    var node = e.target;
    while (node.parentNode.nodeName != "BODY") {
        if (node.nodeName == "A") {
            console.log("a");
            break;
        }
        node = node.parentNode;
    }
}, false);

原型链

代码语言:javascript复制
原型链实际上在上面原型的问题中就有涉及到,在原型的继承中,我们继承来多个原型,这里再提一下实现完美
继承的方案,通过借助寄生组合继承,PersonB.prototype = Object.create(PersonA.prototype)
这是当我们实例化PersonB得到实例化对象,访问实例化对象的属性时会触发get方法,它会先在自身属性上查
找,如果没有这个属性,就会去__proto__中查找,一层层向上直到查找到顶层对象Object,这个查找的过程
就是原型链来。

代码输出结果

代码语言:javascript复制
function foo(something){
    this.a = something
}

var obj1 = {
    foo: foo
}

var obj2 = {}

obj1.foo(2); 
console.log(obj1.a); // 2

obj1.foo.call(obj2, 3);
console.log(obj2.a); // 3

var bar = new obj1.foo(4)
console.log(obj1.a); // 2
console.log(bar.a); // 4

输出结果: 2 3 2 4

解析:

  1. 首先执行obj1.foo(2); 会在obj中添加a属性,其值为2。之后执行obj1.a,a是右obj1调用的,所以this指向obj,打印出2;
  2. 执行 obj1.foo.call(obj2, 3) 时,会将foo的this指向obj2,后面就和上面一样了,所以会打印出3;
  3. obj1.a会打印出2;
  4. 最后就是考察this绑定的优先级了,new 绑定是比隐式绑定优先级高,所以会输出4。

代码输出结果

代码语言:javascript复制
console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})

输出结果如下:

代码语言:javascript复制
1
7
6
8
2
4
3
5
9
11
10
12

(1)第一轮事件循环流程分析如下:

  • 整体script作为第一个宏任务进入主线程,遇到console.log,输出1。
  • 遇到setTimeout,其回调函数被分发到宏任务Event Queue中。暂且记为setTimeout1
  • 遇到process.nextTick(),其回调函数被分发到微任务Event Queue中。记为process1
  • 遇到Promisenew Promise直接执行,输出7。then被分发到微任务Event Queue中。记为then1
  • 又遇到了setTimeout,其回调函数被分发到宏任务Event Queue中,记为setTimeout2

宏任务Event Queue

微任务Event Queue

setTimeout1

process1

setTimeout2

then1

上表是第一轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了1和7。发现了process1then1两个微任务:

  • 执行process1,输出6。
  • 执行then1,输出8。

第一轮事件循环正式结束,这一轮的结果是输出1,7,6,8。

(2)第二轮时间循环从**setTimeout1**宏任务开始:

  • 首先输出2。接下来遇到了process.nextTick(),同样将其分发到微任务Event Queue中,记为process2
  • new Promise立即执行输出4,then也分发到微任务Event Queue中,记为then2

宏任务Event Queue

微任务Event Queue

setTimeout2

process2

then2

第二轮事件循环宏任务结束,发现有process2then2两个微任务可以执行:

  • 输出3。
  • 输出5。

第二轮事件循环结束,第二轮输出2,4,3,5。

(3)第三轮事件循环开始,此时只剩setTimeout2了,执行。

  • 直接输出9。
  • process.nextTick()分发到微任务Event Queue中。记为process3
  • 直接执行new Promise,输出11。
  • then分发到微任务Event Queue中,记为then3

宏任务Event Queue

微任务Event Queue

process3

then3

第三轮事件循环宏任务执行结束,执行两个微任务process3then3

  • 输出10。
  • 输出12。

第三轮事件循环结束,第三轮输出9,11,10,12。

整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。

代码输出结果

代码语言:javascript复制
function runAsync (x) {
    const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
    return p
}

Promise.all([runAsync(1), runAsync(2), runAsync(3)]).then(res => console.log(res))

输出结果如下:

代码语言:javascript复制
1
2
3
[1, 2, 3]

首先,定义了一个Promise,来异步执行函数runAsync,该函数传入一个值x,然后间隔一秒后打印出这个x。

之后再使用Promise.all来执行这个函数,执行的时候,看到一秒之后输出了1,2,3,同时输出了数组1, 2, 3,三个函数是同步执行的,并且在一个回调函数中返回了所有的结果。并且结果和函数的执行顺序是一致的。

TCP的可靠传输机制

TCP 的可靠传输机制是基于连续 ARQ 协议和滑动窗口协议的。

TCP 协议在发送方维持了一个发送窗口,发送窗口以前的报文段是已经发送并确认了的报文段,发送窗口中包含了已经发送但 未确认的报文段和允许发送但还未发送的报文段,发送窗口以后的报文段是缓存中还不允许发送的报文段。当发送方向接收方发 送报文时,会依次发送窗口内的所有报文段,并且设置一个定时器,这个定时器可以理解为是最早发送但未收到确认的报文段。 如果在定时器的时间内收到某一个报文段的确认回答,则滑动窗口,将窗口的首部向后滑动到确认报文段的后一个位置,此时如 果还有已发送但没有确认的报文段,则重新设置定时器,如果没有了则关闭定时器。如果定时器超时,则重新发送所有已经发送 但还未收到确认的报文段,并将超时的间隔设置为以前的两倍。当发送方收到接收方的三个冗余的确认应答后,这是一种指示, 说明该报文段以后的报文段很有可能发生丢失了,那么发送方会启用快速重传的机制,就是当前定时器结束前,发送所有的已发 送但确认的报文段。

接收方使用的是累计确认的机制,对于所有按序到达的报文段,接收方返回一个报文段的肯定回答。如果收到了一个乱序的报文 段,那么接方会直接丢弃,并返回一个最近的按序到达的报文段的肯定回答。使用累计确认保证了返回的确认号之前的报文段都 已经按序到达了,所以发送窗口可以移动到已确认报文段的后面。

发送窗口的大小是变化的,它是由接收窗口剩余大小和网络中拥塞程度来决定的,TCP 就是通过控制发送窗口的长度来控制报文 段的发送速率。

但是 TCP 协议并不完全和滑动窗口协议相同,因为许多的 TCP 实现会将失序的报文段给缓存起来,并且发生重传时,只会重 传一个报文段,因此 TCP 协议的可靠传输机制更像是窗口滑动协议和选择重传协议的一个混合体。

组件之间的传值有几种方式

代码语言:javascript复制
1、父传子
2、子传父
3、eventbus
4、ref/$refs
5、$parent/$children
6、$attrs/$listeners
7、依赖注入(provide/inject)

实现 JSONP 跨域

JSONP 核心原理script 标签不受同源策略约束,所以可以用来进行跨域请求,优点是兼容性好,但是只能用于 GET 请求;

实现

代码语言:javascript复制
const jsonp = (url, params, callbackName) => {
    const generateUrl = () => {
        let dataSrc = "";
        for(let key in params) {
            if(params.hasOwnProperty(key)) {
                dataSrc  = `${key}=${params[key]}&`
            }
        }
        dataSrc  = `callback=${callbackName}`;
        return `${url}?${dataSrc}`;
    }
    return new Promise((resolve, reject) => {
        const scriptEle = document.createElement('script');
        scriptEle.src = generateUrl();
        document.body.appendChild(scriptEle);
        window[callbackName] = data => {
            resolve(data);
            document.removeChild(scriptEle);
        }
    });
}

原型链指向

代码语言:javascript复制
p.__proto__  // Person.prototype
Person.prototype.__proto__  // Object.prototype
p.__proto__.__proto__ //Object.prototype
p.__proto__.constructor.prototype.__proto__ // Object.prototype
Person.prototype.constructor.prototype.__proto__ // Object.prototype
p1.__proto__.constructor // Person
Person.prototype.constructor  // Person

ES6模块与CommonJS模块有什么异同?

ES6 Module和CommonJS模块的区别:

  • CommonJS是对模块的浅拷⻉,ES6 Module是对模块的引⽤,即ES6 Module只存只读,不能改变其值,也就是指针指向不能变,类似const;
  • import的接⼝是read-only(只读状态),不能修改其变量值。 即不能修改其变量的指针指向,但可以改变变量内部指针指向,可以对commonJS对重新赋值(改变指针指向),但是对ES6 Module赋值会编译报错。

ES6 Module和CommonJS模块的共同点:

  • CommonJS和ES6 Module都可以对引⼊的对象进⾏赋值,即对对象内部属性的值进⾏改变。

进程之前的通信方式

(1)管道通信

管道是一种最基本的进程间通信机制。管道就是操作系统在内核中开辟的一段缓冲区,进程1可以将需要交互的数据拷贝到这段缓冲区,进程2就可以读取了。

管道的特点:

  • 只能单向通信
  • 只能血缘关系的进程进行通信
  • 依赖于文件系统
  • 生命周期随进程
  • 面向字节流的服务
  • 管道内部提供了同步机制

(2)消息队列通信

消息队列就是一个消息的列表。用户可以在消息队列中添加消息、读取消息等。消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。 每个数据块都被认为含有一个类型,接收进程可以独立地接收含有不同类型的数据结构。可以通过发送消息来避免命名管道的同步和阻塞问题。但是消息队列与命名管道一样,每个数据块都有一个最大长度的限制。

使用消息队列进行进程间通信,可能会收到数据块最大长度的限制约束等,这也是这种通信方式的缺点。如果频繁的发生进程间的通信行为,那么进程需要频繁地读取队列中的数据到内存,相当于间接地从一个进程拷贝到另一个进程,这需要花费时间。

(3)信号量通信

共享内存最大的问题就是多进程竞争内存的问题,就像类似于线程安全问题。我们可以使用信号量来解决这个问题。信号量的本质就是一个计数器,用来实现进程之间的互斥与同步。例如信号量的初始值是 1,然后 a 进程来访问内存1的时候,我们就把信号量的值设为 0,然后进程b 也要来访问内存1的时候,看到信号量的值为 0 就知道已经有进程在访问内存1了,这个时候进程 b 就会访问不了内存1。所以说,信号量也是进程之间的一种通信方式。

(4)信号通信

信号(Signals )是Unix系统中使用的最古老的进程间通信的方法之一。操作系统通过信号来通知进程系统中发生了某种预先规定好的事件(一组事件中的一个),它也是用户进程之间通信和同步的一种原始机制。

(5)共享内存通信

共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问(使多个进程可以访问同一块内存空间)。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。

(6)套接字通信

上面说的共享内存、管道、信号量、消息队列,他们都是多个进程在一台主机之间的通信,那两个相隔几千里的进程能够进行通信吗?答是必须的,这个时候 Socket 这家伙就派上用场了,例如我们平时通过浏览器发起一个 http 请求,然后服务器给你返回对应的数据,这种就是采用 Socket 的通信方式了。

什么是 DOM 和 BOM?

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

async/await 如何捕获异常

代码语言:javascript复制
async function fn(){
    try{
        let a = await Promise.reject('error')
    }catch(error){
        console.log(error)
    }
}

进程和线程的区别

  • 进程可以看做独立应用,线程不能
  • 资源:进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位);线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)。
  • 通信方面:线程间可以通过直接共享同一进程中的资源,而进程通信需要借助 进程间通信。
  • 调度:进程切换比线程切换的开销要大。线程是CPU调度的基本单位,线程的切换不会引起进程切换,但某个进程中的线程切换到另一个进程中的线程时,会引起进程切换。
  • 系统开销:由于创建或撤销进程时,系统都要为之分配或回收资源,如内存、I/O 等,其开销远大于创建或撤销线程时的开销。同理,在进行进程切换时,涉及当前执行进程 CPU 环境还有各种各样状态的保存及新调度进程状态的设置,而线程切换时只需保存和设置少量寄存器内容,开销较小。

对 CSSSprites 的理解

CSSSprites(精灵图),将一个页面涉及到的所有图片都包含到一张大图中去,然后利用CSS的 background-image,background-repeat,background-position属性的组合进行背景定位。

优点:

  • 利用CSS Sprites能很好地减少网页的http请求,从而大大提高了页面的性能,这是CSS Sprites最大的优点;
  • CSS Sprites能减少图片的字节,把3张图片合并成1张图片的字节总是小于这3张图片的字节总和。

缺点:

  • 在图片合并时,要把多张图片有序的、合理的合并成一张图片,还要留好足够的空间,防止板块内出现不必要的背景。在宽屏及高分辨率下的自适应页面,如果背景不够宽,很容易出现背景断裂;
  • CSSSprites在开发的时候相对来说有点麻烦,需要借助photoshop或其他工具来对每个背景单元测量其准确的位置。
  • 维护方面:CSS Sprites在维护的时候比较麻烦,页面背景有少许改动时,就要改这张合并的图片,无需改的地方尽量不要动,这样避免改动更多的CSS,如果在原来的地方放不下,又只能(最好)往下加图片,这样图片的字节就增加了,还要改动CSS

如何使用for...of遍历对象

for…of是作为ES6新增的遍历方式,允许遍历一个含有iterator接口的数据结构(数组、对象等)并且返回各项的值,普通的对象用for..of遍历是会报错的。

如果需要遍历的对象是类数组对象,用Array.from转成数组即可。

代码语言:javascript复制
var obj = {
    0:'one',
    1:'two',
    length: 2
};
obj = Array.from(obj);
for(var k of obj){
    console.log(k)
}

如果不是类数组对象,就给对象添加一个Symbol.iterator属性,并指向一个迭代器即可。

代码语言:javascript复制
//方法一:
var obj = {
    a:1,
    b:2,
    c:3
};

obj[Symbol.iterator] = function(){
    var keys = Object.keys(this);
    var count = 0;
    return {
        next(){
            if(count<keys.length){
                return {value: obj[keys[count  ]],done:false};
            }else{
                return {value:undefined,done:true};
            }
        }
    }
};

for(var k of obj){
    console.log(k);
}


// 方法二
var obj = {
    a:1,
    b:2,
    c:3
};
obj[Symbol.iterator] = function*(){
    var keys = Object.keys(obj);
    for(var k of keys){
        yield [k,obj[k]]
    }
};

for(var [k,v] of obj){
    console.log(k,v);
}

UDP协议为什么不可靠?

UDP在传输数据之前不需要先建立连接,远地主机的运输层在接收到UDP报文后,不需要确认,提供不可靠交付。总结就以下四点:

  • 不保证消息交付:不确认,不重传,无超时
  • 不保证交付顺序:不设置包序号,不重排,不会发生队首阻塞
  • 不跟踪连接状态:不必建立连接或重启状态机
  • 不进行拥塞控制:不内置客户端或网络反馈机制

0 人点赞