0%

基本概念

定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
使用场景:全局缓存、Windows对象、登录浮窗等
关键:用一个变量来标志是否已经为某个类创建对象,没有则创建,有则返回这个对象

标准单例模式

代码实现:
javascript code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 标准单例模式
* 缺点是使用者必须知道这是个单例类,本来可以直接new xxx,现在变成了xxx.getInstance
*/
var Singleton = function (name) {
this.name = name
this.instance = null
};
Singleton.prototype.getName = function () {
alert(this.name)
}
Singleton.prototype.getInstance = function (name) {
if(this.instance!==null){
this.instance = new Singleton(name)
}
return this.instance
}

透明单例模式

javascript code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 利用闭包和IIFE实现了透明单例模式
* 但违反了单一职责原则
* 缺点是,当不再需要单例时,需要改写构造函数
*/
var CreateDiv = (function() {
var instance
var CreateDiv = function(html) {
if (instance) {
return instance
}
this.html = html
this.init() // 执行init方法
return (instance = this) // 保证只有一个一个对象
}
CreateDiv.prototype.init = function() {
var div = document.createElement('div')
div.innerHTML = this.html
document.body.appendChild(div)
}
return CreateDiv
})()

用代理实现的单例

javascript code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 代理实现单例
* 将负责管理单例的逻辑移到proxySingletonCreate
*/
var CreateSpan = function(html) {
this.html = html
this.init()
}
CreateSpan.prototype.init = function() {
var span = document.createElement('span')
span.innerHTML = this.html
document.body.appendChild(span)
}

var proxySingletonCreate = (function() {
var instance
return function(html) {
if (!instance) [(instance = new CreateSpan('test'))]
return instance
}
})()

创建单例的通用方法

由上可以看出,创建单例的核心即为使用一个变量表示这个对象是否被创建,即:
javascript code

1
2
3
4
var instance
if(!instance) {
instance = xxx
}

具体代码实现即为:
javascript code

1
2
3
4
5
6
7
8
9
10
/**
* 创建单例的抽象方法
* @param {function} fn
*/
var getSingle = function(fn) {
var result
return function() {
return result || (result = fn.apply(this, arguments))
}
}

设计师给的标注文件经常会出现一个情况,字重总是通过直接设置具体的字体来指定字重,实际上在font-family那一行Medium指的是字重。

其对应关系大致符合:
100 - Thin
200 - Extra Light (Ultra Light)
300 - Light
400 - Regular (Normal、Book、Roman)
500 - Medium
600 - Semi Bold (Demi Bold)
700 - Bold
800 - Extra Bold (Ultra Bold)
900 - Black (Heavy)

基本概念

Symbol 的用法其实很简单,就是创建一个独一无二的值。
举个例子:

1
Symbol() == Symbol() // false

需要注意的是,不能使用 new 创建 Symbol ,因为 Symbol 是一个基本类型。

通常在使用 Symbol 的时候,会在其中加入标识符,例如:

1
2
3
4
5
6
7
Symbol('foo') // Symbol(foo)

Symbol('foo') == Symbol('foo') // false

Symbol('foo').toString() // "Symbol(foo)"

Symbol('foo').toString() === Symbol('foo').toString() // true

SymboltoString() 方法

主要使用场景

作为对象的属性名

1
2
3
4
let o = {}
let mySymbol = Symbol()

o[mySymbol] = 'xxx'

需要注意的是,使用 Symbol 作为属性名时,要用 [] 的方式访问,因为点符号后面跟的只能时字符串。

定义常量

保证这些常量不相等

1
2
3
4
5
6
7
8
let log = {}
log.levels = {
DEBUG: Symbol('debug'),
INFO: Symbol('info'),
WARN: Symbol('warn')
};
console.log(log.levels.DEBUG, 'debug message')
console.log(log.levels.INFO, 'info message')

保证 Switch 按照设计使用

1
2
3
4
5
6
7
8
9
10
11
12
13
const COLOR_RED    = Symbol();
const COLOR_GREEN = Symbol();

function getComplement(color) {
switch (color) {
case COLOR_RED:
return COLOR_GREEN;
case COLOR_GREEN:
return COLOR_RED;
default:
throw new Error('Undefined color');
}
}

其他

Symbol 还有一些其他方法,请参阅读MDN Web Docs

不过需要特别注意的是,Symbol 定义的属性名,在使用 for...infor...of 等需要有迭代器的方法时不会出现,要获得一个对象所有的Symbol 属性名可以使用 Object.getOwnPropertySymbols() 方法。

JavaScript 中的 this 可能是 JS 这门语言中最难的一个关键字,想要去理解 this 运作的原理对 JS 需要一定的使用经验。本文并不深究 this 为什么指向某个地方,本文是一篇方法论的文章,只说明怎么样判断 this 的指向。

误区

  1. this 指向自身 ❌
  2. this 指向函数的作用域 ❌

正确的说法

this 实际上是发生在函数调用时候的绑定,它指向什么完全取决于函数在哪里调用

看得明明白白,好像自己真的懂了是不是?呵呵,不可能的。

绑定的几条规则

默认绑定

独立函数调用。

1
2
3
4
5
function f () {
console.log( this.a )
}
var a = 1
f() // 1

f()没有加任何的修饰的函数引用,所以此函数不可能使用其他规则,进而 this 此时指向的就是全局对象。不过需要注意的是,在严格模式中,this 是不可以绑定到全局对象上的。

这条规则就是,别的规则都不生效,那就是这条规则。

隐式绑定

当调用位置有上下文对象。

1
2
3
4
5
6
7
8
9
function f () {
console.log( this.a )
}
var o = {
a:1,
foo: foo
}

o.foo() // 1

但是这条规则有几个需要注意的场景:

1
2
3
4
5
6
7
8
9
function f () {
console.log( this.a )
}
var o = {
a:1,
foo: foo
}
var fCopy = o.foo // 别名
fCopy() // TypeError: this is undefined

为什么会出现这种情况呢?因为实际上fCopyfoo 的引用, 是一个没有修饰符,即没有上下文对象的函数调用。

显示绑定

使用 callapply() 方法

1
2
3
4
5
6
function f () {
console.log( this.a )
}
var o = { a: 1}

f.call(o)

这个很明显了,就是手动的明确的,将f的this指向绑定到o上。

new 绑定

1
2
3
4
5
6
7
function F (a) {
this.a = a
}

var f = new F(1)

f.a // 1

js 的 new 实际上不是面向对象如java那种创建一个对象的实例,它是基于原型关系的。

先创建了一个对象,然后连接到到原型上。关于这一块,可以参考 JavaScript 实现复用那篇文章。

优先级别

new > 显示绑定 > 隐式绑定 > 默认绑定

总结

来自Daily-Interview-Question

Q:介绍 HTTPS 握手过程

  1. Client Hello
  2. Server Hello
  3. Certificate
  4. Server Hello Done
  5. Client Key Exchange
  6. Change Cipher Spec
  7. Finished
  8. Change Cipher Spec
  9. Finished

步骤一:客户端发送请求(Client Hello)

首先,客户端向服务端提供加密信息的通信。

  1. 客户端支持的SSL的指定版本
  2. 客户端产生的随机数(Client Random, 稍后用于生成”对话密钥”
  3. 客户端支持的加密算法

步骤二:服务器回应(Sever Hello)

服务端收到请求,向客户端发出回应

  1. 确认使用的加密通信协议版本,比如TLS 1.0版本。如果浏览器与服务器支持的版本不一致,服务器关闭加密通信。
  2. 一个服务器生成的随机数(Server Random),稍后用于生成”对话密钥”。
  3. 确认使用的加密方法,比如RSA公钥加密。
  4. 服务器证书

第一次握手结束

步骤三:发送验证消息

SSL服务器将数字证书通过Certificate消息发送给SSL客户端,证书里面包含了网站地址,加密公钥,以及证书的颁发机构。

步骤四:服务器回应结束

SSL服务器发送Server Hello Done消息,通知SSL客户端版本和加密套件协商结束,开始进行密钥交换。

步骤五:

SSL客户端验证SSL服务器的证书合法后,如果不合法浏览器会提示。如果合法的话,利用证书中的公钥,SSL客户端随机生成的premaster secret(后续加密数据所需要的对称密钥),并通过Client Key Exchange消息发送给SSL服务器。

第二次握手结束

步骤六:

SSL客户端发送Change Cipher Spec消息,通知SSL服务器后续报文将采用协商好的密钥和加密套件进行加密和MAC计算。

步骤七:

SSL客户端计算已交互的握手消息(除Change Cipher Spec消息外所有已交互的消息)的Hash值,利用协商好的密钥和加密套件处理Hash值(计算并添加MAC值、加密等),并通过Finished消息发送给SSL服务器。SSL服务器利用同样的方法计算已交互的握手消息的Hash值,并与Finished消息的解密结果比较,如果二者相同,且MAC值验证成功,则证明密钥和加密套件协商成功。

步骤八:

同样地,SSL服务器发送Change Cipher Spec消息,通知SSL客户端后续报文将采用协商好的密钥和加密套件进行加密和MAC计算。

步骤九:

SSL服务器计算已交互的握手消息的Hash值,利用协商好的密钥和加密套件处理Hash值(计算并添加MAC值、加密等),并通过Finished消息发送给SSL客户端。SSL客户端利用同样的方法计算已交互的握手消息的Hash值,并与Finished消息的解密结果比较,如果二者相同,且MAC值验证成功,则证明密钥和加密套件协商成功。

第三次握手结束

Q:为什么有的编程规范要求用void 0 代替 undefined?
A: undefined 是一个全局变量,可以被修改。为了准备的表达“未定义”,所以使用void 0

Q:underfined 与 null 的区别?
A:underfined 是Underfined类型的一个值,是一个名为underfined的变量,表达的意思是“从未赋值”“从未定义”,而null是Nulll类型的一个值,是JavaSript语言的关键字,表达的意思是“定义了但是为空”

Q:0.1+0.2 不能 = 0.3?怎么比较浮点数?
A:Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON,浮点数的特性决定了等式两边相差了微小的值,应该比较等式两边之差是否小于最小精度值

Q:为什么parseInt推荐传入第二个参数?
A:在不传入第二个参数的时候,parseInt默认转换0x开头的16进制,还支持0开头的八进制,所以推荐在任何时候都传入第二个参数。parseFloat则直接把字符串作为十进制解析。多数情况下,Number是比parseInt和parseFloat更好的选择。

Q:instanceof、typeof 和 Object.prototype.toString的区别?
Q:[‘1’,’2’,’3’].map(parseInt)的结果?

Q: 一个页面如果有一万个Bottom如何绑定事件?
A:事件委托,绑定父节点

1
2
3
4
5
6
7
8
9
$('.list').on('click', 'li', function(event) { // 绑定事件到父节点
console.log($(event.target).html()); // 注意操作对象是event.target还是this,下面会有详细说明哦
});

$('.list').on('click', function(event) {
if (event.target.tagName === 'LI') { // 判断标签是不是li,注意tagName属性返回的是大写
console.log($(event.target).html());
}
});

Q: 我们现在要实现一个红绿灯,把一个圆形 div 绿色 3 秒,黄色 1 秒,红色 2 秒循环改变背景色
A:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function sleep(duration){
return new Promise(function(resolve){
setTimeout(resolve, duration);
})
}
async function changeColor(duration,color){
document.getElementById("traffic-light").style.background = color;
await sleep(duration);

}
async function main(){
while(true){
await changeColor(3000,"green");
await changeColor(1000, "yellow");
await changeColor(2000, "red");
}
}
main()

时间复杂度

大O表示法

1
2
3
4
5
6
7
8
9
int cal(int n) 
{
int sum = 0;
int i = 1;
for (; i <= n; ++i) {
sum = sum + i;
}
return sum;
}

一行代码执行的时间为unit_time
第4行第5行分别执行了n遍,所以是 2*n unit_time

T(n) = O(f(n))
T(n)表示代码执行的时间;n表示数据规模的大小;f(n)表示每行代码执行的次数总和。因为这是一个公式,所以用f(n)来表示。公式中的O,表示代码的执行时间T(n)与f(n)表达式成正比。

时间复杂度分析

  1. 只关注执行次数最多的一段代码
  2. 加法法则:总复杂度等于量级最大的那段代码的复杂度
  3. 乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积

最好、最坏时间复杂度

1
2
3
4
5
6
7
8
9
10
11
int find(int[] array, int n, int x) 
{
int i = 0;
int pos = -1;
for (; i < n; ++i)
{
if (array[i] == x)
pos = i;
}
return pos;
}

代码中时间度复杂度会因为x在不在数组中变化,如果x在数组中则O(n)=1,否则O(n)=n。

平均时间复杂度

要查找的变量x在数组中的位置,有n+1种情况:在数组的0~n-1位置中和不在数组中。我们把每种情况下,查找需要遍历的元素个数累加起来,然后再除以n+1,就可以得到需要遍历的元素个数的平均值:

$$
\frac{1+2+3+\cdots+n+n}{n+1}=\frac{n(n+3)}{2(n+1)}
$$

考虑到在数组里和不在数组里的概率,以及出现在每个位置的概率,实际上应该是
$$
\begin{aligned} & 1 \times \frac{1}{2 n}+2 \times \frac{1}{2 n}+3 \times \frac{1}{2 n}+\dots+n \times \frac{1}{2 n}+n \times \frac{1}{2}\ = &\frac{3 n+1}{4} \end{aligned}
$$

用大O表示法来表示,去掉系数和常量,这段代码的加权平均时间复杂度仍然是O(n)。

均摊时间复杂度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// array表示一个长度为n的数组 
// 代码中的array.length就等于n
int[] array = new int[n];
int count = 0;
void insert(int val)
{
if (count == array.length)
{
int sum = 0;
for (int i = 0; i < array.length; ++i)
{
sum = sum + array[i];
}
array[0] = sum;
count = 1;
}
array[count] = val;
++count;
}

$$
1 \times \frac{1}{n+1}+1 \times \frac{1}{n+1}+\dots+1 \times \frac{1}{n+1}+n \times \frac{1}{n+1}=O(1)
$$

每一次O(n)的插入操作,都会跟着n-1次O(1)的插入操作,所以把耗时多的那次操作均摊到接下来的n-1次耗时少的操作上,均摊下来,这一组连续的操作的均摊时间复杂度就是O(1)。

思考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 全局变量,大小为10的数组array,长度len,下标i。
int array[] = new int[10];
int len = 10;
int i = 0;
// 往数组中添加一个元素
void add(int element)
{
if (i >= len) // 数组空间不够了 重新申请一个2倍大小的数组空间
{
int new_array[] = new int[len*2];
// 把原来array数组中的数据依次copy到new_array
for (int j = 0; j < len; ++j)
{
new_array[j] = array[j];
}
// new_array复制给array,array现在大小就是2倍len了
array = new_array;
len = 2 * len;
} // 将element放到下标为i的位置,下标i加一
array[i] = element;
++i;
}

时间复杂度:
最好:O(1)
最坏:O(n)
均摊:O(1) 因为前N个操作都是O(1),最后一个均摊到前n个

空间复杂度:
最好:O(1)
最坏:O(n)

在实现面向对象的编程中,有两种不同的描述对象的方式。一是以Java、C++为代表的基于类的编程语言,二是以JavaScript为代表的基于原型的编程语言。
基于“类”关心分类与类,基于“原型”则更关心对象实例。基于“类”的语言总是先有类,然后再去实例化一个对象,类与类之间可以形成继承、组合等关系。但基于”原型“的语言是通过“复制”来创建新的对象。
为了使JavaScript更接近基于“类”的实现方式,社区里曾有过不少接近于类Java的风格方言。但在ES6出现后,class已经成为一个关键字,不需要模拟即可实现“类”,但实际上这个“类”还是基于原型对象之上的。

利用class实现继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Animal{
constructor (name) {
this.name = name
}

run() {
console.log(`${this.name}可以跑`)
}
}

class Cat extends Animal {
constructor(name) {
super(name)
}

run(){
console.log(`${this.name}可以四肢腿跑`)
}
}

class Duck extends Animal {
constructor(name) {
super(name)
}

run(){
console.log(`${this.name}可以两只腿跑`)
}
}

let cat = new Cat('Tom')
let duck = new Duck('Donald')

cat.run() // Tom可以四肢腿跑
duck.run()// Donald可以两只腿跑

当然,ES6在操作对象上也提供了几个方法,使得基于原型的思想也同样可以实现继承。提供的方法如下:

  • Object.create
  • Object.getPrototypeOf
  • Object.sePrototypeOf
    这三个方法具体如何使用请参阅MDN

用原型实现的继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let animal = {
run(){
console.log('我可以跑')
}
}

let cat = Object.create(animal,{
say(){
console.log('我是cat')
}
})

let duck = Object.create(animal,{
say(){
console.log('我是duck')
}
})

let someCat = Object.create(cat)
let someDuck = Object.create(duck)

someCat.say()
someDuck.say()

一年总是转瞬即逝,大学生涯在这一年悄悄的结束,曾经很难想像的生活就这样猝不及防的到了自己面前,好像什么也没准备好就被推到了生活的前面,成了潮水本身。好像越是长大,对时间的感知越是迟钝。十年之前,一年好像是很久很久才会过完的,而到了如今一年又一年,不过一眨眼的事情。

Read more »