JS基础巩固系列:【5】js创建对象的几种方法

使用Object函数创建

1
2
3
var person = new Object();
person.name = 'zhangsan';
person.age = 14;

缺点:大量的重复代码

使用对象字面量创建

1
2
3
4
var person = {
name: 'zhangsan',
age: 14
}

缺点:大量的重复代码

使用工厂模式创建

1
2
3
4
5
6
7
function createPerson(name, age){
var o = new Object();
o.name = name;
o.age = age;
return o;
}
var person = createPerson('zhangsan', 14)

优点:解决了代码量大的问题
缺点:生成的对象缺乏识别性

使用自定义构造函数模式创建

1
2
3
4
5
6
7
8
function Person(name, age){
this.name = name;
this.age = age;
this.sayName = fucntion(){
alert(this.name)
}
}
var person = new Person('zhangsan', 14)

优点:对象有了识别性
缺点:sayName这个方法每次都会生成一次,浪费内存

使用原型模式创建

1
2
3
4
5
6
7
8
9
function Person(){}
Person.proptotype = {
name: 'zhangsan',
age: 14,
sayName: fucntion(){
alert(this.name)
}
}
var person = new Person()

优点:将共有的属性和方法抽到了原型上,避免了重复声明
缺点:生成的对象都是一致的,没有了自定义的属性

使用原型模式和构造函数创建

1
2
3
4
5
6
7
8
9
function Person(name, age){
this.name = name;
this.age = age
}
Person.proptotype.sayName = fucntion(){
alert(this.name)
}
var person1 = new Person('zhangsan', 14);
var person2 = new Person('lisi', 14);

优点:将共有的属性和方法抽到了原型上,自定义的属性通过构造函数重新声明,这是目前使用最广泛、认同度最高的方法

JS基础巩固系列:【4】instanceof的比较原理

js instanceof 的实现类比如下:

1
2
3
4
5
6
7
8
9
10
11
function instance_of(L, R) {//L 表示左表达式,R 表示右表达式
var O = R.prototype;// 取 R 的显示原型
L = L.__proto__;// 取 L 的隐式原型
while (true) {
if (L === null)
return false;
if (O === L)// 这里重点:当 O 严格等于 L 时,返回 true
return true;
L = L.__proto__;
}
}

根据instanceof的比较逻辑,修改代码,使得cat instanceof Animal结果为true

原代码:

1
2
3
4
5
function Animal(){}

function Cat(){}
var cat = new Cat();
console.log(cat instanceof Animal); //true

修改方案1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Animal(){}

function Cat(){
this.__proto__ = new Animal();
}
var cat = new Cat();

function instance_of(L, R) {//L 表示左表达式,R 表示右表达式
var O = R.prototype;// 取 R 的显示原型
L = L.__proto__;// 取 L 的隐式原型
var i = 1;
while (true) {
console.log(`第${i}次比较`, L, O)
if (L === null)
return false;
if (O === L)// 这里重点:当 O 严格等于 L 时,返回 true
return true;
L = L.__proto__;
i++;
}
}
console.log(cat instanceof Animal); //true
//instance_of(cat, Animal)

这里执行了2次比较:

  • cat.__proto__和Animal.prototype,Animal的实例和Animal.prototype比较,不等于;
  • Animal的实例的__proto__ 和 Animal.prototype,相等;

修改方案2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Animal(){}
function Cat(){}
Cat.prototype = new Animal();
var cat = new Cat();

function instance_of(L, R) {//L 表示左表达式,R 表示右表达式
var O = R.prototype;// 取 R 的显示原型
L = L.__proto__;// 取 L 的隐式原型
var i = 1;
while (true) {
console.log(`第${i}次比较`, L, O)
if (L === null)
return false;
if (O === L)// 这里重点:当 O 严格等于 L 时,返回 true
return true;
L = L.__proto__;
i++;
}
}
console.log(cat instanceof Animal); //true
//instance_of(cat, Animal)

这里执行了2次比较:

  • cat.__proto__和Animal.prototype,即Animal的实例和Animal.prototype比较,不等于;
  • Animal的实例的__proto__和Animal.prototype比较,相等;

JS基础巩固系列:【3】js垃圾回收机制

垃圾回收机制的必要性

我们引用《JavaScript权威指南(第四版)》的一段话来解释一下:由于字符串、对象和数组没有固定大小,所有当他们的大小已知时,才能对他们进行动态的存储分配。JavaScript程序每次创建字符串、数组或对象时,解释器都必须分配内存来存储那个实体。只要像这样动态地分配了内存,最终都要释放这些内存以便他们能够被再用,否则,JavaScript的解释器将会消耗完系统中所有可用的内存,造成系统崩溃。

js是如何实现垃圾回收的

js有自己的一套垃圾回收机制(Garbage Collection)。JavaScript的解析器可以监测到何时程序不再使用一个对象了,当确认这个对象是无用的时候,就可以把他的内存释放掉了,上例子:

1
2
3
var a = 'before';
var b = 'override a';
a = b;

这段代码运行后,’before’已经失去了引用,系统检测到后,就会释放’before’所占的存储空间。

垃圾回收原理

现在各大浏览器通常使用的垃圾回收有两种方式:标记清除和引用计数

  1. 标记清除
    这是 JavaScript 中最常见的垃圾回收方式。为什么说这是种最常见的方法,因为从 2012 年起,所有现代浏览器都使用了标记-清除的垃圾回收方法,除了低版本 IE…它们采用的是引用计数方法。
    那什么叫标记清除呢?JavaScript 中有个全局对象,浏览器中是 window。定期的,垃圾回收期将从这个全局对象开始,找所有从这个全局对象开始引用的对象,再找这些对象引用的对象…对这些活着的对象进行标记,这是标记阶段。清除阶段就是清除那些没有被标记的对象。
    工作流程:
  • 垃圾收集器会在运行的时候会给存储在内存中的 所有变量都加上标记 。
  • 去掉环境中的变量以及被环境中的变量引用的变量的标记。
  • 那些还存在标记的变量被视为准备删除的变量。
  • 最后垃圾收集器会执行最后一步内存清除的工作,销毁那些带标记的值并回收它们所占用的内存空间。
    标记-清除法的一个问题就是不那么有效率,因为在标记-清除阶段,整个程序将会等待,所以如果程序出现卡顿的情况,那有可能是收集垃圾的过程。
    标记-清除还有一个问题,就是在清除之后,内存空间是不连续的,即出现了内存碎片。如果后面需要一个比较大的连续的内存空间时,那将不能满足要求。而标记-整理方法可以有效地解决这个问题。标记阶段没有什么不同,只是标记结束后,标记-整理方法会将活着的对象向内存的一边移动,最后清理掉边界的内存。不过可以想象,这种做法的效率没有标记-清除高。
  1. 引用计数
    在内存管理环境中,对象 A 如果有访问对象 B 的权限,叫做对象 A 引用对象 B。引用计数的策略是将“对象是否不再需要”简化成“对象有没有其他对象引用到它”,如果没有对象引用这个对象,那么这个对象将会被回收。上例子:
    1
    2
    3
    4
    let obj1 = { a: 1 }; // 一个对象(称之为 A)被创建,赋值给 obj1,A 的引用个数为 1 
    let obj2 = obj1; // A 的引用个数变为 2
    obj1 = 0; // A 的引用个数变为 1
    obj2 = 0; // A 的引用个数变为 0,此时对象 A 就可以被垃圾回收了

但是引用计数有个最大的问题: 循环引用。

1
2
3
4
5
6
7
function func() {
let obj1 = {};
let obj2 = {};

obj1.a = obj2; // obj1 引用 obj2
obj2.a = obj1; // obj2 引用 obj1
}

当函数 func 执行结束后,返回值为 undefined,所以整个函数以及内部的变量都应该被回收,但根据引用计数方法,obj1 和 obj2 的引用次数都不为 0,所以他们不会被回收。
这种情况我们可以通过,给将变量赋值为null,来释放其引用。这个方法被称为“解除引用”

JS基础巩固系列:【2】闭包

什么是闭包

函数A内部有一个函数B,函数B可以访问到函数A中的变量,那么函数B就是闭包。我个人的理解是:闭包就是能够读取其他函数内部变量的函数,并不需要return一个函数。

1
2
3
4
5
6
7
8
function A() {
let a = 1
window.B = function () {
console.log(a)
}
}
A()
B() // 1

闭包的作用

根据闭包的概念,我们可以理解到闭包最重要的作用就是可以将函数内部和函外部链接起来,他的作用有两处,一个是可以读取函数内部的变量,另一个是让这些变量的值始终保持在内存中,我们用一段代码来解释这两点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function f1(){
var n = 999;
nAdd = function(){
n+=1;
}
function f2(){
console.log(n)
}
return f2;
}

var result = f1();
result(); //999
nAdd();
result(); //1000

  1. 首先函数f1执行后将f2赋给了全局变量result,而函数f2在函数f1内部,所以在f2的内部可以访问到f1的变量n。
  2. 因为f2的存在依赖了f1中的变量n,所以使得f1也始终被存储在了内存中,在调用之后,不会被垃圾回收机制回收。(内部函数引用了位于外部函数的变量,当外部函数调用完毕后,这些变量在内存不会被释放,因为闭包需要它们)

使用闭包的注意点

由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

JS基础巩固系列:【1】this的指向问题

普通的调用场景

1
2
3
4
5
6
7
8
9
10
11
function foo() {
console.log(this.a)
}
var a = 1;
foo();
const obj = {
a: 2,
foo: foo
};
obj.foo();
const c = new foo();

我们针对上面几个场景分析:

  1. 对于直接调用 foo 来说,不管 foo 函数被放在了什么地方,this 一定是 window
  2. 对于 obj.foo() 来说,我们只需要记住,谁调用了函数,谁就是 this,所以在这个场景下 foo 函数中的 this 就是 obj 对象
  3. 对于 new 的方式来说,this 被永远绑定在了 c 上面,不会被任何方式改变 this

特殊情况

箭头函数

1
2
3
4
5
6
7
8
function a() {
return () => {
return () => {
console.log(this)
}
}
}
console.log(a()()())

要理解上面这种情况,首先我们要明白几个知识点:

  1. 箭头函数其实是没有 this 的,箭头函数中的 this 只取决包裹箭头函数的第一个普通函数的 this,对于上面的例子,包裹箭头函数的第一个普通函数是 a,所以此时的this是window。
  2. 对于箭头函数,使用bind这类函数是无效的

bind

1
2
3
4
5
6
7
let a = { 
name: 'test'
};
function foo() {
console.log(this)
}
foo.bind(a)();
  • 使用bind一类可以改变上下文的API,this 取决于第一个参数,如果第一个参数为空,那么就是 window。
1
2
3
4
5
let a = {}
let fn = function () {
console.log(this)
}
fn.bind().bind(a)()
  • 对于上面的例子,如果对一个函数进行多次bind,这个时候的this会是谁呢?

上述代码可以转换成另外一种方式

1
2
3
4
5
6
7
// fn.bind().bind(a) 等于
let fn2 = function fn1() {
return function() {
return fn.apply()
}.apply(a)
}
fn2()

从这个函数来看,我们可以发现,不管给函数 bind 了多少次,fn中的this,永远是由第一次 bind 决定的,所以这里this的结果永远是 window。

以上就是 this 的规则了,但是可能会发生多个规则同时出现的情况,这时候不同的规则之间会根据优先级最高的来决定 this 最终指向哪里。

首先,new 的方式优先级最高,接下来是 bind 这些函数,然后是 obj.foo() 这种调用方式,最后是 foo 这种调用方式,同时,箭头函数的 this 一旦被绑定,就不会再被任何方式所改变。

上面的逻辑,汇总到流程图中的规则如下,仅适用单个规则的情况:
Image text

顺便提一下,js可以改变上下文的API包含 apply、call以及bind,三者的关系如下

  • apply 、 call 、bind 三者都是用来改变函数的this对象的指向的;
  • apply 、 call 、bind 三者第一个参数都是this要指向的对象,也就是想指定的上下文。非严格模式下,如果第一个参数传入null或者undefined,this会指向window;
  • apply 、 call 、bind 三者都可以利用后续参数传参,apply第二个参数是一个参数数组,call接受一个参数列表;
  • bind 是返回对应函数,便于稍后调用;apply 、call 则是立即调用。

nginx常用功能

简介

Nginx 是俄罗斯人编写的十分轻量级的 HTTP 服务器,Nginx,它的发音为“engine X”,是一个高性能的HTTP和反向代理服务器,同时也是一个 IMAP/POP3/SMTP 代理服务器。其特点是占有内存少,并发能力强,事实上nginx的并发能力确实在同类型的网页服务器中表现较好,中国大陆使用nginx网站用户有:百度、京东、新浪、网易、腾讯、淘宝等。

主要功能以及用途

  1. 正向代理(内网服务器主动去请求外网服务的一种行为)
    客户端无法主动或者不打算主动去向某服务器发送请求,而是委托nginx代理服务器去向服务器发送请求,并且获得处理结果,返回给客户端。VPN的原理大体上类似于一个正向代理,需要访问外网的电脑,发起一个访问外网的请求,通过本机上的VPN去寻找一个可以访问国外网站的代理服务器,代理服务器向外国网站发送请求,然后把结果返回给本机。
  2. 反向代理
    反向代理是指用代理服务器来接受客户端发来的请求,然后将请求转发给内网中的上游服务器,上游服务器处理完之后,把结果通过nginx返回给客户端。

  3. 透明代理
    透明代理:也叫做简单代理,意思客户端向服务端发起请求时,请求会先到达透明代理服务器,代理服务器再把请求转交给真实的源服务器处理,也就是是客户端根本不知道有代理服务器的存在。

  1. 负载均衡
    将服务器接收到的请求按照规则分发的过程,称为负载均衡。负载均衡是反向代理的一种体现。
    负载均衡的模式:
    1. 轮询:每个请求按时间顺序逐一分配到不同的后端服务器,也是nginx的默认模式。轮询模式的配置很简单,只需要把服务器列表加入到upstream模块中即可。
    2. ip_hash:每个请求按访问IP的hash结果分配,同一个IP客户端固定访问一个后端服务器。可以保证来自同一ip的请求被打到固定的机器上,可以解决session问题。
    3. url_hash:按访问url的hash结果来分配请求,相同的url固定转发到同一个后端服务器处理。
    4. fair:按后端服务器的响应时间来分配请求,响应时间短的优先分配。
      每一种模式中,每一台服务器后面可以携带的参数有:
    • down: 当前服务器暂不参与负载
    • weight: 权重,值越大,服务器的负载量越大。
    • max_fails:允许请求失败的次数,默认为1。
    • fail_timeout:max_fails次失败后暂停的时间。
    • backup:备份机, 只有其它所有的非backup机器down或者忙时才会请求backup机器。
  2. 静态服务器

使用requestAnimationFrame做性能优化

window.requestAnimationFrame() 方法告诉浏览器您希望执行动画并请求浏览器在下一次重绘之前调用指定的函数来更新动画。该方法使用一个回调函数作为参数,这个回调函数会在浏览器重绘之前调用。

语法

window.requestAnimationFrame(callback);

参数

callback
一个指定函数的参数,该函数在下次重新绘制动画时调用。这个回调函数只有一个传参,DOMHighResTimeStamp,指示requestAnimationFrame() 开始触发回调函数的当前时间(performance.now() 返回的时间)。

返回值

一个 long 整数,请求 ID ,是回调列表中唯一的标识。是个非零值,没别的意义。你可以传这个值给 window.cancelAnimationFrame() 以取消回调函数。

以下示例为页面渲染100000条数据时的优化方案

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
36
37
38
39
40
41
42
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<ul>控件</ul>
<script>
setTimeout(() => {
// 插入十万条数据
const total = 100000;
// 一次插入 20 条,如果觉得性能不好就减少
const once = 20;
// 渲染数据总共需要几次
const loopCount = total / once;
let countOfRender = 0;
let ul = document.querySelector("ul");
function add() {
// 优化性能,插入不会造成回流
const fragment = document.createDocumentFragment();
for (let i = 0; i < once; i++) {
const li = document.createElement("li");
li.innerText = Math.floor(Math.random() * total);
fragment.appendChild(li);
}
ul.appendChild(fragment);
countOfRender += 1;
loop();
}
function loop() {
if (countOfRender < loopCount) {
window.requestAnimationFrame(add);
}
}
loop();
}, 0);
</script>
</body>
</html>