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实现继承
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之间如何通讯
-
postMessage,且可支持跨域,mdn见 developer.mozilla.org/zh-CN/docs/…
-
websocket,且支持跨域
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')代码,同样是不会触发监听函数的。