发布于 2024 年 2 月 26 日,星期一
该标题聚焦于前端开发中的JavaScript面试题,旨在帮助开发者准备面试。JavaScript作为前端开发的核心技术,其面试题通常涵盖基础语法、高级特性、异步编程、DOM操作、性能优化等多个方面。通过解析这些题目,文章不仅帮助读者巩固JavaScript知识,还能提升其在实际项目中的应用能力。面试题的解析过程往往涉及对语言特性的深入理解,以及对常见问题的解决方案的探讨,从而帮助开发者更好地应对面试挑战,提升职业竞争力。
系列首发于公众号『非同质前端札记』https://mp.weixin.qq.com/s?__biz=MzkyOTI2MzE0MQ==&mid=2247485576&idx=1&sn=5ddfe93f427f05f5d126dead859d0dc8&chksm=c20d73c2f57afad4bbea380dfa1bcc15367a4cc06bf5dd0603100e8bd7bb317009fa65442cdb&token=1071012447&lang=zh_CN#rd ,若不想错过更多精彩内容,请“星标”一下,敬请关注公众号最新消息。
use strict
1.函数调用模式:当一个函数不是一个对象的属性时,直接作为函数来调用时, 严格模式下指向 undefined, 非严格模式下,this 指向全局对象。
// 严格模式'use strict';var name = 'window';var doSth = function () { console.log(typeof this === 'undefined'); console.log(this.name);};doSth(); // true,// 报错,因为this是undefined// 非严格模式let name2 = 'window2';let doSth2 = function () { console.log(this === window); console.log(this.name2);};doSth2(); // true, undefined
2.方法调用模式:如果一个函数作为一个对象的方法来调用时,this 指向当前这个对象
var name = 'window';var doSth = function () { console.log(this.name);};var student = { name: 'lc', doSth: doSth, other: { name: 'other', doSth: doSth, },};student.doSth(); // 'lc'student.other.doSth(); // 'other'// 用call类比则为:student.doSth.call(student);// 用call类比则为:student.other.doSth.call(student.other);
3.构造器调用模式:如果一个函数通过 new 调用时,函数执行前会新创建一个对象,this 指向这个新创建的对象。
var Obj = function (p) { this.p = p;};var o = new Obj('Hello World!');o.p; // "Hello World!"
4.apply, call, bind 模式:显示更改 this 指向,严格模式下,指向绑定的第一个参数,非严格模式下,null 和 undefined 指向全局对象(浏览器中是 window),其余指向被 new Object() 包裹的对象。
- apply: apply(this 绑定的对象,参数数组) func.apply(thisValue, [arg1, arg2, ...])
function f(x, y) { console.log(x + y);}f.call(null, 1, 1); // 2f.apply(null, [1, 1]); // 2
func.call(thisValue, arg1, arg2, ...)
var doSth = function (name) { console.log(this); console.log(name);};doSth.call(2, 'lc'); // Number{2}, 'lc'var doSth2 = function (name) { 'use strict'; console.log(this); console.log(name);};doSth2.call(2, 'lc'); // 2, 'lc'
func.bind(thisValue)
var counter = { count: 0, inc: function () { this.count++; },};var obj = { count: 100,};var func = counter.inc.bind(obj);func();obj.count; // 101// eg2:var add = function (x, y) { return x * this.m + y * this.n;};var obj = { m: 2, n: 2,};var newAdd = add.bind(obj, 5);newAdd(5); // 20
构造器模式 > apply, call, bind > 方法调用模式 > 函数调用模式
解析 URL
: 分析所使用的协议和请求资源的路径缓存判断
:判断当前请求资源是否存在缓存中,如果在缓存中且没有失效,直接返回,否则向服务器发起请求。DNS 解析
:从 URL 中解析出 IP 地址,然后判断本地是否有缓存,有则使用,没有则向本地 DNS 服务器发起请求,先判断是否存在缓存,没有则向跟域名服务器发起请求,最终获得域名 IP 地址后,本地 DNS 服务器再将这个 IP 返回给用户。获取 MAC 地址
:获取到 IP 地址后,数据传输还需目的主机的 MAC 地址,应用层下发数据给传输层,TCP 协议会执行端口号和目的端口号,然后下发数据给网络层。TCP 三次握手
:首先客户端向服务器发送一个 SYN 连接请求报文段和一个随机序号,服务端接收到请求后向服务器端发送一个 SYN ACK 报文段,确认连接请求,并且也向客户端发送一个随机序号。客户端接收服务器的确认应答后,进入连接建立的状态,同时向服务器也发送一个 ACK 确认报文段,服务器端接收到确认后,也进入连接建立状态,此时双方的连接就建立起来了。HTTPS 握手
:如果使用的是 HTTPS 协议,在通信前还存在 TLS 的一个四次握手的过程。首先由客户端向服务器端发送使用的协议的版本号、一个随机数和可以使用的加密方法。服务器端收到后,确认加密的方法,也向客户端发送一个随机数和自己的数字证书。客户端收到后,首先检查数字证书是否有效,如果有效,则再生成一个随机数,并使用证书中的公钥对随机数加密,然后发送给服务器端,并且还会提供一个前面所有内容的 hash 值供服务器端检验。服务器端接收后,使用自己的私钥对数据解密,同时向客户端发送一个前面所有内容的 hash 值供客户端检验。这个时候双方都有了三个随机数,按照之前所约定的加密方法,使用这三个随机数生成一把秘钥,以后双方通信前,就使用这个秘钥对数据进行加密后再传输。返回数据
:当页面请求发送到服务器端后,服务器端会返回一个 html 文件作为响应,浏览器接收到响应后,开始对 html 文件进行解析,开始页面的渲染过程。页面渲染
: 浏览器首先会根据 html 文件构建 DOM 树,根据解析到的 css 文件构建 CSSOM 树,如果遇到 script 标签,则判端是否含有 defer 或者 async 属性,要不然 script 的加载和执行会造成页面的渲染的阻塞。当 DOM 树和 CSSOM 树建立好后,根据它们来构建渲染树。渲染树构建好后,会根据渲染树来进行布局。布局完成后,最后调用 paint 方法对页面进行绘制。这个时候整个页面就显示出来了。TCP四次挥手
: 最后一步是 TCP 断开连接的四次挥手过程。若客户端认为数据发送完成,则它需要向服务端发送连接释放请求。服务端收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,此时表明客户端到服务端的连接已经释放,不再接收客户端发的数据了。但是因为 TCP 连接是双向的,所以服务端仍旧可以发送数据给客户端。服务端如果此时还有没发完的数据会继续发送,完毕后会向客户端发送连接释放请求,然后服务端便进入 LAST-ACK 状态。客户端收到释放请求后,向服务端发送确认应答,此时客户端进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有服务端的重发请求的话,就进入 CLOSED 状态。当服务端收到确认应答后,也便进入 CLOSED 状态。1.工厂模式:通过一个函数来封装创建对象的过程。只是简单的封装了复用代码,没有建立起对象与类型之间的关系。
// 工厂函数 通过改变参数多次使用function Person(name, age) { const obj = {}; obj.name = name; obj.age = age; return obj;}const person = Person('dz', 23);const person1 = Person('dz1', 24);console.log(person instanceof Person); // -> falseconsole.log(person1.__proto__ == person.__proto_); // -> false
2.构造函数模式:通过 new 关键字来创建。但会造成不必要的函数对象的创建。因为在 js 中函数也是一个对象,因此如果对象属性中如果包含函数的话,那就会每次都新建一个对象,浪费了不必要的内存空间,因为在函数中所有的实例都可以通用的。
// 内置构造函数创建对象function Person(name, age) { this.name = name; this.age = age; this.sayname = () => { console.log(this.name); };}const p1 = new Person('dz', 23);const p2 = new Person('dz1', 24);console.log(p1 instanceof Person, p2 instanceof Person); // --> true true
3.原型模式:因为每一个函数都有一个 prototype 属性,这个属性是一个对象,它包含了通过构造函数创建的所有实例都能共享的属性和方法。
function Person() {}Person.prototype.name = 'Nike';Person.prototype.age = 20;Person.prototype.jbo = 'teacher';Person.prototype.sayName = function () { alert(this.name);};var person1 = new Person();person1.sayName();
4.构造函数 + 原型模式:每个实例拥有自己的属性和方法, 以及共享相同的方法, 用的较多一种模式
function Person(name, age) { this.name = name; this.age = age;}Person.prototype.sayname = () => { console.log(this.name);};const p1 = new Person('dz', 23);const p2 = new Person('dz1', 24);console.log(p1.name, p2.name); // dz dz1
5.动态原型模式:这一种模式将原型方法赋值的创建过程移动到了构造函数的内部,通过对属性是否存在的判断,可以实现仅在第一次调用函数时对原型对象赋值一次的效果。这一种方式很好地对上面的混合模式进行了封装。
function Person(name, age) { this.name = name; this.age = age; if (typeof this.sayname != 'function') { Person.prototype.sayname = () => { console.log(this.name); }; }}const p1 = new Person('dz', 23);console.log(p1.sayname); // -> dz
6.寄生构造函数模式:
- 基本思想:这一种模式和工厂模式的实现基本相同。它主要是基于一个已有的类型,在实例化时对实例化的对象进行扩展。这样既不用修改原来的构造函数,也达到了扩展对象的目的。它的一个缺点和工厂模式一样,无法实现对象的识别。
function SpecialArray() { var array = new Array(); array.push.apply(array, arguments); array.toPipedString = function () { return this.join('|'); }; return array;}var colors = new SpecialArray('red', 'green', 'pink');alert(colors.toPipedString()); // red|green|pinkalert(colors instanceof SpecialArray); // false
7.class 创建对象:constructor 是构造方法,类似构造函数, 定义这个方法里面的内容都是实例自身的属性和方法, 不会被其他实例共享, 而写在外面的 sayname 表示原型上的方法, 是会被共享的。
class Person { constructor(name, age) { // constructor构造函数 this.name = name this.age = age } sayname() { //原型上的 console.log(this.name) } static sayAge() { console.log(this.age) }}const per = new Person('dz', 23)per.sayname() // -> dzPerson.sayAge() // 23// static 表示静态,加了static的函数不会挂载到prototype 上,而是挂载到 class类 上, 类似于:Promise.resolve(...)Math.max(...)
无论是函数还是变量在那个位置上声明,都会被提升到函数之前,可保证变量声明前可访问而不会报错
变量提升的本质:js 引擎在代码执行前有一个解析的过程,会创建一个执行上下文,初始化一些代码执行所需要的参数。当访问一个变量时,会在当前执行上下文的作用域链中去查找,而作用域链的首端指向的是当前执行上下文的变量对象,这个变量对象是执行上下文的一个属性,它包含了函数的形参,所有函数和变量声明,这个对象是在代码解析的时候创建的。
代码执行过程:
为什么要进行变量提升?
a = 1;var a;console.log(a);
总结:
导致的问题:
var tmp = new Date();function fn() { console.log(tmp); if (false) { var tmp = 'hello world'; }}fn(); // undefined// 在这个函数中,原本是要打印出外层的tmp变量,但是因为变量提升的问题,内层定义的tmp被提到函数内部的最顶部,相当于覆盖了外层的tmp,所以打印结果为undefined。
for (var i = 0; i < 3; i++) { setTimeout(() => { console.log(i); }, 2000);} // 3 3 3for (let i = 0; i < 3; i++) { setTimeout(() => { console.log(i); }, 2000);} // 0 1 2// 由于遍历时定义的i会变量提升成为一个全局变量,在函数结束之后不会被销毁,所以,一直修改的是之前的定义的全局变量,所以第一个输出三次 3, 第二个输出 0 1 2。// 在 for 循环中,let 声明的变量会存在一个块级作用域的概念,使用 let 声明的迭代变量时,js 引擎会在后台为每一个迭代循环声明一个新的迭代变量,因此每次使用的 i 都是不同的。
区别 | var | let | const |
---|---|---|---|
是否有块级作用域{} 包裹 | ❌ | ✔️ | ✔️ |
是否存在变量提升 | ✔️ | ❌ | ❌ |
是否添加全局属性 | ✔️ | ❌ | ❌ |
能否重复声明变量 | ✔️ | ❌ | ❌ |
是否存在暂时性死区 | ❌ | ✔️ | ✔️ |
是否必须设置初始值 | ❌ | ❌ | ✔️ |
能否改变指针指向 | ✔️ | ✔️ | ❌ |
Q:(question)
R:(result)
A:(attention matters)
D:(detail info)
S:(summary)
Ana:(analysis)
T:(tips)