前端增长题(js进阶)

238 阅读10分钟

js代码的执行流程是怎样的?

变量和函数声明在代码里的位置是不会改变的,而且是在编译阶段被 JavaScript 引擎放入内存中。编译完成之后,才会进入执行阶段。大致流程你可以参考下图:

  • 编译阶段 确定执行上下文和可执行代码(执行上下文中存在一个变量环境的对象(Viriable Environment),该对象中保存了变量提升的内容)
  • 执行阶段 当要用到变量时,会在变量环境中找到要用到的变量,如果未赋值就返回undefined

var有什么缺陷?为什么要引入let,const?

  • var会带来变量提升,可能在不经意间出现变量覆盖。(如下面这种例子)
function varTest() {
  var x = 1;
  if (true) {
    var x = 2;  // 同样的变量!
    console.log(x);  // 2
  }
  console.log(x);  // 2
}
  • 本该销毁的没有销毁(如下面这种例子)

function foo(){
  for (var i = 0; i < 7; i++) {
  }
  console.log(i); 
}
foo()

更多内容见我的有道笔记:note.youdao.com/s/U6ifKJYG

反正就是let const是不存在与变量环境了,而是存在于词法环境,此环境也是栈的数据结构,当访问一个变量时,先从词法环境中查找,从栈顶查到到栈顶,如果还没有找到,再去变量环境中查找。

什么是闭包?

说到闭包,得先了解作用域链。其实在每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用称为 outer。内部函数访问某个变量如果没找到时,就去outer找,查找的这条链就叫作用域链。

根据词法作用域的规则,内部函数总是可以访问它们的外部函数中的变量,所以当外部函数已经执行结束,但是内部函数依然可以使用外部函数中的变量,即通过作用域链。(词法作用域是代码编译阶段就决定好的,和函数是怎么调用的没有关系。)

具体可看我有道笔记总结的,也有例子。( note.youdao.com/s/Lrigqav3

模块化的进化史

commonJs:模块化,同步,只能在node运行

requireJs:关键字(define,require),异步

AMD:对requireJs的规范化

CMD:延迟执行(AMD后面也改成延迟执行了),依赖就近

UMD:commonJS与CMD的柔和,跨平台通用

es6:node、浏览器都支持的模块化语言,但对于低版本的浏览器,需要做转义,转成es5

前端性能优化方法

  • 减少http请求
  • 减少重绘重排
  • 图片预加载
  • 缓存

js代码性能优化

  • 少用全局变量(不利于gc)
  • 缓存变量,避免重复的链式调用
  • for循环优化,见note.youdao.com/s/aalRyWS3
  • 使用Fragment塞入碎片化的dom,再一次性塞入dom
  • 使用cloneNode代替新创建元素节点
  • 字面量代替new操作
  • 减少判断层次
  • 减少作用域链的查找
  • 减少声明和语句数
  • 采用事件委托代替循环监听

js实现继承

note.youdao.com/s/DGkmHLz3

new操作符具体干了什么

function Func(){
}
let func= new Func();
  • 创建了空对象
let obj = new Object();
  • 指定该obj的原型为该构造函数的原型
obj.__proto__ = Func.prototype;
  • 重新绑定将该构造函数的this指向,this指向改为obj
let result = Func.call(obj);
  • 返回新对象 如果无返回值 或者 返回一个非对象值,则将 obj 作为新对象返回;否则会将 result 作为新对象返回。
if (typeof(result) == "object"){
  func=result;
}
else{
  func=obj;
}

手写call,apply,bind

call

Function.prototype.call = function (context, ...args) {
  context = context || window;
  
  const fnSymbol = Symbol("fn");
  context[fnSymbol] = this; // this则是当前要执行的函数,比如a.call(...),则this是a函数
  
  let res = context[fnSymbol](...args);

  delete context[fnSymbol]; // 删除临时变量,避免对传入的context造成污染

  return res;
}

apply

Function.prototype.call = function (context, args) {
  context = context || window;
  
  const fnSymbol = Symbol("fn"); // 使用Symbol创建唯一值
  context[fnSymbol] = this;
  
  let res = context[fnSymbol](...args);

  delete context[fnSymbol];

  return res;
}

bind

Function.prototype.bind = function (context, ...args) {
  context = context || window;
  const fnSymbol = Symbol("fn");
  context[fnSymbol] = this;
  
  return function (..._args) {
    args = args.concat(_args);
    
    let res = context[fnSymbol](...args);
    delete context[fnSymbol];   
    return res
  }
}

编写一个promise

const PENDING = 'pending'; // 等待
const FULFILLED = 'fulfilled'; // 成功
const REJECTED = 'rejected'; // 失败

class MyPromise {
  constructor (executor) { // executor执行器
    try {
      executor(this.resolve, this.reject)
    } catch (e) {
      this.reject(e);
    }
  }
  // promsie 状态 
  status = PENDING;
  // 成功之后的值
  value = undefined;
  // 失败后的原因
  reason = undefined;
  // 成功回调
  successCallback = [];
  // 失败回调
  failCallback = [];

  resolve = value => {
    // 如果状态不是等待 阻止程序向下执行
    if (this.status !== PENDING) return;
    // 将状态更改为成功
    this.status = FULFILLED;
    // 保存成功之后的值
    this.value = value;
    // 判断成功回调是否存在 如果存在 调用
    // this.successCallback && this.successCallback(this.value);
    while(this.successCallback.length) this.successCallback.shift()()
  }
  reject = reason => {
    // 如果状态不是等待 阻止程序向下执行
    if (this.status !== PENDING) return;
    // 将状态更改为失败
    this.status = REJECTED;
    // 保存失败后的原因
    this.reason = reason;
    // 判断失败回调是否存在 如果存在 调用
    // this.failCallback && this.failCallback(this.reason);
    while(this.failCallback.length) this.failCallback.shift()()
  }
  then (successCallback, failCallback) {
    // 参数可选
    successCallback = successCallback ? successCallback : value => value;
    // 参数可选
    failCallback = failCallback ? failCallback: reason => { throw reason };
    let promsie2 = new MyPromise((resolve, reject) => {
      // 判断状态
      if (this.status === FULFILLED) {
        setTimeout(() => {
          try {
            let x = successCallback(this.value);
            // 判断 x 的值是普通值还是promise对象
            // 如果是普通值 直接调用resolve 
            // 如果是promise对象 查看promsie对象返回的结果 
            // 再根据promise对象返回的结果 决定调用resolve 还是调用reject
            resolvePromise(promsie2, x, resolve, reject)
          }catch (e) {
            reject(e);
          }
        }, 0)
      }else if (this.status === REJECTED) {
        setTimeout(() => {
          try {
            let x = failCallback(this.reason);
            // 判断 x 的值是普通值还是promise对象
            // 如果是普通值 直接调用resolve 
            // 如果是promise对象 查看promsie对象返回的结果 
            // 再根据promise对象返回的结果 决定调用resolve 还是调用reject
            resolvePromise(promsie2, x, resolve, reject)
          }catch (e) {
            reject(e);
          }
        }, 0)
      } else {
        // 等待
        // 将成功回调和失败回调存储起来
        this.successCallback.push(() => {
          setTimeout(() => {
            try {
              let x = successCallback(this.value);
              // 判断 x 的值是普通值还是promise对象
              // 如果是普通值 直接调用resolve 
              // 如果是promise对象 查看promsie对象返回的结果 
              // 再根据promise对象返回的结果 决定调用resolve 还是调用reject
              resolvePromise(promsie2, x, resolve, reject)
            }catch (e) {
              reject(e);
            }
          }, 0)
        });
        this.failCallback.push(() => {
          setTimeout(() => {
            try {
              let x = failCallback(this.reason);
              // 判断 x 的值是普通值还是promise对象
              // 如果是普通值 直接调用resolve 
              // 如果是promise对象 查看promsie对象返回的结果 
              // 再根据promise对象返回的结果 决定调用resolve 还是调用reject
              resolvePromise(promsie2, x, resolve, reject)
            }catch (e) {
              reject(e);
            }
          }, 0)
        });
      }
    });
    return promsie2;
  }
  finally (callback) {
    return this.then(value => {
      return MyPromise.resolve(callback()).then(() => value);
    }, reason => {
      return MyPromise.resolve(callback()).then(() => { throw reason })
    })
  }
  catch (failCallback) {
    return this.then(undefined, failCallback)
  }
  static all (array) {
    let result = [];
    let index = 0;
    return new MyPromise((resolve, reject) => {
      function addData (key, value) {
        result[key] = value;
        index++;
        if (index === array.length) {
          resolve(result);
        }
      }
      for (let i = 0; i < array.length; i++) {
        let current = array[i];
        if (current instanceof MyPromise) {
          // promise 对象
          current.then(value => addData(i, value), reason => reject(reason))
        }else {
          // 普通值
          addData(i, array[i]);
        }
      }
    })
  }
  static resolve (value) {
    if (value instanceof MyPromise) return value;
    return new MyPromise(resolve => resolve(value));
  }
}

function resolvePromise (promsie2, x, resolve, reject) {
  if (promsie2 === x) {
    return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
  }
  if (x instanceof MyPromise) {
    // promise 对象
    x.then(resolve, reject);
  } else {
    // 普通值
    resolve(x);
  }
}

module.exports = MyPromise;

事件驱动,发布订阅,观察者,这三种模式都有什么区分?

  • 观察者 接收事件和发布事件是直接的,有发的就有接收的

  • 发布订阅 接收和发布之间有个代理

  • 事件驱动 发布的,在未来某个时间点接收并执行(比如node里事件循环机制就体现了事件驱动模式)

v8里的GC机制都有什么回收机制?

回收掉没有用到,或者从根上不能访问到对象

常用的回收算法有引用计数,标记清除,标记整理,分代回收

  • 引用计数 为0时回收

即使清除,及时释放了内存。但性能消耗大,且不能识别到循环引用的对象

  • 标记清除 先遍历标记活跃对象,再遍历一遍把没有标记的清除。

可以清除循环引用问题(因为无法从根抵达到循环引用的对象,所以没标记,会被清除掉),但会造成内存碎步化。

  • 标记整理 标记清除的升级版。在清除的时候会移动活跃对象,避免碎片化。

说说对v8的认识?以及是如何回收垃圾的?

js的执行引擎,对js即时编译,有内存限制,使用分代回收思想进行垃圾回收。

  • 新生代(复制算法和标记整理) 存活时间较短的放在新生代中。新生代区是一分为二的,有From和To,From通过标记整理后(如果某些变量空间占用超过25%,会直接放到老生代中;或者一轮GC后还存活着,也会晋升到老生代里去),放到To中,然后To和Form互换,完成释放。

注:因为新生代的空间比较小,所以要用标记整理法,避免空间的浪费。而也是因为空间小,所以直接用复制算法,因为并不会造化太大空间的浪费。

  • 老生代(标记清除,标记整理,增量标记)

注:因为老生代的空间比较充足,所以只有在新生代往老生代挪的时候发现没有足够的空间时,才会开启标记整理。增量标记其实就是分段回收,而不是一口气做完,这样程序就不会暂停太久,从而提高性能。

v8内存为什么会设置上限?

  • 目前给定的上限,对浏览器来讲一般是够用的了。
  • 如果空间太大,在垃圾回收的时候是会暂停js的执行的,如果回收占用时间过长,用户会感知到卡顿了

如何对网站资源进行优化

1.文件合并(目的是减少http请求):使用css sprites合并图片,一个网站经常使用小图标和小图片进行美化,但是很遗憾这些小图片占用了大量的HTTP请求,因此可以采用sprites的方式把所有的图片合并成一张图片 ,可以通过相关工具在线合并,也可以在ps中合并。

2.使用CDN(内容分发网络)加速,降低通信距离。

3.缓存的使用,添加Expire/Cache-Control头。

4.启用Gzip压缩文件。

5.将css放在页面最上面。

6.将script放在页面最下面。

7.避免在css中使用表达式。见www.cnblogs.com/chenxizhang…

8.将css, js都放在外部文件中。

9.减少DNS查询。

10.文件压缩:最小化css, js,减小文件体积。

11.避免重定向。

12.移除重复脚本。

13.配置实体标签ETag。

14.使用AJAX缓存,让网站内容分批加载,局部更新。

怎么使用网络缓存提高性能?

  • 强制缓存 Expires:对应一个未来的时间戳,但客户端和服务器的时间可能不一样,所以不完全准确

Cache-Control,它的常用值有下面几个:

no-cache,表示使用协商缓存,即每次使用缓存前必须向服务端确认缓存资源是否更新;

no-store,禁止浏览器以及所有中间缓存存储响应内容;

public,公有缓存,表示可以被代理服务器缓存,可以被多个用户共享;

private,私有缓存,不能被代理服务器缓存,不可以被多个用户共享;

max-age,以秒为单位的数值,表示缓存的有效时间;

must-revalidate,当缓存过期时,需要去服务端校验缓存的有效性。

  • 协商缓存 Last-Modified 和 If-Modified-Since:对应着文件修改时间

ETag 和 If-None-Match:对应着文件hash值,更准确

ServiceWorker:浏览器和服务端之间的代理服务器,主要实现离线缓存。

Window.onLoad 和 DOMContentLoaded事件的先后顺序

DOMContentLoaded事件要在window.onload之前执行,当DOM树构建完成的时候就会执行 DOMContentLoaded事件,而window.onload是在页面载入完成的时候,才执行,这其中包括图片等元素。大多数时候我们只是想在 DOM树构建完成后,绑定事件到元素,我们并不需要图片元素,加上有时候加载外域图片的速度非常缓慢。

x instanceof y 的运行原理?

x会一直沿着隐式原型链proto向上查找直到x.proto.proto......===y.prototype为止,如果找到则返回true,也就是x为y的一个实例。否则返回false,x不是y的实例。

说下this的指向

  • 全局情况下,this即是window,严格模式下是undefined
  • 全局调用函数的情况下,里面的this还是指向window
  • 作为对象里函数,函数里的this指向对象,即谁调用这个函数,this指向谁
let o = {
	a: 1,
    b: function () {
    	console.log(this.a)
    }
}

console.log(o.b()) // 1
  • 原型链上的函数,也能指向当前对象
let o = {
    b: function () {
    	console.log(this.a)
    }
}
let t = Object.create(o)
t.a = 1
console.log(t.b()) // 1
  • 构造函数里的this,当实例化这个构造函数时,this指向这个构造函数。如果这个构造函数有返回一个对象,this指向这个对象
function a () {this.t = 1}
function b () {this.t = 1; return {t:2}}
console.log(new a().t) // 1
console.log(new b().t) // 2

什么是作用域链?什么是词法作用域链?

由内到外的寻找某变量,这个寻找的过程就是靠作用域链完成的。

词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。也就是说,词法作用域是代码编译阶段就决定好的,和函数是怎么调用的没有关系。

举个例子

function bar() {
    console.log(myName)
}
function foo() {
    var myName = "极客邦"
    bar()
}
var myName = "极客时间"
foo()

根据词法作用域,foo 和 bar 的上级作用域都是全局作用域,所以如果 foo 或者 bar 函数使用了一个它们没有定义的变量,那么它们会到全局作用域去查找。所以打印出来的是“极客时间”

什么是oop

一种设计范型。它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性,灵活性,扩展性。有继承,封装,多态,抽象4个特点。

实现继承的方式

  • Object.create
function p () {}
function s () {}
s.prototype = Object.create(p.prototype)

使用Object.create是因为可以新建一个对象,让这个对象的原型指向p,但当修改s时,不会修改到p

  • 使用es6的class和extend

模拟一个eventBus(发布订阅模式)

class EventBus {
    constructor () {
        this.map = {}
    }

    $emit (eventName) {
        let events = this.map[eventName]
        (events || []).forEach(fn => fn())
    }

    $on (eventName, cb) {
        if (!this.map[eventName]) {
            this.map[eventName] = []
        }
        this.map[eventName].push(cb)
    }

    $off (eventName) {
        if (this.map[eventName]) {
            delete this.map[eventName]
        }
    }
}

模拟一个简单的观察者模式

// 发布者(目标)
class Dep {
	constructor () {
    	this.subs = []
    }
    
    addSub (sub) {
    	if (sub && sub.update) {
        	this.subs.push(sub)
        }
    }
    
    nofity () {
    	this.subs.forEach(fn => fn())
    }
}

// 订阅者(观察者)
class Watcher () {
	update () {
    	console.log(1)
    }
}

// 测试
let dep = new Dep()
let watcher = new Watcher()
dep.addSub(watcher)
dep.notify()

什么是cors

参考阮一峰文章:www.ruanyifeng.com/blog/2016/0…

全称是"跨域资源共享"(Cross-origin resource sharing)。它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。

整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。

浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。

  • 简单请求(同时满足下面这2个条件)
1) 请求方法是以下三种方法之一:
HEAD
GET
POST

(2)HTTP的头信息不超出以下几种字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
  • 非简单请求(不符合上面条件即是)

实现过程

  • 简单请求 在头信息之中,增加一个Origin字段。

如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。

如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。

Access-Control-Allow-Origin: http://api.bob.com (该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。)
Access-Control-Allow-Credentials: true (该字段可选。它的值是一个布尔值,表示是否服务器允许发送Cookie)
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8
  • 非简单请求 非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

下面是这个"预检"请求的HTTP头信息。

OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

"预检"请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪个源。

除了Origin字段,"预检"请求的头信息包括两个特殊字段。

(1)Access-Control-Request-Method

该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT。

(2)Access-Control-Request-Headers

该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header。

服务器收到"预检"请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com (该字段也可以设为星号,表示同意任意跨源请求。)
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

如果服务器否定了"预检"请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。控制台会打印出如下的报错信息。

XMLHttpRequest cannot load http://api.alice.com.
Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.

一旦服务器通过了"预检"请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。

与jsonp的对比

JSONP只支持GET请求,CORS支持所有类型的HTTP请求。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。

图片懒加载原理

参考 www.jianshu.com/p/68a6683b6… 核心代码:(监听scroll事件)

function isShow($node){
    return $node.offset().top <= $(window).height() + $(window).scrollTop()
}

两个tab之间如何通讯

var ws = new WebSocket("wss://echo.websocket.org");

ws.onopen = function(evt) { 
  console.log("Connection open ..."); 
  ws.send("Hello WebSockets!");
};

ws.onmessage = function(evt) {
  console.log( "Received Message: " + evt.data);
  ws.close();
};

ws.onclose = function(evt) {
  console.log("Connection closed.");
};      
  • html5浏览器的新特性SharedWorker(首先在服务器上要有一个js,处理需要通信的数据) 具体使用例子看 www.jianshu.com/p/5785513a9…

  • localstorage

window.onstorage = (e) => {console.log(e)}
// 或者这样
window.addEventListener('storage', (e) => console.log(e))

onstorage以及storage事件,针对都是非当前页面对localStorage进行修改时才会触发,当前页面修改localStorage不会触发监听函数。然后就是在对原有的数据的值进行修改时才会触发,比如原本已经有一个key会a值为b的localStorage,你再执行:localStorage.setItem('a', 'b')代码,同样是不会触发监听函数的。