写点什么

这样回答前端面试题才能拿到 offer

作者:loveX001
  • 2023-01-04
    浙江
  • 本文字数:18531 字

    阅读完需:约 61 分钟

什么是文档的预解析?

Webkit 和 Firefox 都做了这个优化,当执行 JavaScript 脚本时,另一个线程解析剩下的文档,并加载后面需要通过网络加载的资源。这种方式可以使资源并行加载从而使整体速度更快。需要注意的是,预解析并不改变 DOM 树,它将这个工作留给主解析过程,自己只解析外部资源的引用,比如外部脚本、样式表及图片。

代码输出结果

async function async1 () {  console.log('async1 start');  await new Promise(resolve => {    console.log('promise1')    resolve('promise1 resolve')  }).then(res => console.log(res))  console.log('async1 success');  return 'async1 end'}console.log('srcipt start')async1().then(res => console.log(res))console.log('srcipt end')
复制代码


这里是对上面一题进行了改造,加上了 resolve。


输出结果如下:


script startasync1 startpromise1script endpromise1 resolveasync1 successasync1 end
复制代码

常见浏览器所用内核

(1) IE 浏览器内核:Trident 内核,也是俗称的 IE 内核;


(2) Chrome 浏览器内核:统称为 Chromium 内核或 Chrome 内核,以前是 Webkit 内核,现在是 Blink 内核;


(3) Firefox 浏览器内核:Gecko 内核,俗称 Firefox 内核;


(4) Safari 浏览器内核:Webkit 内核;


(5) Opera 浏览器内核:最初是自己的 Presto 内核,后来加入谷歌大军,从 Webkit 又到了 Blink 内核;


(6) 360 浏览器、猎豹浏览器内核:IE + Chrome 双内核;


(7) 搜狗、遨游、QQ 浏览器内核:Trident(兼容模式)+ Webkit(高速模式);


(8) 百度浏览器、世界之窗内核:IE 内核;


(9) 2345 浏览器内核:好像以前是 IE 内核,现在也是 IE + Chrome 双内核了;


(10)UC 浏览器内核:这个众口不一,UC 说是他们自己研发的 U3 内核,但好像还是基于 Webkit 和 Trident ,还有说是基于火狐内核。

如果一个构造函数,bind 了一个对象,用这个构造函数创建出的实例会继承这个对象的属性吗?为什么?

不会继承,因为根据 this 绑定四大规则,new 绑定的优先级高于 bind 显示绑定,通过 new 进行构造函数调用时,会创建一个新对象,这个新对象会代替 bind 的对象绑定,作为此函数的 this,并且在此函数没有返回对象的情况下,返回这个新建的对象

介绍下 promise 的特性、优缺点,内部是如何实现的,动手实现 Promise

1)Promise 基本特性


  • 1、Promise 有三种状态:pending(进行中)、fulfilled(已成功)、rejected(已失败)

  • 2、Promise 对象接受一个回调函数作为参数, 该回调函数接受两个参数,分别是成功时的回调 resolve 和失败时的回调 reject;另外 resolve 的参数除了正常值以外, 还可能是一个 Promise 对象的实例;reject 的参数通常是一个 Error 对象的实例。

  • 3、then 方法返回一个新的 Promise 实例,并接收两个参数 onResolved(fulfilled 状态的回调);onRejected(rejected 状态的回调,该参数可选)

  • 4、catch 方法返回一个新的 Promise 实例

  • 5、finally 方法不管 Promise 状态如何都会执行,该方法的回调函数不接受任何参数

  • 6、Promise.all()方法将多个多个 Promise 实例,包装成一个新的 Promise 实例,该方法接受一个由 Promise 对象组成的数组作为参数(Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例),注意参数中只要有一个实例触发 catch 方法,都会触发 Promise.all()方法返回的新的实例的 catch 方法,如果参数中的某个实例本身调用了 catch 方法,将不会触发 Promise.all()方法返回的新实例的 catch 方法

  • 7、Promise.race()方法的参数与 Promise.all 方法一样,参数中的实例只要有一个率先改变状态就会将该实例的状态传给 Promise.race()方法,并将返回值作为 Promise.race()方法产生的 Promise 实例的返回值

  • 8、Promise.resolve()将现有对象转为 Promise 对象,如果该方法的参数为一个 Promise 对象,Promise.resolve()将不做任何处理;如果参数 thenable 对象(即具有 then 方法),Promise.resolve()将该对象转为 Promise 对象并立即执行 then 方法;如果参数是一个原始值,或者是一个不具有 then 方法的对象,则 Promise.resolve 方法返回一个新的 Promise 对象,状态为 fulfilled,其参数将会作为 then 方法中 onResolved 回调函数的参数,如果 Promise.resolve 方法不带参数,会直接返回一个 fulfilled 状态的 Promise 对象。需要注意的是,立即 resolve()的 Promise 对象,是在本轮“事件循环”(event loop)的结束时执行,而不是在下一轮“事件循环”的开始时。

  • 9、Promise.reject()同样返回一个新的 Promise 对象,状态为 rejected,无论传入任何参数都将作为 reject()的参数


2)Promise 优点


  • ①统一异步 API

  • Promise 的一个重要优点是它将逐渐被用作浏览器的异步 API ,统一现在各种各样的 API ,以及不兼容的模式和手法。

  • ②Promise 与事件对比

  • 和事件相比较, Promise 更适合处理一次性的结果。在结果计算出来之前或之后注册回调函数都是可以的,都可以拿到正确的值。 Promise 的这个优点很自然。但是,不能使用 Promise 处理多次触发的事件。链式处理是 Promise 的又一优点,但是事件却不能这样链式处理。

  • ③Promise 与回调对比

  • 解决了回调地狱的问题,将异步操作以同步操作的流程表达出来。

  • ④Promise 带来的额外好处是包含了更好的错误处理方式(包含了异常处理),并且写起来很轻松(因为可以重用一些同步的工具,比如 Array.prototype.map() )。


3)Promise 缺点


  • 1、无法取消 Promise,一旦新建它就会立即执行,无法中途取消。

  • 2、如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。

  • 3、当处于 Pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

  • 4、Promise 真正执行回调的时候,定义 Promise 那部分实际上已经走完了,所以 Promise 的报错堆栈上下文不太友好。


4)简单代码实现 最简单的 Promise 实现有 7 个主要属性, state(状态), value(成功返回值), reason(错误信息), resolve 方法, reject 方法, then 方法


class Promise{  constructor(executor) {    this.state = 'pending';    this.value = undefined;    this.reason = undefined;    let resolve = value => {      if (this.state === 'pending') {        this.state = 'fulfilled';        this.value = value;      }    };    let reject = reason => {      if (this.state === 'pending') {        this.state = 'rejected';        this.reason = reason;      }    };    try {      // 立即执行函数      executor(resolve, reject);    } catch (err) {      reject(err);    }  }  then(onFulfilled, onRejected) {    if (this.state === 'fulfilled') {      let x = onFulfilled(this.value);    };    if (this.state === 'rejected') {      let x = onRejected(this.reason);    };  }}
复制代码


5)面试够用版


function myPromise(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.reason=reason;        self.status="rejected";       }  }  //捕获构造异常   try{      constructor(resolve,reject);  }catch(e){    reject(e);    } }myPromise.prototype.then=function(onFullfilled,onRejected){   let self=this;  switch(self.status){    case "resolved": onFullfilled(self.value); break;    case "rejected": onRejected(self.reason); break;    default:   }}
// 测试var p=new myPromise(function(resolve,reject){resolve(1)}); p.then(function(x){console.log(x)})//输出1
复制代码


6)大厂专供版


const PENDING = "pending"; const FULFILLED = "fulfilled"; const REJECTED = "rejected";const resolvePromise = (promise, x, resolve, reject) => {  if (x === promise) {    // If promise and x refer to the same object, reject promise with a TypeError as the reason.    reject(new TypeError('循环引用'))  }  // if x is an object or function,  if (x !== null && typeof x === 'object' || typeof x === 'function') {    // If both resolvePromise and rejectPromise are called, or multiple calls to the same argument are made, the first call takes precedence, and any further calls are ignored.    let called    try { // If retrieving the property x.then results in a thrown exception e, reject promise with e as the reason.      let then = x.then // Let then be x.then      // If then is a function, call it with x as this      if (typeof then === 'function') {        // If/when resolvePromise is called with a value y, run [[Resolve]](promise, y)        // If/when rejectPromise is called with a reason r, reject promise with r.        then.call(x, y => {          if (called) return          called = true          resolvePromise(promise, y, resolve, reject)        }, r => {          if (called) return          called = true          reject(r)        })      } else {        // If then is not a function, fulfill promise with x.        resolve(x)      }    } catch (e) {      if (called) return      called = true      reject(e)    }  } else {    // If x is not an object or function, fulfill promise with x    resolve(x)  }}function Promise(excutor) {  let that = this; // 缓存当前promise实例例对象  that.status = PENDING; // 初始状态  that.value = undefined; // fulfilled状态时 返回的信息  that.reason = undefined; // rejected状态时 拒绝的原因   that.onFulfilledCallbacks = []; // 存储fulfilled状态对应的onFulfilled函数  that.onRejectedCallbacks = []; // 存储rejected状态对应的onRejected函数  function resolve(value) { // value成功态时接收的终值    if(value instanceof Promise) {      return value.then(resolve, reject);    }    // 实践中要确保 onFulfilled 和 onRejected ⽅方法异步执⾏行行,且应该在 then ⽅方法被调⽤用的那⼀一轮事件循环之后的新执⾏行行栈中执⾏行行。    setTimeout(() => {      // 调⽤用resolve 回调对应onFulfilled函数      if (that.status === PENDING) {        // 只能由pending状态 => fulfilled状态 (避免调⽤用多次resolve reject)        that.status = FULFILLED;        that.value = value;        that.onFulfilledCallbacks.forEach(cb => cb(that.value));      }    });  }  function reject(reason) { // reason失败态时接收的拒因    setTimeout(() => {      // 调⽤用reject 回调对应onRejected函数      if (that.status === PENDING) {        // 只能由pending状态 => rejected状态 (避免调⽤用多次resolve reject)        that.status = REJECTED;        that.reason = reason;        that.onRejectedCallbacks.forEach(cb => cb(that.reason));      }    });  }
// 捕获在excutor执⾏行行器器中抛出的异常 // new Promise((resolve, reject) => { // throw new Error('error in excutor') // }) try { excutor(resolve, reject); } catch (e) { reject(e); }}Promise.prototype.then = function(onFulfilled, onRejected) { const that = this; let newPromise; // 处理理参数默认值 保证参数后续能够继续执⾏行行 onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value; onRejected = typeof onRejected === "function" ? onRejected : reason => { throw reason; }; if (that.status === FULFILLED) { // 成功态 return newPromise = new Promise((resolve, reject) => { setTimeout(() => { try{ let x = onFulfilled(that.value); resolvePromise(newPromise, x, resolve, reject); //新的promise resolve 上⼀一个onFulfilled的返回值 } catch(e) { reject(e); // 捕获前⾯面onFulfilled中抛出的异常then(onFulfilled, onRejected); } }); }) } if (that.status === REJECTED) { // 失败态 return newPromise = new Promise((resolve, reject) => { setTimeout(() => { try { let x = onRejected(that.reason); resolvePromise(newPromise, x, resolve, reject); } catch(e) { reject(e); } }); }); } if (that.status === PENDING) { // 等待态// 当异步调⽤用resolve/rejected时 将onFulfilled/onRejected收集暂存到集合中 return newPromise = new Promise((resolve, reject) => { that.onFulfilledCallbacks.push((value) => { try { let x = onFulfilled(value); resolvePromise(newPromise, x, resolve, reject); } catch(e) { reject(e); } }); that.onRejectedCallbacks.push((reason) => { try { let x = onRejected(reason); resolvePromise(newPromise, x, resolve, reject); } catch(e) { reject(e); } }); }); }};
复制代码

代码输出结果

const p1 = new Promise((resolve) => {  setTimeout(() => {    resolve('resolve3');    console.log('timer1')  }, 0)  resolve('resovle1');  resolve('resolve2');}).then(res => {  console.log(res)  // resolve1  setTimeout(() => {    console.log(p1)  }, 1000)}).finally(res => {  console.log('finally', res)})
复制代码


执行结果为如下:


resolve1finally  undefinedtimer1Promise{<resolved>: undefined}
复制代码


需要注意的是最后一个定时器打印出的 p1 其实是.finally的返回值,我们知道.finally的返回值如果在没有抛出错误的情况下默认会是上一个 Promise 的返回值,而这道题中.finally上一个 Promise 是.then(),但是这个.then()并没有返回值,所以 p1 打印出来的 Promise 的值会是undefined,如果在定时器的下面加上一个return 1,则值就会变成 1。


参考 前端进阶面试题详细解答

为什么 0.1 + 0.2 != 0.3,请详述理由

因为 JS 采用 IEEE 754 双精度版本(64 位),并且只要采用 IEEE 754 的语言都有该问题。


我们都知道计算机表示十进制是采用二进制表示的,所以 0.1 在二进制表示为


// (0011) 表示循环0.1 = 2^-4 * 1.10011(0011)
复制代码


那么如何得到这个二进制的呢,我们可以来演算下


小数算二进制和整数不同。乘法计算时,只计算小数位,整数位用作每一位的二进制,并且得到的第一位为最高位。所以我们得出 0.1 = 2^-4 * 1.10011(0011),那么 0.2 的演算也基本如上所示,只需要去掉第一步乘法,所以得出 0.2 = 2^-3 * 1.10011(0011)


回来继续说 IEEE 754 双精度。六十四位中符号位占一位,整数位占十一位,其余五十二位都为小数位。因为 0.10.2 都是无限循环的二进制了,所以在小数位末尾处需要判断是否进位(就和十进制的四舍五入一样)。


所以 2^-4 * 1.10011...001 进位后就变成了 2^-4 * 1.10011(0011 * 12次)010 。那么把这两个二进制加起来会得出 2^-2 * 1.0011(0011 * 11次)0100 , 这个值算成十进制就是 0.30000000000000004


下面说一下原生解决办法,如下代码所示


parseFloat((0.1 + 0.2).toFixed(10))
复制代码

DNS 同时使用 TCP 和 UDP 协议?

DNS 占用 53 号端口,同时使用 TCP 和 UDP 协议。 (1)在区域传输的时候使用 TCP 协议


  • 辅域名服务器会定时(一般 3 小时)向主域名服务器进行查询以便了解数据是否有变动。如有变动,会执行一次区域传送,进行数据同步。区域传送使用 TCP 而不是 UDP,因为数据同步传送的数据量比一个请求应答的数据量要多得多。

  • TCP 是一种可靠连接,保证了数据的准确性。


(2)在域名解析的时候使用 UDP 协议


  • 客户端向 DNS 服务器查询域名,一般返回的内容都不超过 512 字节,用 UDP 传输即可。不用经过三次握手,这样 DNS 服务器负载更低,响应更快。理论上说,客户端也可以指定向 DNS 服务器查询时用 TCP,但事实上,很多 DNS 服务器进行配置的时候,仅支持 UDP 查询包。

CSS3 的新特性

  • transition:过渡

  • transform: 旋转、缩放、移动或倾斜

  • animation: 动画

  • gradient: 渐变

  • box-shadow: 阴影

  • border-radius: 圆角

  • word-break: normal|break-all|keep-all; 文字换行(默认规则|单词也可以换行|只在半角空格或连字符换行)

  • text-overflow: 文字超出部分处理

  • text-shadow: 水平阴影,垂直阴影,模糊的距离,以及阴影的颜色。

  • box-sizing: content-box|border-box 盒模型

  • 媒体查询 @media screen and (max-width: 960px) {}还有打印print

New 的原理

常见考点


  • new 做了那些事?

  • new 返回不同的类型时会有什么表现?

  • 手写 new 的实现过程


new 关键词的主要作用就是执行一个构造函数、返回一个实例对象,在 new 的过程中,根据构造函数的情况,来确定是否可以接受参数的传递。下面我们通过一段代码来看一个简单的 new 的例子


function Person(){   this.name = 'Jack';}var p = new Person(); console.log(p.name)  // Jack
复制代码


这段代码比较容易理解,从输出结果可以看出,p 是一个通过 person 这个构造函数生成的一个实例对象,这个应该很容易理解。


new 操作符可以帮助我们构建出一个实例,并且绑定上 this,内部执行步骤可大概分为以下几步:


  1. 创建一个新对象

  2. 对象连接到构造函数原型上,并绑定 this(this 指向新对象)

  3. 执行构造函数代码(为这个新对象添加属性)

  4. 返回新对象


在第四步返回新对象这边有一个情况会例外:


那么问题来了,如果不用 new 这个关键词,结合上面的代码改造一下,去掉 new,会发生什么样的变化呢?我们再来看下面这段代码


function Person(){  this.name = 'Jack';}var p = Person();console.log(p) // undefinedconsole.log(name) // Jackconsole.log(p.name) // 'name' of undefined
复制代码


  • 从上面的代码中可以看到,我们没有使用 new 这个关键词,返回的结果就是 undefined。其中由于 JavaScript 代码在默认情况下 this 的指向是 window,那么 name 的输出结果就为 Jack,这是一种不存在 new 关键词的情况。

  • 那么当构造函数中有 return 一个对象的操作,结果又会是什么样子呢?我们再来看一段在上面的基础上改造过的代码。


function Person(){   this.name = 'Jack';    return {age: 18}}var p = new Person(); console.log(p)  // {age: 18}console.log(p.name) // undefinedconsole.log(p.age) // 18
复制代码


通过这段代码又可以看出,当构造函数最后 return 出来的是一个和 this 无关的对象时,new 命令会直接返回这个新对象而不是通过 new 执行步骤生成的 this 对象


但是这里要求构造函数必须是返回一个对象,如果返回的不是对象,那么还是会按照 new 的实现步骤,返回新生成的对象。接下来还是在上面这段代码的基础之上稍微改动一下


function Person(){   this.name = 'Jack';    return 'tom';}var p = new Person(); console.log(p)  // {name: 'Jack'}console.log(p.name) // Jack
复制代码


可以看出,当构造函数中 return 的不是一个对象时,那么它还是会根据 new 关键词的执行逻辑,生成一个新的对象(绑定了最新 this),最后返回出来


因此我们总结一下:new 关键词执行之后总是会返回一个对象,要么是实例对象,要么是 return 语句指定的对象



手工实现 New 的过程


function create(fn, ...args) {  if(typeof fn !== 'function') {    throw 'fn must be a function';  }    // 1、用new Object() 的方式新建了一个对象obj  // var obj = new Object()    // 2、给该对象的__proto__赋值为fn.prototype,即设置原型链  // obj.__proto__ = fn.prototype
// 1、2步骤合并 // 创建一个空对象,且这个空对象继承构造函数的 prototype 属性 // 即实现 obj.__proto__ === constructor.prototype var obj = Object.create(fn.prototype);
// 3、执行fn,并将obj作为内部this。使用 apply,改变构造函数 this 的指向到新建的对象,这样 obj 就可以访问到构造函数中的属性 var res = fn.apply(obj, args); // 4、如果fn有返回值,则将其作为new操作返回内容,否则返回obj return res instanceof Object ? res : obj;};
复制代码


  • 使用 Object.createobj 的proto指向为构造函数的原型

  • 使用 apply 方法,将构造函数内的 this 指向为 obj

  • create 返回时,使用三目运算符决定返回结果。


我们知道,构造函数如果有显式返回值,且返回值为对象类型,那么构造函数返回结果不再是目标实例


如下代码:


function Person(name) {  this.name = name  return {1: 1}}const person = new Person(Person, 'lucas')console.log(person)// {1: 1}
复制代码


测试


//使用create代替newfunction Person() {...}// 使用内置函数newvar person = new Person(1,2)
// 使用手写的new,即createvar person = create(Person, 1,2)
复制代码


new 被调用后大致做了哪几件事情


  • 让实例可以访问到私有属性;

  • 让实例可以访问构造函数原型(constructor.prototype)所在原型链上的属性;

  • 构造函数返回的最后结果是引用数据类型。

createElement 过程

React.createElement(): 根据指定的第一个参数创建一个 React 元素


React.createElement(  type,  [props],  [...children])
复制代码


  • 第一个参数是必填,传入的是似 HTML 标签名称,eg: ul, li

  • 第二个参数是选填,表示的是属性,eg: className

  • 第三个参数是选填, 子节点,eg: 要显示的文本内容


//写法一:
var child1 = React.createElement('li', null, 'one'); var child2 = React.createElement('li', null, 'two'); var content = React.createElement('ul', { className: 'teststyle' }, child1, child2); // 第三个参数可以分开也可以写成一个数组 ReactDOM.render( content, document.getElementById('example') );
//写法二:
var child1 = React.createElement('li', null, 'one'); var child2 = React.createElement('li', null, 'two'); var content = React.createElement('ul', { className: 'teststyle' }, [child1, child2]); ReactDOM.render( content, document.getElementById('example') );
复制代码

左右居中方案

  • 行内元素: text-align: center

  • 定宽块状元素: 左右 margin 值为 auto

  • 不定宽块状元素: table布局,position + transform


/* 方案1 */.wrap {  text-align: center}.center {  display: inline;  /* or */  /* display: inline-block; */}/* 方案2 */.center {  width: 100px;  margin: 0 auto;}/* 方案2 */.wrap {  position: relative;}.center {  position: absulote;  left: 50%;  transform: translateX(-50%);}
复制代码

变量提升

当执行 JS 代码时,会生成执行环境,只要代码不是写在函数中的,就是在全局执行环境中,函数中的代码会产生函数执行环境,只此两种执行环境。


b() // call bconsole.log(a) // undefined
var a = 'Hello world'
function b() { console.log('call b')}
复制代码


想必以上的输出大家肯定都已经明白了,这是因为函数和变量提升的原因。通常提升的解释是说将声明的代码移动到了顶部,这其实没有什么错误,便于大家理解。但是更准确的解释应该是:在生成执行环境时,会有两个阶段。第一个阶段是创建的阶段,JS 解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明并且赋值为 undefined,所以在第二个阶段,也就是代码执行阶段,我们可以直接提前使用


  • 在提升的过程中,相同的函数会覆盖上一个函数,并且函数优先于变量提升


b() // call b second
function b() { console.log('call b fist')}function b() { console.log('call b second')}var b = 'Hello world'
复制代码


var 会产生很多错误,所以在 ES6 中引入了 letlet不能在声明前使用,但是这并不是常说的 let 不会提升,let提升了,在第一阶段内存也已经为他开辟好了空间,但是因为这个声明的特性导致了并不能在声明前使用

作用域

  • 作用域: 作用域是定义变量的区域,它有一套访问变量的规则,这套规则来管理浏览器引擎如何在当前作用域以及嵌套的作用域中根据变量(标识符)进行变量查找

  • 作用域链: 作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,我们可以访问到外层环境的变量和 函数。


作用域链的本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。作用域链的前 端始终都是当前执行上下文的变量对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象。


  • 当我们查找一个变量时,如果当前执行环境中没有找到,我们可以沿着作用域链向后查找

  • 作用域链的创建过程跟执行上下文的建立有关....


作用域可以理解为变量的可访问性,总共分为三种类型,分别为:


  • 全局作用域

  • 函数作用域

  • 块级作用域,ES6 中的 letconst 就可以产生该作用域


其实看完前面的闭包、this 这部分内部的话,应该基本能了解作用域的一些应用。


一旦我们将这些作用域嵌套起来,就变成了另外一个重要的知识点「作用域链」,也就是 JS 到底是如何访问需要的变量或者函数的。


  • 首先作用域链是在定义时就被确定下来的,和箭头函数里的 this 一样,后续不会改变,JS 会一层层往上寻找需要的内容。

  • 其实作用域链这个东西我们在闭包小结中已经看到过它的实体了:[[Scopes]]



图中的 [[Scopes]] 是个数组,作用域的一层层往上寻找就等同于遍历 [[Scopes]]


1. 全局作用域


全局变量是挂载在 window 对象下的变量,所以在网页中的任何位置你都可以使用并且访问到这个全局变量


var globalName = 'global';function getName() {   console.log(globalName) // global  var name = 'inner'  console.log(name) // inner} getName();console.log(name); // console.log(globalName); //globalfunction setName(){   vName = 'setName';}setName();console.log(vName); // setName
复制代码


  • 从这段代码中我们可以看到,globalName 这个变量无论在什么地方都是可以被访问到的,所以它就是全局变量。而在 getName 函数中作为局部变量的 name 变量是不具备这种能力的

  • 当然全局作用域有相应的缺点,我们定义很多全局变量的时候,会容易引起变量命名的冲突,所以在定义变量的时候应该注意作用域的问题。


2. 函数作用域


函数中定义的变量叫作函数变量,这个时候只能在函数内部才能访问到它,所以它的作用域也就是函数的内部,称为函数作用域


function getName () {  var name = 'inner';  console.log(name); //inner}getName();console.log(name);
复制代码


除了这个函数内部,其他地方都是不能访问到它的。同时,当这个函数被执行完之后,这个局部变量也相应会被销毁。所以你会看到在 getName 函数外面的 name 是访问不到的


3. 块级作用域


ES6 中新增了块级作用域,最直接的表现就是新增的 let 关键词,使用 let 关键词定义的变量只能在块级作用域中被访问,有“暂时性死区”的特点,也就是说这个变量在定义之前是不能被使用的。


在 JS 编码过程中 if 语句for 语句后面 {...} 这里面所包括的,就是块级作用域


console.log(a) //a is not definedif(true){  let a = '123';  console.log(a); // 123}console.log(a) //a is not defined
复制代码


从这段代码可以看出,变量 a 是在 if 语句{...} 中由 let 关键词进行定义的变量,所以它的作用域是 if 语句括号中的那部分,而在外面进行访问 a 变量是会报错的,因为这里不是它的作用域。所以在 if 代码块的前后输出 a 这个变量的结果,控制台会显示 a 并没有定义

TCP 的可靠传输机制

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


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


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


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


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

viewport

<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no" />    // width    设置viewport宽度,为一个正整数,或字符串‘device-width’    // device-width  设备宽度    // height   设置viewport高度,一般设置了宽度,会自动解析出高度,可以不用设置    // initial-scale    默认缩放比例(初始缩放比例),为一个数字,可以带小数    // minimum-scale    允许用户最小缩放比例,为一个数字,可以带小数    // maximum-scale    允许用户最大缩放比例,为一个数字,可以带小数    // user-scalable    是否允许手动缩放
复制代码


  • 延伸提问

  • 怎样处理 移动端 1px 被 渲染成 2px问题


局部处理


  • meta标签中的 viewport属性 ,initial-scale 设置为 1

  • rem按照设计稿标准走,外加利用transfromescale(0.5) 缩小一倍即可;


全局处理


  • mate标签中的 viewport属性 ,initial-scale 设置为 0.5

  • rem 按照设计稿标准走即可

HTTPS 是如何保证安全的?

先理解两个概念:


  • 对称加密:即通信的双⽅都使⽤同⼀个秘钥进⾏加解密,对称加密虽然很简单性能也好,但是⽆法解决⾸次把秘钥发给对⽅的问题,很容易被⿊客拦截秘钥。

  • ⾮对称加密:


  1. 私钥 + 公钥= 密钥对

  2. 即⽤私钥加密的数据,只有对应的公钥才能解密,⽤公钥加密的数据,只有对应的私钥才能解密

  3. 因为通信双⽅的⼿⾥都有⼀套⾃⼰的密钥对,通信之前双⽅会先把⾃⼰的公钥都先发给对⽅

  4. 然后对⽅再拿着这个公钥来加密数据响应给对⽅,等到到了对⽅那⾥,对⽅再⽤⾃⼰的私钥进⾏解密


⾮对称加密虽然安全性更⾼,但是带来的问题就是速度很慢,影响性能。


解决⽅案:


结合两种加密⽅式,将对称加密的密钥使⽤⾮对称加密的公钥进⾏加密,然后发送出去,接收⽅使⽤私钥进⾏解密得到对称加密的密钥,然后双⽅可以使⽤对称加密来进⾏沟通。


此时⼜带来⼀个问题,中间⼈问题:如果此时在客户端和服务器之间存在⼀个中间⼈,这个中间⼈只需要把原本双⽅通信互发的公钥,换成⾃⼰的公钥,这样中间⼈就可以轻松解密通信双⽅所发送的所有数据。


所以这个时候需要⼀个安全的第三⽅颁发证书(CA),证明身份的身份,防⽌被中间⼈攻击。 证书中包括:签发者、证书⽤途、使⽤者公钥、使⽤者私钥、使⽤者的 HASH 算法、证书到期时间等。


但是问题来了,如果中间⼈篡改了证书,那么身份证明是不是就⽆效了?这个证明就⽩买了,这个时候需要⼀个新的技术,数字签名。


数字签名就是⽤CA⾃带的 HASH 算法对证书的内容进⾏HASH 得到⼀个摘要,再⽤CA 的私钥加密,最终组成数字签名。当别⼈把他的证书发过来的时候,我再⽤同样的 Hash 算法,再次⽣成消息摘要,然后⽤CA 的公钥对数字签名解密,得到 CA 创建的消息摘要,两者⼀⽐,就知道中间有没有被⼈篡改了。这个时候就能最⼤程度保证通信的安全了。

即时通讯的实现:短轮询、长轮询、SSE 和 WebSocket 间的区别?

短轮询和长轮询的目的都是用于实现客户端和服务器端的一个即时通讯。


短轮询的基本思路: 浏览器每隔一段时间向浏览器发送 http 请求,服务器端在收到请求后,不论是否有数据更新,都直接进行响应。这种方式实现的即时通信,本质上还是浏览器发送请求,服务器接受请求的一个过程,通过让客户端不断的进行请求,使得客户端能够模拟实时地收到服务器端的数据的变化。这种方式的优点是比较简单,易于理解。缺点是这种方式由于需要不断的建立 http 连接,严重浪费了服务器端和客户端的资源。当用户增加时,服务器端的压力就会变大,这是很不合理的。


长轮询的基本思路: 首先由客户端向服务器发起请求,当服务器收到客户端发来的请求后,服务器端不会直接进行响应,而是先将这个请求挂起,然后判断服务器端数据是否有更新。如果有更新,则进行响应,如果一直没有数据,则到达一定的时间限制才返回。客户端 JavaScript 响应处理函数会在处理完服务器返回的信息后,再次发出请求,重新建立连接。长轮询和短轮询比起来,它的优点是明显减少了很多不必要的 http 请求次数,相比之下节约了资源。长轮询的缺点在于,连接挂起也会导致资源的浪费。


SSE 的基本思想: 服务器使用流信息向服务器推送信息。严格地说,http 协议无法做到服务器主动推送信息。但是,有一种变通方法,就是服务器向客户端声明,接下来要发送的是流信息。也就是说,发送的不是一次性的数据包,而是一个数据流,会连续不断地发送过来。这时,客户端不会关闭连接,会一直等着服务器发过来的新的数据流,视频播放就是这样的例子。SSE 就是利用这种机制,使用流信息向浏览器推送信息。它基于 http 协议,目前除了 IE/Edge,其他浏览器都支持。它相对于前面两种方式来说,不需要建立过多的 http 请求,相比之下节约了资源。


WebSocket 是 HTML5 定义的一个新协议议,与传统的 http 协议不同,该协议允许由服务器主动的向客户端推送信息。使用 WebSocket 协议的缺点是在服务器端的配置比较复杂。WebSocket 是一个全双工的协议,也就是通信双方是平等的,可以相互发送消息,而 SSE 的方式是单向通信的,只能由服务器端向客户端推送信息,如果客户端需要发送信息就是属于下一个 http 请求了。


上面的四个通信协议,前三个都是基于 HTTP 协议的。


对于这四种即使通信协议,从性能的角度来看: WebSocket > 长连接(SEE) > 长轮询 > 短轮询 但是,我们如果考虑浏览器的兼容性问题,顺序就恰恰相反了: 短轮询 > 长轮询 > 长连接(SEE) > WebSocket 所以,还是要根据具体的使用场景来判断使用哪种方式。

对 WebSocket 的理解

WebSocket 是 HTML5 提供的一种浏览器与服务器进行全双工通讯的网络技术,属于应用层协议。它基于 TCP 传输协议,并复用 HTTP 的握手通道。浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接, 并进行双向数据传输。


WebSocket 的出现就解决了半双工通信的弊端。它最大的特点是:服务器可以向客户端主动推动消息,客户端也可以主动向服务器推送消息。


WebSocket 原理:客户端向 WebSocket 服务器通知(notify)一个带有所有接收者 ID(recipients IDs)的事件(event),服务器接收后立即通知所有活跃的(active)客户端,只有 ID 在接收者 ID 序列中的客户端才会处理这个事件。​


WebSocket 特点的如下:


  • 支持双向通信,实时性更强

  • 可以发送文本,也可以发送二进制数据‘’

  • 建立在 TCP 协议之上,服务端的实现比较容易

  • 数据格式比较轻量,性能开销小,通信高效

  • 没有同源限制,客户端可以与任意服务器通信

  • 协议标识符是 ws(如果加密,则为 wss),服务器网址就是 URL

  • 与 HTTP 协议有着良好的兼容性。默认端口也是 80 和 443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。


Websocket 的使用方法如下:


在客户端中:


// 在index.html中直接写WebSocket,设置服务端的端口号为 9999let ws = new WebSocket('ws://localhost:9999');// 在客户端与服务端建立连接后触发ws.onopen = function() {    console.log("Connection open.");     ws.send('hello');};// 在服务端给客户端发来消息的时候触发ws.onmessage = function(res) {    console.log(res);       // 打印的是MessageEvent对象    console.log(res.data);  // 打印的是收到的消息};// 在客户端与服务端建立关闭后触发ws.onclose = function(evt) {  console.log("Connection closed.");}; 
复制代码

面向对象

编程思想


  • 基本思想是使用对象,类,继承,封装等基本概念来进行程序设计

  • 优点

  • 易维护

  • 采用面向对象思想设计的结构,可读性高,由于继承的存在,即使改变需求,那么维护也只是在局部模块,所以维护起来是非常方便和较低成本的

  • 易扩展

  • 开发工作的重用性、继承性高,降低重复工作量。

  • 缩短了开发周期


一般面向对象包含:继承,封装,多态,抽象


1. 对象形式的继承


浅拷贝


var Person = {    name: 'poetry',    age: 18,    address: {        home: 'home',        office: 'office',    }    sclools: ['x','z'],};
var programer = { language: 'js',};
function extend(p, c){ var c = c || {}; for( var prop in p){ c[prop] = p[prop]; }}extend(Person, programer);programer.name; // poetryprogramer.address.home; // homeprogramer.address.home = 'house'; //housePerson.address.home; // house
复制代码


从上面的结果看出,浅拷贝的缺陷在于修改了子对象中引用类型的值,会影响到父对象中的值,因为在浅拷贝中对引用类型的拷贝只是拷贝了地址,指向了内存中同一个副本


深拷贝


function extendDeeply(p, c){    var c = c || {};    for (var prop in p){        if(typeof p[prop] === "object"){            c[prop] = (p[prop].constructor === Array)?[]:{};            extendDeeply(p[prop], c[prop]);        }else{            c[prop] = p[prop];        }    }}
复制代码


利用递归进行深拷贝,这样子对象的修改就不会影响到父对象


extendDeeply(Person, programer);programer.address.home = 'poetry';Person.address.home; // home
复制代码


利用 call 和 apply 继承


function Parent(){    this.name = "abc";    this.address = {home: "home"};}function Child(){    Parent.call(this);    this.language = "js"; }
复制代码


ES5 中的 Object.create()


var p = { name : 'poetry'};var obj = Object.create(p);obj.name; // poetry
复制代码


Object.create()作为 new 操作符的替代方案是 ES5 之后才出来的。我们也可以自己模拟该方法:


//模拟Object.create()方法function myCreate(o){    function F(){};    F.prototype = o;    o = new F();    return o;}var p = { name : 'poetry'};var obj = myCreate(p);obj.name; // poetry
复制代码


目前,各大浏览器的最新版本(包括 IE9)都部署了这个方法。如果遇到老式浏览器,可以用下面的代码自行部署


if (!Object.create) {    Object.create = function (o) {       function F() {}      F.prototype = o;      return new F();    };  }
复制代码


2. 类的继承


Object.create()


function Person(name, age){}Person.prototype.headCount = 1;Person.prototype.eat = function(){    console.log('eating...');}function Programmer(name, age, title){}
Programmer.prototype = Object.create(Person.prototype); //建立继承关系Programmer.prototype.constructor = Programmer; // 修改constructor的指向
复制代码


调用父类方法


function Person(name, age){    this.name = name;    this.age = age;}Person.prototype.headCount = 1;Person.prototype.eat = function(){    console.log('eating...');}
function Programmer(name, age, title){ Person.apply(this, arguments); // 调用父类的构造器}

Programmer.prototype = Object.create(Person.prototype);Programmer.prototype.constructor = Programmer;
Programmer.prototype.language = "js";Programmer.prototype.work = function(){ console.log('i am working code in '+ this.language); Person.prototype.eat.apply(this, arguments); // 调用父类上的方法}
复制代码


3. 封装


  • 命名空间

  • js 是没有命名空间的,因此可以用对象模拟


var app = {};  // 命名空间app//模块1app.module1 = {    name: 'poetry',    f: function(){        console.log('hi robot');    }};app.module1.name; // "poetry"app.module1.f();  // hi robot
复制代码


对象的属性外界是可读可写 如何来达到封装的额目的?答:可通过闭包+局部变量来完成


  • 在构造函数内部声明局部变量 和普通方法

  • 因为作用域的关系 只有构造函数内的方法

  • 才能访问局部变量 而方法对于外界是开放的

  • 因此可以通过方法来访问 原本外界访问不到的局部变量 达到函数封装的目的


function Girl(name,age){    var love = '小明';//love 是局部变量 准确说不属于对象 属于这个函数的额激活对象 函数调用时必将产生一个激活对象 love在激活对象身上   激活对象有作用域的关系 有办法访问  加一个函数提供外界访问    this.name = name;    this.age = age;    this.say = function () {        return love;    };
this.movelove = function (){ love = '小轩'; //35 }
}
var g = new Girl('yinghong',22);
console.log(g);console.log(g.say());//小明console.log(g.movelove());//undefined 因为35行没有返回console.log(g.say());//小轩


function fn(){ function t(){ //var age = 22;//声明age变量 在t的激活对象上 age = 22;//赋值操作 t的激活对象上找age属性 ,找不到 找fn的激活对象....再找到 最终找到window.age = 22; //不加var就是操作window全局属性
} t();}console.log(fn());//undefined
复制代码


4. 静态成员


面向对象中的静态方法-静态属性:没有 new 对象 也能引用静态方法属性


function Person(name){    var age = 100;    this.name = name;}//静态成员Person.walk = function(){    console.log('static');};Person.walk();  // static
复制代码


5. 私有与公有


function Person(id){    // 私有属性与方法    var name = 'poetry';    var work = function(){        console.log(this.id);    };    //公有属性与方法    this.id = id;    this.say = function(){        console.log('say hello');        work.call(this);    };};var p1 = new Person(123);p1.name; // undefinedp1.id;  // 123p1.say();  // say hello 123
复制代码


6. 模块化


var moduleA;moduleA = function() {    var prop = 1;
function func() {}
return { func: func, prop: prop };}(); // 立即执行匿名函数
复制代码


7. 多态


多态:同一个父类继承出来的子类各有各的形态


function Cat(){    this.eat = '肉';}
function Tiger(){ this.color = '黑黄相间';}
function Cheetah(){ this.color = '报文';}
function Lion(){ this.color = '土黄色';}
Tiger.prototype = Cheetah.prototype = Lion.prototype = new Cat();//共享一个祖先 Cat
var T = new Tiger();var C = new Cheetah();var L = new Lion();
console.log(T.color);console.log(C.color);console.log(L.color);

console.log(T.eat);console.log(C.eat);console.log(L.eat);
复制代码


8. 抽象类


在构造器中 throw new Error(''); 抛异常。这样防止这个类被直接调用


function DetectorBase() {    throw new Error('Abstract class can not be invoked directly!');}
DetectorBase.prototype.detect = function() { console.log('Detection starting...');};DetectorBase.prototype.stop = function() { console.log('Detection stopped.');};DetectorBase.prototype.init = function() { throw new Error('Error');};
// var d = new DetectorBase();// Uncaught Error: Abstract class can not be invoked directly!
function LinkDetector() {}LinkDetector.prototype = Object.create(DetectorBase.prototype);LinkDetector.prototype.constructor = LinkDetector;
var l = new LinkDetector();console.log(l); //LinkDetector {}__proto__: LinkDetectorl.detect(); //Detection starting...l.init(); //Uncaught Error: Error
复制代码


用户头像

loveX001

关注

还未添加个人签名 2022-09-01 加入

还未添加个人简介

评论

发布
暂无评论
这样回答前端面试题才能拿到offer_JavaScript_loveX001_InfoQ写作社区