My Little World

learn and share


  • 首页

  • 分类

  • 标签

  • 归档

  • 关于
My Little World

懒加载

发表于 2017-12-01

lazy

图片lazyload

基本原理是图片image标签的src属性在构建DOM时,先给一特定的默认值,可以是本地图片路径,然后通过监听scroll事件,
判断图片所在位置是否在可视屏幕里面,如果在可视屏幕里面,就将要加载的真正图片资源给到image的src属性
当然通过判断src的值是否是默认值,就可以知道图片是否已经被加载过,加载过就不会被从新赋值,然后重新加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function lazyload() {
const images = document.getElementsByTagName('img')
const len = images.length
let n = 0
return function() {
console.log(1)
const seeHeight = document.documentElement.clientHeight
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop
for (let i = n; i < len; i++) {
if (images[i].offsetTop < seeHeight + scrollTop) { //是否在可视范围
if (images[i].getAttribute('src') === '默认图片的url') {
images[i].src = images[i].getAttribute('data-src') //赋值资源链接
}
n = n + 1
}
}
}
}
var loadImages = lazyload()
window.onload = function () {
loadImages()
window.addEventListener('scroll', loadImages, false)
}

但缺点就是,只要触发了scroll事件,就会去调函数,无论图片有没有被加载,这样就会造成一些没用的调用,所以对此进行改进

改进方式一:使用节流阀(Throttle)

响应函数函数在一定时间内只允许被调用一次,函数能否被执行根据时间来确定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function throttle(fn, delay, atleast) {
let timeout = null
let startTime = new Date()
return function() {
const curTime = new Date()
clearTimeout(timeout) //3,取消限定时间内预约的将来的函数执行,防止多次执行
if (curTime - startTime >= atleast) {//1,超过限定时间,执行一次
fn()
startTime = curTime
} else {
timeout = setTimeout(fn, delay) //2,没超过限定时间,在限定时间内不执行,延续时间到超过限定时间后再执行
}
}
}
var loadImages = lazyload()
window.onload = function () {
loadImages()
window.addEventListener('scroll', throttle(loadImages, 500, 1000), false)
}

改进方式二:使用防抖动技术(debounce)

当事件发生时,不会立即激活回调。
等待一定的时间并检查相同的事件是否再次触发。
如果是,重置定时器,并再次等待。
如果在等待期间没有发生相同的事件,等待时间结束后就立即激活回调。

1
2
3
4
5
6
7
8
9
10
11
12
13
function debounced(fn,delay) {
clearTimeout(timeoutID); // reset timer
timeoutID = setTimeout(function() {
// wait for some time
// and check if event happens again
fn()
}, delay);
};
var loadImages = lazyload()
window.onload = function () {
loadImages()
window.addEventListener('scroll', debounced(loadImages, 500), false)
}

组件的lazyload

实现一:AMD模式require实现异步加载模块
在AMD模式里面每个模块都会被define包裹,对无序的代码进行有序的模块化定义,目的就是为了使js能够按照一定秩序执行,
require模块时,会根据模块之间的依赖关系按顺序加载

实现二:使用require.ensure()方法调用异步模块,配置chunkFilename为异步模块要打包到的地方,webpack打包时,就会根据依赖关系打包成异步加载的模式,在运行时就会异步加载模块

实现三:将组建都标签化后,通过对标签添加是否异步的标志,从而实现对该组件的异步加载

预加载技术

1.提前加载下一页数据
2.加载页面时,尽早发出数据请求,实现页面数据预加载

图片的 base64 编码

图片的 base64 编码就是可以将一图片数据编码成一串字符串,使用该字符串代替代码中的图像地址
图片就可以随着HTML的下载同时下载到本地,不用为了下载图片向服务器发出请求

将图片转化为Base64编码的工具,可以使用线上网站,也可以用以下方法
在 chrome 下打开新的标签页,然后把要转化的图片直接拖入浏览器,打开控制台,点 Source,source的内容内容就是图片base64编码

可以看到一个图片的base64编码的字符数量一般是很大的,这样,当把图片编码无论是写到css文件还是html文件,
都会因为解析时间过长从而造成渲染速度下降,因此将图片进行base64编码最好的应用场景是如下情况:
如果图片足够小且因为用处的特殊性无法被制作成雪碧图(CssSprites),在整个网站的复用性很高且基本不会被更新

base64 编码

Angular会自动为每个拥有作用域的DOM节点加上 ng-scope 类
AngularJs的angular.extend()方法可以把一个或多个对象中的方法和属性扩展到一个目的对象中,使得这个对象拥有其他对象相同的方法和属性
angular.extends()方法的第一个参数是要被扩展的对象,后面可以传入一个或多个对象

补充

优化占位图

VG元素/提取图片背景色以base64形式直出
使用SVG画图形轮廓,再加一个模糊滤镜
对图片进行二值化提取剪影

浏览器渲染图片原理

构建DOM树,遇到img标签加载图片
构建样式树,遇到backgroud-img图片不加载
构建render树,所有属性都会构建,如果元素有display:none属性,则其子元素不被构建
渲染DOM树,仅渲染没有display:none属性的元素,如果发现元素有该属性则不进行渲染;没有被构建的子元素背景图片不会被加载更不会被加载

设置了display:none属性的元素,图片不会渲染出来,但会加载,不管是直接的img属性还是div的背景图片都会被加载

1
2
3
4
5
6
7
8
<img src="https://cdn-jlsq-img.thy360.com/2e3457ef739c4116847eb359dffcf651.jpg!thumbnail"> //加载
<div class="skslsl"></div>//加载
<style type="text/css">
.skslsl{
background-image:url('https://cdn-jlsq-img.thy360.com/3aeac46295cb4f15bfb1154692e00c95.JPEG');

}
</style>

设置了display:none属性元素的子元素,样式表中的背景图片不会渲染出来,也不会加载;而img标签的图片不会渲染出来,但会加载。

1
2
3
4
5
6
7
8
9
10
11
<div style="display: none;">
<img src="https://cdn-jlsq-img.thy360.com/2e3457ef739c4116847eb359dffcf651.jpg!thumbnail"> //加载
<div class="skslsl"></div>//不加载
</div>

<style type="text/css">
.skslsl{
background-image:url('https://cdn-jlsq-img.thy360.com/3aeac46295cb4f15bfb1154692e00c95.JPEG');

}
</style>

当触发伪类的时候,伪类样式上的背景图片才会加载。

重复图片只加载一次,是因为加载一次后,之后的加载均从缓存中读取;
链接

My Little World

AngularJS issue

发表于 2017-10-31

#$digest和$apply

当调用$digest的时候,只触发当前作用域和它的子作用域上的监控,但是当调用$apply的时候,会触发作用域树上的所有监控。

因此,从性能上讲,如果能确定自己作的这个数据变更所造成的影响范围,应当尽量调用$digest,只有当无法精确知道数据变更造成的影响范围时,才去用$apply,很暴力地遍历整个作用域树,调用其中所有的监控。

视图模型的层次

在Angular中,存在作用域的继承。所谓作用域的继承,是指:如果两个视图有包含关系,内层视图对应的作用域可以共享外层视图作用域的数据。
以下情况会造成嵌套作用域

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
43
44
45
46
47
// 1. controller 套嵌
如果两个控制器所对应的视图存在上下级关系,它们的作用域就自动产生继承关系
html:
<body ng-app="test">
<div ng-controller="OuterCtrl">//父域
<span ng-bind="a"></span>
<div ng-controller="InnerCtrl">//子域
<span ng-bind="a"></span>
<span ng-bind="b"></span>
</div>
</div>
</body>
js:
var app = angular.module("test", []);

app.controller("OuterCtrl", function ($scope) {
$scope.a = 1;
});

app.controller("InnerCtrl", function ($scope) {
$scope.b = 100;

$scope.increasea = function() {
$scope.a++;
};
});

//2.数组和对象属性迭代的时候,循环的每个元素都建立了单独的作用域
<ul>
<li ng-repeat="member in members">{{member.name}} in {{teamname}}</li> //每一个都是子域,但可以共享父域的teamname
</ul>

//3.使用ng-if,ng-include,ng-view等指令时构建的DOM,会形成自己的子域

inner.html
<div>
<span ng-bind="name"></span>
</div>

outer.html
<div ng-controller="OuterCtrl">
<span ng-bind="name"></span>
<div ng-include="'inner.html'"></div>//ng-include会创建一层作用域,inner.html里面bind的name会使用OuterCtrl这个父域里面的name
</div>
function OuterCtrl($scope) {
$scope.name = "outer name";
}

作用域继承的缺点就是,子域绑定值能继承父域同名绑定值的值,但是一旦子域绑定值,因为某种触发函数发生改变,父域同名绑定值不会随之改变
虽然可以通过$parent获取父域的同名绑定值,然后子域中做到同时修改,但如果在代码中使用了这种方式,意味着视图模型也只能这样包含,如果再中间插一层子域,
则同名关系变成$parent.$parent,使关系进一步复杂
所以,应当尽量避免父子作用域存在同名变量的情况

从可重用性角度来看,如果满分5分的话,整个应用的这些部分的得分应当是这样:

服务,比如说,对后端RESTful接口的AJAX调用,对本地存储的访问等,5分
控制器(也就是视图模型),2-3分
指令,这个要看情况,有的指令是当作对HTML元素体系的扩展来用的,有些是其他事情的
纯UI类型的指令,也可以算是控件,比如DatetimePicker,5分
有些用于沟通DOM跟视图模型的指令,2分
界面模板,这个基本就没有重用性了,1分

模块机制-module的坑

1.覆盖问题
如果moduleA,同时依赖moduleB和moduleC,而moduleB和moduleC有同名的factoryD,那在moduleA使用factoryD的时候,
根据依赖顺序,如果moduleB在前,则moduleC的factoryD会覆盖moduleB的factoryD,moduleA使用的是moduleC的factoryD
如果在以上依赖情况基础上,moduleA自己又定义了同名factoryD,那么moduleA自己的factoryD会覆盖moduleC的factoryD

2.module不支持运行时添加依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
angular.module("some.components", [])
//这里定义了一些组件
;
//上面是一个组件库,集中存放于components.js中,要在自己的应用中使用,必须:
angular.module("our.app", ["some.components"]);

//不可以这样
angular.module("our.app", []);

require("components.js", function() {
// angular.module("our.app").addDependency("some.components");
// ready to use
});

表达式

1.在绑定表达式里面,只能使用自定义函数,不能使用原生函数

1
<div>{{abs(-1)}}</div>//不允许这样使用

如果确实需要调用原生函数,可以用一个自定义函数作包装,在自定义函数里面可以随意使用各种原生对象
绑定表达式里可以使用自定义函数,但如果只是对数据做简单处理可以使用过滤器

2.数组里有重复元素的情况,ng-repeat代码不能起作用,原因是Angular默认需要在数组中使用唯一索引
可以指定它使用序号作索引

1
2
3
4
$scope.arr2 = [1, 1, 3];
<ul>
<li ng-repeat="item in arr2 track by $index">{{item}}</li>
</ul>

但对象值有重复的话,不用像数组那么麻烦需要指定$index做索引,因为它是对象的key做索引,是不会重复的。

事件流

angular事件流是一个通知广播的过程
现有A、B两个视图,每个视图又包含各自两个子视图,如果A视图的A1子视图想通知B视图的子视图B1一个业务事件(或传递某个消息)
那A1就发出业务通知,这个通知会沿着父作用域一路往上到达双方共同的祖先作用域
然后这个通知会以广播的形式从祖先作用域一级一级往下进行广播,直到到达需要的地方
issue1
相关事件:
从作用域往上发送事件,使用scope.$emit。

1
$scope.$emit("someEvent", {});

从作用域往下发送事件,使用scope.$broadcast

1
$scope.$broadcast("someEvent", {});

这两个方法的第二个参数是要随事件带出的数据。
这两种方式传播事件,事件的发送方自己也会收到一份。
无论是$emit还是$broadcast发送的事件,都可以被接收,接收这两种事件的方式是一样的:

1
2
3
$scope.$on("someEvent", function(e) {
// 这里从e上可以取到发送过来的数据
});

如果想要阻止$emit事件的继续传播,可以调用事件对象的stopPropagation()方法

1
2
3
$scope.$on("someEvent", function(e) {
e.stopPropagation();
});

想要阻止$broadcast事件的传播,首先,调用事件对象的preventDefault()方法,
然后,在收取这个事件对象的时候,判断它的defaultPrevented属性,如果为true,就忽略此事件。
这个过程比较麻烦,其实一般是不需要管的,只要不监听对应的事件就可以了。
在实际使用过程中,也应当尽量少使用事件的广播,尤其是从较高的层级进行广播
上级作用域

1
2
3
$scope.$on("someEvent", function(e) {
e.preventDefault();
});

下级作用域

1
2
3
4
5
$scope.$on("someEvent", function(e) {
if (e.defaultPrevented) {
return;
}
});

事件总线

事件流的通知过程效率低,而且很多下级视图可能根本不要接收消息,进行多余的广播,
所以组建订阅发布模式,接收方在这里订阅消息,发布方在这里发布消息,这个地方就是事件总线

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
app.factory("EventBus", function() {
var eventMap = {};

var EventBus = {
on : function(eventType, handler) { //订阅事件
//multiple event listener
if (!eventMap[eventType]) {
eventMap[eventType] = [];
}
eventMap[eventType].push(handler);
},

off : function(eventType, handler) {
for (var i = 0; i < eventMap[eventType].length; i++) {
if (eventMap[eventType][i] === handler) {
eventMap[eventType].splice(i, 1);
break;
}
}
},

fire : function(event) { //发布事件
var eventType = event.type;
if (eventMap && eventMap[eventType]) {
for (var i = 0; i < eventMap[eventType].length; i++) {
eventMap[eventType][i](event);
}
}
}
};
return EventBus;
});

My Little World

AngularJS Digest

发表于 2017-10-28

实现原理

通常angularjs 环境中变量都绑定在Scope对象上,检测这些变量变化的机制也绑定在该对象上

为了检测数据发生变化,所以为每一个变量创建监听器
监听器包含两部分内容
一个监控函数,负责前后数据对比
一个监听函数,规定对数据变化做出什么响应

但是监听器需要有机制进行触发才能发挥作用
即调用监控函数,看数据是否发生变化,然后根据结果再调用监听函数,对变化做出响应

因此,在Scope对象上挂载函数和属性实现以上机制

Scope.$$watchers = [] 用于保存注册过的所有监听器

Scope.prototype.$watch = function(watchFn, listenerFn) {} 用于将监控函数和监听函数组合成监听器,然后保存到$$watchers

1
2
3
4
5
6
7
Scope.prototype.$watch = function(watchFn, listenerFn) {
var watcher = {
watchFn: watchFn,//监控函数
listenerFn: listenerFn || function() { } //监听函数
};
this.$$watchers.push(watcher);
};

Scope.prototype.$$digestOnce = function() {}
具体的脏检查过程,初始化变更标记,遍历一遍$$watchers,
如果某个数据发生变化就执行对应监听函数,
并将变更标记设置true返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Scope.prototype.$$digestOnce = function() {
var self = this;
var dirty;
_.forEach(this.$$watchers, function(watch) {
var newValue = watch.watchFn(self);
var oldValue = watch.last;
if (newValue !== oldValue) {
watch.listenerFn(newValue, oldValue, self);
dirty = true;
}
watch.last = newValue;
});
return dirty;
};

Scope.prototype.$digest = function() {} 循环执行检测,保证监控器内部对属性进行的变更也能被检测到

1
2
3
4
5
6
7
8
9
10
Scope.prototype.$digest = function() {
var ttl = 10;
var dirty;
do {
dirty = this.$$digestOnce();
if (dirty && !(ttl--)) {
throw "10 digest iterations reached";
}
} while (dirty);
};

如果仅执行一次digest,检测不到B监听器监听函数修改的A监听器监听的属性新值,
因此进行持续遍历所有监听器,直到监控的值停止变更
$digest为$$digestOnce包裹一个do-while“外层循环”,
如果第一次运行完,有监控值发生变更了,标记为dirty,所有监听器再运行第二次。这会一直运行,直到所有监控的值都不再变化,整个局面稳定下来了。
如果两个监听器互相监控了对方产生的变更,状态始终不会稳定,因此添加迭代数量ttl,限制迭代次数,保证循环检测的可控性,达到ttl就抛异常

对于新旧值的判断 - $$areEqual

$$digestOnce中使用!==判断新旧值,可以判断值的引用但无法判断值的变更
因此在监控器里增加字段valueEq,用来定制判断方法,

1
2
3
4
5
6
7
8
Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) {
var watcher = {
watchFn: watchFn,
listenerFn: listenerFn,
valueEq: !!valueEq
};
this.$$watchers.push(watcher);
};

定制判断方法,抽离成函数

1
2
3
4
5
6
7
8
9
Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) {
if (valueEq) {
return _.isEqual(newValue, oldValue);
} else {
return newValue === oldValue ||
(typeof newValue === 'number' && typeof oldValue === 'number' &&
isNaN(newValue) && isNaN(oldValue));//对NaN特殊情况进行处理
}
};

判断方法发生改变,进而存储方法也要根据判断方式进行改变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Scope.prototype.$$digestOnce = function() {
var self = this;
var dirty;
_.forEach(this.$$watchers, function(watch) {
var newValue = watch.watchFn(self);
var oldValue = watch.last;
if (!self.$$areEqual(newValue, oldValue, watch.valueEq)) {
watch.listenerFn(newValue, oldValue, self);
dirty = true;
}
watch.last = (watch.valueEq ? _.cloneDeep(newValue) : newValue); //定制存储方式
});
return dirty;
};

Angular默认不使用基于值的脏检测的原因,用户需要显式设置这个标记去打开它。

集成外部代码与digest循环-$apply

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//$eval使用一个函数作参数,所做的事情是立即执行这个传入的函数,并且把作用域自身当作参数传递给它,
//返回的是这个函数的返回值。$eval也可以有第二个参数,它所做的仅仅是把这个参数传递给这个函数。
Scope.prototype.$eval = function(expr, locals) {
return expr(this, locals);
};
//$apply可以执行一些与Angular无关的代码(expr),这些代码也还是可以改变作用域上的东西,
//$apply可以保证作用域上的监听器可以检测这些变更。
Scope.prototype.$apply = function(expr) {
try {
return this.$eval(expr);
} finally {
this.$digest();
}
};

延迟执行 - $evalAsync

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
Scope.$$asyncQueue = [];//存储$evalAsync列入计划的任务
Scope.$$phase = null;//作用域上一个简单的字符串属性,存储了现在正在做的信息
//设置$$phase
Scope.prototype.$beginPhase = function(phase) {
if (this.$$phase) {
throw this.$$phase + ' already in progress.';
}
this.$$phase = phase;
};
//删除$$phase
Scope.prototype.$clearPhase = function() {
this.$$phase = null;
};

Scope.prototype.$digest = function() {
var ttl = 10;
var dirty;
this.$beginPhase("$digest"); //设置状态
do {
while (this.$$asyncQueue.length) {
var asyncTask = this.$$asyncQueue.shift();
this.$eval(asyncTask.expression);
}
dirty = this.$$digestOnce();
if (dirty && !(ttl--)) {
this.$clearPhase();
throw "10 digest iterations reached";
}
} while (dirty);
this.$clearPhase();//移除状态
};

Scope.prototype.$apply = function(expr) {
try {
this.$beginPhase("$apply");//设置状态
return this.$eval(expr);
} finally {
this.$clearPhase();//移除状态
this.$digest();
}
};

Scope.prototype.$evalAsync = function(expr) { 添加任务
Scope.prototype.$evalAsync = function(expr) {
var self = this;
if (!self.$$phase && !self.$$asyncQueue.length) {
console.log(1)
setTimeout(function() {利用异步,连续添加任务后一定会立即digest一次
console.log(3)
if (self.$$asyncQueue.length) {
self.$digest();
}
}, 0);
}
console.log(2)
self.$$asyncQueue.push({scope: self, expression: expr});
};

};
Scope.prototype.$digest = function() {
var ttl = 10;
var dirty;
do {
while (this.$$asyncQueue.length) {
var asyncTask = this.$$asyncQueue.shift();
this.$eval(asyncTask.expression);
}
dirty = this.$$digestOnce();
if (dirty && !(ttl--)) {
throw "10 digest iterations reached";
}
} while (dirty);
};

在监听函数中执行$evalAsync,digest第一次时延迟任务不会被执行,当dirty时,才会被执行
例

1
2
3
4
5
6
7
8
9
10
11
12
var scope = new Scope();
scope.asyncEvaled = false;

scope.$evalAsync(function(scope) {
scope.asyncEvaled = false;
});
console.log(4)
scope.$evalAsync(function(scope) {
scope.asyncEvaled = true;
});

console.log("Evaled after digest: "+scope.asyncEvaled);//true

在digest之后执行代码 - $$postDigest

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
this.$$postDigestQueue = [];//$$postDigest函数列入计划
Scope.prototype.$$postDigest = function(fn) {
this.$$postDigestQueue.push(fn);
};

Scope.prototype.$digest = function() {
var ttl = 10;
var dirty;
this.$beginPhase("$digest");
do {
while (this.$$asyncQueue.length) {
try {//执行函数时,使用try-catch进行异常处理
var asyncTask = this.$$asyncQueue.shift();
this.$eval(asyncTask.expression);
} catch (e) {
(console.error || console.log)(e);
}
}
dirty = this.$$digestOnce();
if (dirty && !(ttl--)) {
this.$clearPhase();
throw "10 digest iterations reached";
}
} while (dirty);
this.$clearPhase();

while (this.$$postDigestQueue.length) { //在digest最后执行
try {
this.$$postDigestQueue.shift()();
} catch (e) {
(console.error || console.log)(e);
}
}
};

销毁一个监听器

在注册监听器时,返回一个销毁该监听器的函数,如果将来要销毁该监听器,就将返回的函数保存
在销毁时,直接执行该函数即可

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
Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) {
var self = this;
var watcher = {
watchFn: watchFn,
listenerFn: listenerFn,
valueEq: !!valueEq
};
self.$$watchers.push(watcher);
return function() {
var index = self.$$watchers.indexOf(watcher);
if (index >= 0) {
self.$$watchers.splice(index, 1);
}
};
};
var scope = new Scope();
var removeWatch = scope.$watch(
function(scope) {
return scope.aValue;
},
function(newValue, oldValue, scope) {
scope.counter++;
}
);
removeWatch();

文档链接
完整代码

My Little World

关于缓存

发表于 2017-10-22

缓存的好处

利用缓存,可减少对源服务器的访问,因此也就节省了通信流量和通信时间
从而减少了延迟(加快页面打开速度),降低网络负载,保证稳定性(服务能正常使用)

浏览器本地存储与服务器端存储之间的区别

其实数据既可以在浏览器本地存储,也可以在服务器端存储。

浏览器端可以保存一些数据,需要的时候直接从本地获取,sessionStorage、localStorage和cookie都由浏览器存储在本地的数据。

服务器端也可以保存所有用户的所有数据,但需要的时候浏览器要向服务器请求数据。
1.服务器端可以保存用户的持久数据,如数据库和云存储将用户的大量数据保存在服务器端。
2.服务器端也可以保存用户的临时会话数据。服务器端的session机制,如jsp的 session 对象,数据保存在服务器上。实现上,服务器和浏览器之间仅需传递session id即可,服务器根据session id找到对应用户的session对象。会话数据仅在一段时间内有效,这个时间就是server端设置的session有效期。

服务器端保存所有的用户的数据,所以服务器端的开销较大,而浏览器端保存则把不同用户需要的数据分布保存在用户各自的浏览器中。
浏览器端一般只用来存储小数据,而服务器可以存储大数据或小数据。
服务器存储数据安全一些,浏览器只适合存储一般数据。

http协议里面的缓存机制

相关首部字段

Expires:资源失效日期
收到该字段后会缓存资源,再将缓存的资源给客户端,对于以后的请求,在该时间之前,响应缓存资源,当超过该时间后,缓存服务器在请求发来时,会向服务器请求资源
漏洞:用户时间和服务器时间不一致的话,会造成用户资源更新不及时

cache-control:通过配置参数,设置缓存机制,相关参数如下:
public:允许多用户共享缓存
private:只允许特定用户使用缓存
no-cache:在请求中配置,告诉服务器,我不要缓存的响应,中间服务器必须从源服务器取资源;
在响应中配置,告诉中间服务器不能对响应进行缓存,源服务器以后对中间服务器提出的有效性校验不在进行确认
no-store:请求/响应中含有机密信息,不允许缓存资源
max-age:缓存保存时长
缓存保存到现在的时长小于该值,就可以返回缓存,否则像源服务器请求资源

last-modified:资源最终修改时间
服务器会比较请求的该值与资源最终修改时间,不一致的话就返回新资源,一致的话,就返回304,not modified
漏洞:时间精确度到秒,可能存在同一秒保存两次文件,无法区分,
也可能因为通过cdn,造成服务器上文件修改时间并不一致

E-Tag:服务器为每份资源提供的唯一性标识,相当于文件MD5值
服务器会跟剧请求的if-no-match与资源ttag比较,不一致返回新资源,一致返回304

服务器处理机制:
当首部字段Cache-Control有指定max-age指令,Expires也存在时,会优先处理max-age,用于判断资源是否失效
当判断max-age失效后,会去判断last-modified和E-Tag,如果服务器仅支持http1.0,则仅根据last-modified做校验
如果服务器支持http1.1,则先根据E-Tag做校验,然后在根据last-modified做校验
相关链接

资源缓存

HTML等页面文件:短效缓存,本地服务器
更新策略:使用no-cache

css、js:长效缓存,CDN
更新策略:在URI上添加更新标识(md5,timestamp,version)
MD5:不同文件MD5值如果相同,有覆盖危险
timestamp:文件修改/上线时间,因为可能存在不正确性,所以需要其他操作判断文件是否有变化
version:每次都需要更新版本号

Image:长效缓存,CDN
更新策略:使用随机名字

数据缓存

Cookie

在web storage出现前,缓存都放在cookie里面,但存储数量有限,而且每次请求都会携带一堆数据,就会占用宽带
工作原理
当浏览器访问服务器时,服务器可以将少量数据以set-cookie消息头的方式发送给浏览器,浏览器会将这些数据临时保存下来。
当浏览器再次访问服务器,会将之前保存的这些数据以cookie消息头的方式发送给服务器。
cookie的限制
a. cookie可以被用户禁止。
b. cookie不安全(对于敏感数据,需要加密)。
c. cookie只能保存少量的数据(大约是4k)。
d. cookie的数量也有限制(大约是几百个)。
e.cookie只能保存字符串
生存时间问题
缺省情况下,浏览器会把cookie保存在内存里面,只要浏览器不关闭,cookie就会一直存在,浏览器关闭,cookie就会被销毁。
相关链接
相关链接

localStorage

大小有限制,数据不安全/不可信(有xss风险,存爆,读取不成功,被损坏),不应当存放重要数据,并且要做好容错
可以用于跨页面通讯
永久性的本地存储
将数据保存在客户端硬件设备上,不管它是什么,意思就是下次打开计算机时候数据还在
localStorage提供了四个方法来进行对本地存储做相关操作。
(1)setItem(key,value):添加本地存储数据。
(2)getItem(key):通过key获取相应的Value。
(3)removeItem(key):通过key删除本地数据。
(4)clear():清空数据。

SessionStorage

类似localStorage,但在窗口关闭后不存在
会话级别的本地存储
通过此对象可以直接操作存储在浏览器中的会话级别的WebStorage。
存储在sessionStorage中的数据首先是Key-Value形式的
另外就是它跟浏览器当前会话相关,当会话结束后,数据会自动清除,跟未设置过期时间的Cookie类似。即窗口关闭,数据清除
sessionStorage提供了四个方法来进行对本地存储做相关操作。
(1)setItem(key,value):添加本地存储数据。
(2)getItem(key):通过key获取相应的Value。
(3)removeItem(key):通过key删除本地数据。
(4)clear():清空数据。
注意区别session机制
session机制是一种服务器端的机制,服务器使用一种类似于散列表的结构(也可能就是使用散列表)来保存信息。
当程序需要为某个客户端的请求创建一个session时,服务器首先检查这个客户端的请求里是否已包含了一个session标识(称为session id),如果已包含则说明以前已经为此客户端创建过session,服务器就按照session id把这个session检索出来使用(检索不到,会新建一个),如果客户端请求不包含session id,则为此客户端创建一个session并且生成一个与此session相关联的session id,session id的值应该是一个既不会重复,又不容易被找到规律以仿造的字符串,这个session id将被在本次响应
中随cookie返回给客户端保存。
即
Session是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中
但cookie可以被人为的禁止,则必须有其他机制以便在cookie被禁止时仍然能够把session id传递回服务器。

经常被使用的一种技术叫做URL重写,就是把session id直接附加在URL路径的后面。还有一种技术叫做表单隐藏字段。就是服务器
会自动修改表单,添加一个隐藏字段,以便在表单提交时能够把session id传递回服务器

sessionStorage 、localStorage 和 cookie 之间的区别

共同点:都是保存在浏览器端,且同源的。
区别:
1.cookie数据始终在同源的http请求中携带(即使不需要),即cookie在浏览器和服务器间来回传递。而sessionStorage和localStorage不会自动把数据发给服务器,仅在本地保存。cookie数据还有路径(path)的概念,可以限制cookie只属于某个路径下。
2.存储大小限制也不同,cookie数据不能超过4k,同时因为每次http请求都会携带cookie,所以cookie只适合保存很小的数据,如会话标识。sessionStorage和localStorage 虽然也有存储大小的限制,但比cookie大得多,可以达到5M或更大。
3.数据有效期不同,sessionStorage:仅在当前浏览器窗口关闭前有效,自然也就不可能持久保持;localStorage:始终有效,窗口或浏览器关闭也一直保存,因此用作持久数据;cookie只在设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭。
4.作用域不同,sessionStorage不在不同的浏览器窗口中共享,即使是同一个页面;localStorage 在所有同源窗口中都是共享的;cookie也是在所有同源窗口中都是共享的。
5.Web Storage 支持事件通知机制,可以将数据更新的通知发送给监听者。
6.Web Storage 的 api 接口使用更方便。
Cache-control参数解析

web SQL

在浏览器端创建的一个本地的数据库,而且支持标准的SQL的CRUD操作,让离线的Web应用更加方便的存储结构化的数据
Web SQL Database 规范中定义的三个核心方法:
openDatabase:这个方法使用现有数据库或新建数据库来创建数据库对象
transaction:这个方法允许我们根据情况控制事务提交或回滚
executeSql:这个方法用于执行SQL 查询,用SQL语句做查询索引工作
使用链接
实例参考
实际上已经被废弃

indexDB

适用持续化的数据,不会被清除的数据
也相当于在本地创建数据库,通过调用API接口进行数据操作,
在IndexedDB大部分操作并不是常用的调用方法,返回结果的模式,而是请求——响应的模式,比如打开数据库的操作
不会返回一个DB对象的句柄,得到的是一个IDBOpenDBRequest对象,而希望得到的DB对象在其result属性中
对数据的所有操作,增删改查,均通过调用api函数实现
MDN接口文档
使用教程1
使用教程2
持续化存储,可在 web worker中使用

离线缓存

离线缓存之App Cache

AppCache就是对app内存缓存的方案,具体表现为当请求某个文件时不是从网络获取该文件,而是从本地获取。
应用程序缓存为应用带来三个优势:
离线浏览 - 用户可在应用离线时使用它们
速度 - 已缓存资源加载得更快
减少服务器负载 - 浏览器将只从服务器下载更新过或更改过的资源

缺点:
可能会缓存错误页面,甚至缓存运营商的劫持广告
更新中有一个文件失败了,就会全部退回上一个版本
由于是Lazy更新,如果后台接口有break change将会是大麻烦
manifest文件也有可能不小心被缓存了

manifest 文件是简单的文本文件,它告知浏览器被缓存的内容(以及不缓存的内容)。
manifest 文件可分为三个部分:
CACHE MANIFEST - 在此标题下列出的文件将在首次下载后进行缓存,等价于CACHE:
NETWORK - 在此标题下列出的文件需要与服务器的连接,且不会被缓存
FALLBACK - 在此标题下列出的文件规定当页面无法访问时的回退页面(比如 404 页面
机制分析

离线缓存之service worker

service worker 相当于在浏览器的请求层建了一个服务器,所有的请求都会进入service worker进行处理,决定资源使用缓存还是从新获取服务器资源
service worker可以:
后台消息传递
网络代理,转发请求,伪造响应
离线缓存
消息推送
…

使用时必须使用HTTPS请求协议

在 installing 状态中,Service Worker 脚本中的 install 事件被执行。通常在安装事件中,为 document 缓存静态文件
处于 activating 状态期间,Service Worker 脚本中的 activate 事件被执行。通常在 activate 事件中,清理 cache 中的文件
如果 Service Worker 处于激活态,就可以监听事件性事件 —— fetch 和 message。
service worker可以通过fetch事件拦截请求,并且给出自己的响应。
页面和serviceWorker之间可以通过posetMessage()方法发送消息,发送的消息可以通过message事件接收到。

w3c提供了一个新的fetch api,用于取代XMLHttpRequest,与XMLHttpRequest最大不同有两点:

  1. fetch()方法返回的是Promise对象,通过then方法进行连续调用,减少嵌套。ES6的Promise在成为标准之后,会越来越方便开发人员。
  2. 提供了Request、Response对象,如果做过后端开发,对Request、Response应该比较熟悉。前端要发起请求可以通过url发起,也可以使用Request对象发起,而且Request可以复用。但是Response用在哪里呢?在service worker出现之前,前端确实不会自己给自己发消息,但是有了service worker,就可以在拦截请求之后根据需要发回自己的响应,对页面而言,这个普通的请求结果并没有区别,这是Response的一处应用。

1.注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//在页面文件注册
if (navigator.serviceWorker) {
// 注册Service Worker scope表示作用的页面的path
// register函数返回Promise
navigator.serviceWorker.register('./service-worker.js', {scope: './'})
.then(function (registration) {
console.log(registration);
})
.catch(function (e) {
console.error(e);
})
} else {
console.log('Service Worker is not supported in this browser.')
}

上面的代码检查 service worker API 是否可用,如果可用, /service-worker.js 这个文件将会作为 service worker 被注册。
scope表示作用的页面的path

2.安装使用
service-worker.js文件

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
importScripts('js/cache-polyfill.js'); // cache 扩展

var CACHE_VERSION = 'app-v1'; // 缓存文件的版本
var CACHE_FILES = [ // 需要缓存的页面文件
'/',
'images/background.jpeg',
'js/app.js',
'css/styles.css'
];


self.addEventListener('install', function (event) { // 监听worker的install事件
event.waitUntil( // 延迟install事件直到缓存初始化完成
caches.open(CACHE_VERSION) //开启一个缓存
.then(function (cache) {
console.log('Opened cache');
return cache.addAll(CACHE_FILES); //缓存文件
//这个过程是通过一连串 promise (caches.open 和 cache.addAll)完成的。
//event.waitUntil 会拿到一个 promise ,并使用其来获取安装耗费的时间以及是否安装成功。
//如果所有的文件都缓存成功,service worker 就安装成功了。
//如果任何一个文件下载失败,那么安装步骤就会失败。这个方式依赖于自己指定的资源,但这意味着,需要非常仔细地确定哪些文件需要被缓存。
//指定了太多文件的话,会增加失败率
//可以在 install 事件中执行其他操作,甚至忽略 install 事件
})
);
});

self.addEventListener('activate', function (event) { // 监听worker的activate事件,正在激活状态会触发该事件,更新service worker时,也会触发
event.waitUntil( // 延迟activate事件直到
caches.keys().then(function(keys){
return Promise.all(keys.map(function(key, i){ // 清除旧版本缓存
if(key !== CACHE_VERSION){
return caches.delete(keys[i]);
//需要在 activate 的 callback 中进行 cache 管理,来清理老的 cache。
//在 activate 而不是 install 的时候进行的原因,是如果在 install 的时候进行清理,
//那么老的 service worker 仍然在控制页面,他们依赖的缓存就失效了,因此就会突然被停止
}
}))
})
)
});



self.addEventListener('fetch', function (event) { // 截取页面的资源请求
event.respondWith( // 返回页面的资源请求
caches.match(event.request).then(function(res){ // 判断缓存是否命中
if(res){ // 返回缓存中的资源
return res;
}
requestBackend(event); // 执行请求备份操作
})
)
});

function requestBackend(event){ // 请求备份操作
var url = event.request.clone();
return fetch(url).then(function(res){ // 请求线上资源
//if not a valid response send the error
//确保 response 有效
//检查 response 的状态是200
//确保 response 的类型是 basic 类型的,这说明请求是同源的,这意味着第三方的请求不能被缓存。
if(!res || res.status !== 200 || res.type !== 'basic'){
return res;
}

var response = res.clone();
//如果检查通过会clone 这个请求。
//这么做的原因是如果 response 是一个 Stream,那么它的 body 只能被消费一次。
//所以为了让浏览器跟缓存都使用这个body,必须克隆这个 body,一份到浏览器,一份到缓存中缓存

caches.open(CACHE_VERSION).then(function(cache){ // 缓存从线上获取的资源
cache.put(event.request, response);
});

return res;
})
}

service worker 更新步骤:
1.更新 service worker 的 JavaScript 文件
当用户浏览你的网站时,浏览器尝试在后台重新下载 service worker 的脚本文件。
经过对比,只要服务器上的文件和本地文件有一个字节不同,这个文件就认为是新的。
2.之后更新后的 service worker 启动并触发 install 事件。
3.此时,当前页面生效的依然是老版本的 service worker,新的 service worker 会进入 “waiting” 状态。
4.当页面关闭之后,老的 service worker 会被干掉,新的 servicer worker 接管页面
5.一旦新的 service worker 生效后会触发 activate 事件。

关于 service worker 的一些注意点:
1.service worker 是一个JavaScript worker ,所以它不能直接访问 DOM 。相反, service worker 可以通过postMessage 接口与跟其相关的页面进行通信,发送消息,从而让这些页面在有需要的时候去操纵 DOM 。
2.Service worker 是一个可编程的网络代理,允许你去控制如何处理页面的网络请求。
3.Service worker 在不使用时将被终止,并会在需要的时候重新启动,因此你不能把onfetch 和 onmessage事件来作为全局依赖处理程序。如果你需要持久话一些信息并在重新启动Service worker后使用他,可以使用 IndexedDBAPI ,service worker 支持。   
4.Service worker 广泛使用了 promise
缺点:
如果安装失败了,没有一个很好的方式来知晓
fetch api()目前仅支持Service Workers中使用
fetch() 中不会被触发重定向
页面在改变 hash 时,service worker 会停止工作

Service Worker 生命周期
Service Worker那些事
深入了解 Service Worker
Service Worker初体验
serviceworke Introduction

离线缓存之 Hybrid Cache离线包

前端开发”就是使用 HTML、CSS、JS 技术给一个网站或 Web 应用开发图形用户界面
所以,前端应用本质上一个 GUI 程序,而 GUI 程序有三种典型形态:
Web : 以浏览器为运行环境,基于浏览器内核支持的编程语言、API 来实现,被浏览器解释执行
Native : 以操作系统为运行环境,基于操作系统原生支持的编程语言、API 接口实现,以二进制包的形式运行
Hybird : 基于 Native 应用提供的一个支持 HTML、CSS、JS 的容器开发的应用,相当与用开发 Web 的方式开发 Native 应用

原理:
1.o2o 在线资源抓取程序:基于 phantomjs 解析资源
2.grunt-inc-offline 增量包计算器:基于 git-diff 的增量包运算
3.离线包生成器
CACHE1
1.o2o 定时程序监听线上页面变更,将其所携带的资源(HTML、CSS、JS 和部分图片)抓取下来
2.增量包计算器会计算好与之前若干版本之间的增量文件,配合包生成器将增量包逐一构建打包,同时生成好每个增量包的 Diff Json
3.调用 Clam 命令通过 Gitlab 将资源包部署至 CDN,以备手机端更新。
4.Gitlab 仓库 的更新会触发一个 Hook 脚本,调用 tSync 服务器的接口,来通知资源变更
5.tSync 服务器沙箱完成消息封装,包括了第二步生成了的 Diff Json 文本
6.tSync 长连接将消息指令下发给手机终端
7.手机终端拼好资源文件链接,从 CDN 将增量包更新下来,随后执行 Diff Json中的指令,完成包的更新。

手机端的两个重要进程:

1.资源预加载进程:在实际访问页面之前,将资源预加载到缓存池并更新 Cache Map
2.创建 WebView 进程:只聚焦本地资源读写,别的什么也不干
手机端 touch 到网络的环节收敛到了两处,第一,Package Update Controller,第二,WebView 本身必要的网络请求
CACHE2
相关链接

My Little World

实现一个 EventEmitter

发表于 2017-10-21

实现一个 EventEmitter

EventEmitter 模块,它是一个类,它的实例具有以下几个方法:on、emit、off:
on(eventName, func):监听 eventName 事件,事件触发的时候调用 func 函数。
emit(eventName, arg1, arg2, arg3…):触发 eventName 事件,并且把参数 arg1, arg2, arg3… 传给事件处理函数。
off(eventName, func):停止监听某个事件。

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
class EventEmitter {
/* TODO */
constructor() {
this.listeners = [];
}

on(eventName, func){
for(let i=0;i<this.listeners.length;i++){
if(this.listeners[i].name === eventName){
this.listeners[i].funcs.push(func);
return
}
}
this.listeners.push({
name:eventName,
funcs:[]
})
this.listeners[this.listeners.length-1].funcs.push(func)
}
emit(eventName, ...args){
for(let j=0;j<this.listeners.length;j++){
if(this.listeners[j].name === eventName){
for(let k =0;k<this.listeners[j].funcs.length;k++){
this.listeners[j].funcs[k](args)
}
}
}
}
off(eventName, func){
for(let j=0;j<this.listeners.length;j++){
if(this.listeners[j].name === eventName){
if(func == undefined){ //整个事件不再监听
this.listeners.splice(j,1);
}
for(let k =0;k<this.listeners[j].funcs.length;k++){
if(func === this.listeners[j].funcs[k]){
this.listeners[j].funcs.splice(k,1);
return
}
}
}
}
}
}

const emitter = new EventEmitter()
const sayHi = (name) => console.log(`Hello ${name}`)
const sayHi2 = (name) => console.log(`Good night, ${name}`)

emitter.on('hi', sayHi)
emitter.on('hi', sayHi2)
emitter.on('hi2', sayHi)
emitter.on('hi2', sayHi2)
emitter.emit('hi', 'ScriptOJ')
// => Hello ScriptOJ
// => Good night, ScriptOJ

emitter.off('hi', sayHi)
emitter.emit('hi', 'ScriptOJ')
emitter.emit('hi2', 'ScriptOJ')

const emitter2 = new EventEmitter()
emitter2.on('hi', (name, age) => {
console.log(`I am ${name}, and I am ${age} years old`)
})
emitter2.emit('hi', 'Jerry', 12)
My Little World

react 简略学习笔记

发表于 2017-10-09

react.js是一个UI库,按照react规则,可以组建页面里每一小块的功能,这种局部的功能就是一个组件,
各组件之间可以组合、嵌套,从而拼成整个页面
react就是在组建各个尽可能通用的组件

环境搭建

直接使用官方工具 create-react-app
npm install -g create-react-app 安装命令行
create-react-app hello-react 构建工程
cd hello-react 进入工程
npm start 运行
src/App.js 即是编写组件的文件
一个简单的组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React, { Component } from 'react' //写组件必须要引入的依赖
import ReactDOM from 'react-dom' //把组件渲染到页面上去,就是把组件的html结构形成页面
import './index.css'

class Header extends Component {
render () { //组件必须要有的函数,且必须要返回JSX
return ( //返回JSX写的组件内容
<div>
<h1>React 小书</h1>
</div>
)
}
}

//ReactDOM.render 功能就是把组件渲染并且构造 DOM 树,然后插入到页面上某个特定的元素上(在这里是 id 为 root 的 div 元素)。

ReactDOM.render(
<Header />,
document.getElementById('root')
)

在编写 React.js 组件的时候,一般都需要继承 React.js 的 Component(也有别的编写组件的方式–>函数式组件)。一个组件类必须要实现一个 render 方法,这个 render 方法必须要返回一个 JSX 元素。但这里要注意的是,

必须要用一个外层的 JSX 元素把所有内容包裹起来。返回并列多个 JSX 元素是不允许的,

1
2
3
4
5
6
7
8
9
10
11
12
//不对的
return (
<div>第一个</div>
<div>第二个</div>
)
//对的
return (
<div>
<div>第一个</div>
<div>第二个</div>
</div>
)

JSX

可以把它看做是js的一种数据类型,长得很像HTML,但不是HTML,可以像html那样写,但有些地方要注意

可应用{}插入任何js代码,
如果是表达式函数,则返回的结果会相应地渲染到页面上,变量则将变量值对应渲染

{}内如果是条件表达式,即可以实现,根据不同条件返回不同jsx内容,
如果在表达式插入里面返回 null ,那么 React.js 会什么都不显示,相当于忽略了该表达式插入。结合条件返回的话,就做到显示或者隐藏某些元素

可以做变量的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
render () {
const isGoodWord = true
const goodWord = <strong> is good</strong>
const badWord = <span> is not good</span>
const title = <h1 className='title'> ScriptOJ </h1>
const page = <div className= 'content'> {title} </div> //page里面用title的值,即 <h1 className='title'> ScriptOJ </h1>
return (
<div>
<h1>
React 小书
{isGoodWord ? goodWord : badWord}
</h1>
</div>
)
}

给标签添加class时,不能用class,要用className

1
2
3
4
5
6
7
8
render () {
const className = 'header'
return (
<div className={className}>
<h1>React 小书</h1>
</div>
)
}

for属性不能用,要用htmlfor代替,
其他的 HTML 属性例如 style 、data-* 等就可以像普通的 HTML 属性那样直接添加上去。

组件树

通过继承Component可以自定义形成多个类,即形成多个组件,但这些组件名,
或者说类名要作为标签使用,为与普通小写字母开头的HTML标签做区分,一定要以大写字母开头

自定义组件以标签的形式放入另一个自定义组件的JSX中,即可实现包含关系
多个自定义标签组件并列放入同一个JSX中即实现并列关系
组件可以和组件组合在一起,组件内部可以使用别的组件,就像普通的 HTML 标签一样使用就可以
这样的组合嵌套,最后构成一个所谓的组件树

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import React, { Component } from 'react';
import ReactDOM from 'react-dom';

class Title extends Component {
render () {
return (
<h1>hello react</h1>
)
}
}

class Header extends Component {
render () {
return (
<div>
<Title /> //header里面包含Title
<h2>This is Header</h2>
</div>
)
}
}

class Main extends Component {
render () {
return (
<div>
<h2>This is main content</h2>
</div>
)
}
}

class Footer extends Component {
render () {
return (
<div>
<h2>This is footer</h2>
</div>
)
}
}

class Index extends Component {
render () {
return (
<div> //多标签并列放置,组成页面
<Header />
<Main />
<Footer />
</div>
)
}
}

ReactDOM.render(
<Index />,
document.getElementById('root')
)

事件监听

React.js 不需要手动调用浏览器原生的 addEventListener 进行事件监听。
React.js 封装好了一系列的 on 的属性,当需要为某个元素监听某个事件的时候,只需要简单地给它加上 on 就可以了。
而且不需要考虑不同浏览器兼容性的问题,React.js 都封装好了这些细节。

但是要注意,没有经过特殊处理的话,这些 on* 的事件监听只能用在普通的 HTML 的标签上,而不能用在组件标签上

React.js 会给每个事件监听传入一个 event 对象,这个对象提供的功能和浏览器提供的功能一致,而且它是兼容所有浏览器的。

React.js 的事件监听方法需要手动 bind 到当前实例,这种模式在 React.js 中非常常用。不然监听事件调用的方法里面的this是undefined
另外可以在bind 的时候传参

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
class Title extends Component {
handleClickOnTitle (e) {
console.log(this)//undefined
console.log(e.target.innerHTML)//hello React
}

render () {
return (
<h1 onClick={this.handleClickOnTitle}>hello React</h1>
)
}
}

class Title extends Component {
handleClickOnTitle (word, e) {
console.log(this, word)//Title 对象,“hello”
console.log(e.target.innerHTML)//hello React
}

render () {
return (
<h1 onClick={this.handleClickOnTitle.bind(this, 'Hello')}>hello React </h1>
)
}
}

setState函数和pros

一个组件的显示形态是可以由它数据状态(state)和配置参数(pros)决定的,即两种方式控制组件显示形态

方式一:利用state的值,通过渲染控制显示状态

一个组件可以拥有自己的状态,React.js 的 state 用来存储这种可变化的状态,再让setState函数根据state的值重新渲染组件到页面
setState 方法由父类 Component 所提供。当调用这个函数的时候,React.js 会更新组件的状态 state ,并且重新调用 render 方法,然后再把 render 方法所渲染的最新的内容显示到页面上。
注意,当要改变组件的状态的时候,不能直接用 this.state = xxx 这种方式来修改,如果这样做 React.js 就没办法知道你修改了组件的状态,它也就没有办法更新页面。所以,一定要使用 React.js 提供的 setState 方法,它接受一个对象或者函数作为参数。

传入一个对象的时候,这个对象表示该组件的新状态。但只需要传入需要更新的部分就可以了,而不需要传入整个对象
要注意的是,当调用 setState 的时候,React.js 并不会马上修改 state。而是把这个对象放到一个更新队列里面,稍后才会从队列当中把新的状态提取出来合并到 state 当中,然后再触发组件更新

1
2
3
4
5
6
7
handleClickOnLikeButton () {
console.log(this.state.isLiked)
this.setState({
isLiked: !this.state.isLiked
})
console.log(this.state.isLiked) //第二次打印的就是第一次打印的值
}

如果需要多次setState,而且后续传入参数依赖前一个setState 参数,这时就要用函数做setState参数,
React.js 会把上一个 setState 的结果传入这个函数,你就可以使用该结果进行运算、操作,然后返回一个对象作为更新 state 的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
handleClickOnLikeButton () {

// this.setState({ count: 0 }) // => this.state.count 还是 undefined
// this.setState({ count: this.state.count + 1}) // => undefined + 1 = NaN
// this.setState({ count: this.state.count + 2}) //最终结果为NAN

this.setState((prevState) => {
return { count: 0 }
})
this.setState((prevState) => {
return { count: prevState.count + 1 } // 上一个 setState 的返回是 count 为 0,当前返回 1
})
this.setState((prevState) => {
return { count: prevState.count + 2 } // 上一个 setState 的返回是 count 为 1,当前返回 3
})
// 最后的结果是 this.state.count 为 3,所以如果JSX中如果显示count,则显示3
}

多次调用setState只会返回多次运行后的结果,React.js 内部会把 JavaScript 事件循环中的消息队列的同一个消息中的 setState 都进行合并以后再重新渲染组件。

方式二:在JSX中使用自定义组件时,传入配置属性,根据属性显示状态

在使用一个组件的时候,可以把参数放在标签的属性当中,所有的属性都会作为 props 对象的键值
组件内部就可以通过 this.props 来访问到这些配置参数
可以把任何类型的数据作为组件的参数,包括字符串、数字、对象、数组、甚至是函数等等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Index extends Component {
render () {
return (
<div>
<LikeButton wordings={{likedText: '已赞', unlikedText: '赞'}}
onClick={() => console.log('Click on like button!')/> //这里onclick可以被当做属性访问到
</div>
)
}
}
//LikeButton 类
handleClickOnLikeButton () {

this.setState({
isLiked: !this.state.isLiked
})
if (this.props.onClick) { //这里的onclick是使用<LikeButton />时定义的属性
this.props.onClick() //这里的函数通过属性传递进来,不是组件自己实现
}
}
render()return 的JSX的button onclick事件绑定handleClickOnLikeButton函数

JSX 的{}内可以嵌入任何表达式,{ {} }就是在 {} 内部用对象}字面量返回一个对象而已,
this.props.wordings就是对象{ likedText: ‘已赞’, unlikedText: ‘赞’}

可以用defaultProps配置默认属性,这样就不需要判断配置属性是否传进来了,如果没有传进来,会直接使用 defaultProps 中的默认属性

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
class LikeButton extends Component {
static defaultProps = {
likedText: '取消',
unlikedText: '点赞'
}

constructor () {
super()
this.state = { isLiked: false }
}

handleClickOnLikeButton () {
this.setState({
isLiked: !this.state.isLiked
})
}

render () {
//不需要在这里向下面这样判断是否传进来了配置属性,如果配置了wordings就用wordings,否则用后面{}内的默认值
<!-- const wordings = this.props.wordings || {
likedText: '取消',
unlikedText: '点赞'
} -->
return (
<button onClick={this.handleClickOnLikeButton.bind(this)}>
{this.state.isLiked
? this.props.likedText //直接使用
: this.props.unlikedText} 👍
</button>
)
}
}

一个组件在输入确定的 props 的时候,能够输出确定的 UI 显示形态。如果想修改props决定的显示形态,可以主动通过重新渲染的方式,
把新的props传入到组件当中,这样这个组件中由 props 决定的显示形态也会得到相应的改变。

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
class Index extends Component {
constructor () {
super()
this.state = {
likedText: '已赞',
unlikedText: '赞'
}
}

handleClickOnChange () {
this.setState({
likedText: '取消',
unlikedText: '点赞'
})
}

render () {
return (
<div>
<LikeButton
likedText={this.state.likedText}
unlikedText={this.state.unlikedText} />
<div>
<button onClick={this.handleClickOnChange.bind(this)}>
修改 wordings
</button>
</div>
</div>
)
}
}

即props 一旦传入,你就不可以在组件内部对它进行修改,但是可以通过父组件主动重新渲染的方式来传入新的 props,从而达到更新的效果

小结
state 是让组件控制自己的状态,props 是让外部对组件自己进行配置。
没有 state 的组件叫无状态组件(stateless component),设置了 state 的叫做有状态组件(stateful component)
因为状态会带来管理的复杂性,所以尽量多地写无状态组件,尽量少地写有状态的组件。这样会降低代码维护的难度,也会在一定程度上增强组件的可复用性。

函数式组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//普通写法
class HelloWorld extends Component {
constructor() {
super()
}

sayHi () {
alert('Hello World')
}

render () {
return (
<div onClick={this.sayHi.bind(this)}>Hello World</div>
)
}
}

//函数式组件的编写方式
const HelloWorld = (props) => {
const sayHi = (event) => alert('Hello World')
return (
<div onClick={sayHi}>Hello World</div>
)
}

以前一个组件是通过继承 Component 来构建,一个子类就是一个组件。而用函数式的组件编写方式是一个函数就是一个组件,但使用方法同普通写法
函数式组件只能接受 props 而无法像跟类组件一样可以在 constructor 里面初始化 state。你可以理解函数式组件就是一种只能接受 props 和提供 render 方法的类组件。

渲染列表数据

如果往 {} 放一个数组,React.js 会帮你把数组里面一个个元素罗列并且渲染出来,如果数组每一项是JSX,就可以将数据渲染到html里面了
所以react渲染列表的思路就是,将数组数据利用es6的map方法转换成JSX,再将JSX数组放到render()返回值中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Index extends Component {
render () {
return (
<div>
{users.map((user) => {
return (
<div>
<div>姓名:{user.username}</div>
<div>年龄:{user.age}</div>
<div>性别:{user.gender}</div>
<hr />
</div>
)
})}
</div>
)
}
}

进一步优化,将数组每项JSX抽离成自定义组件

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
const users = [
{ username: 'Jerry', age: 21, gender: 'male' },
{ username: 'Tomy', age: 22, gender: 'male' },
{ username: 'Lily', age: 19, gender: 'female' },
{ username: 'Lucy', age: 20, gender: 'female' }
]

class User extends Component {
render () {
const { user } = this.props
return (
<div>
<div>姓名:{user.username}</div>
<div>年龄:{user.age}</div>
<div>性别:{user.gender}</div>
<hr />
</div>
)
}
}

class Index extends Component {
render () {
return (
<div>
{users.map((user) => <User user={user} />)}
<!-- {users.map((user, i) => <User key={i} user={user} />)} -->
</div>
)
}
}

ReactDOM.render(
<Index />,
document.getElementById('root')
)

但是要注意
对于用表达式套数组罗列到页面上的元素,都要为每个元素加上 key 属性,这个 key 必须是每个元素唯一的标识
这里用循环计数器 i 作为 key对循环标签User添加key,就不会报需要key的错了
{users.map((user) => )} 变成 {users.map((user, i) => )}

受控组件

类似于 <input />、<select />、<textarea> 这些元素的 value 值被 React.js 所控制、渲染的组件,在 React.js 当中被称为受控组件(Controlled Component)。对于用户可输入的控件,一般都可以让它们成为受控组件.
React.js 认为所有的状态都应该由 React.js 的 state 控制,只要类似于 <input />、<textarea />、<select /> 这样的输入控件被设置了 value 值,那么它们的值等于state的一个属性值,就会永远以被设置的值为准。值不变,value 就不会变化。
在 React.js 当中必须要用 setState 才能更新组件的内容,所以可以通过给输入框绑定监听输入框的 onChange 事件,然后获取到用户输入的内容,再通过 setState 的方式更新 state 中的 value属性绑定的值,这样 输入框的内容才会更新。

子组件可以利用props向父组件传递数据,即,父组件在使用子组件时,添加属性A,并将该A属性值赋值为一个父组件的函数B,
子组件在触发数据提交的时候,检查自己的props有没有属性A,如果有的话,将数据以参数的形式,传递给属性A,即
this.props.A({key1:value1,key2”value2….})
父组件函数B通过函数参数就可以获得传递过来的数据

子组件从父组件获取数据,同样使用props,也就是说,父组件在使用子组件的时候把自己的state的一个属性C赋值给子组件的属性D,
子组件通过属性D获取父组件传递过来的数据,在子组件中直接使用props.D的值,但要注意给子组件设置defaultProps,给属性D一个默认值,防止出现不传值的情况
这种行为叫做“状态提升”
如果将属性D的值,再保存到子组件的state中在使用,那么当其他组件也需要这份数据的时候,将无法使用,因为根本没办法访问,因此采用使用props值的办法,将这种组件之间共享的状态交给组件最近的公共父节点保管,然后通过 props 把状态传递给子组件,这样就可以在组件之间共享数据了

当某个状态被多个组件依赖或者影响的时候,就把该状态提升到这些组件的最近公共父组件中去管理,用 props 传递数据或者函数来管理这种依赖或着影响的行为。
对于不会被多个组件依赖和影响的状态(例如某种下拉菜单的展开和收起状态),一般来说只需要保存在组件内部即可,不需要做提升或者特殊的管理。

生命周期

挂载

React.js 将组件渲染,并且构造 DOM 元素然后塞入页面的过程称为组件的挂载
挂载过程:
-> constructor() //自身的状态的初始化工作
-> componentWillMount()//组件还没挂载完成时进行的组件启动工作,例如 Ajax 数据拉取、定时器的启动。
-> render()
// 然后构造 DOM 元素插入页面
-> componentDidMount() //组件挂载完成以后,也就是 DOM 元素已经插入页面后调用。进行依赖DOM的启动工作
// …
// 即将从页面中删除, setState 只能在已经挂载或者正在挂载的组件上调用,
//组件隐藏的时候,组件的回调函数可能还在不停地尝试 setState,因此会报错
-> componentWillUnmount() //组件对应的 DOM 元素从页面中删除之前调用,处理数据清理工作,如定时器的清理
// 从页面中删除

更新阶段

setState 导致 React.js 重新渲染组件并且把组件的变化应用到 DOM 元素上的过程,这是一个组件的变化过程。而 React.js 也提供了一系列的生命周期函数可以让我们在这个组件更新的过程执行一些操作。
关于更新阶段的组件生命周期:
shouldComponentUpdate(nextProps, nextState):你可以通过这个方法控制组件是否重新渲染。如果返回 false 组件就不会重新渲染。这个生命周期在 React.js 性能优化上非常有用。
componentWillReceiveProps(nextProps):组件从父组件接收到新的 props 之前调用。
componentWillUpdate():组件开始重新渲染之前调用。
componentDidUpdate():组件重新渲染并且把更改变更到真实的 DOM 以后调用。
官方文档

ref属性

通过在html标签或自定义的组件标签中添加 ref属性,可以绑定DOM操作

1
2
3
4
5
6
7
8
9
10
11
class AutoFocusInput extends Component {
componentDidMount () { //利用组件声明周期函数
this.input.focus() //这里的this.input就是页面里的DOM元素,因此可以直接使用DOM API
}

render () {
return (
<input ref={(input) => this.input = input} /> //把input标签DOM元素挂到组件的属性input上
)
}
}

input 元素加了一个 ref 属性,这个属性值是一个函数,
当 input 元素在页面上挂载完成以后,React.js 就会调用这个函数,函数的参数,就是挂载以后的DOM结点
在函数中把这个 DOM 元素设置为组件实例的一个属性,这样就可以通过 this.input 获取到这个 DOM 元素。

但注意,能不用 ref 就不用,因为React.js 本来就可以做到的页面自动更新的操作和事件监听,多余DOM操作,不利于理解和维护

dangerouslySetHTML

dangerouslySetInnerHTML 属性可以用于动态渲染HTML结构,即将HTML字符串,在页面中显示时,当做HTML去渲染
给 dangerouslySetInnerHTML 传入一个对象,这个对象的 __html 属性值就相当于元素的 innerHTML,这样就可以动态渲染元素的 innerHTML 结构了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
constructor() {
super()
this.state = {
content: '<h1>React.js cool!</h1>'
}
}

render () {
return (
<div
className='editor-wrapper'
dangerouslySetInnerHTML={{__html: this.state.content}} />
)
}

因为设置 innerHTML 可能会导致跨站脚本攻击(XSS),所以 React.js 团队认为把事情搞复杂可以防止(警示)大家滥用这个属性。这个属性不必要的情况就不要使用

style

在 React.js 中需要把 CSS 属性变成一个对象再传给元素
style 接受一个对象,这个对象里面是这个元素的 CSS 属性键值对,原来 CSS 属性中带 - 的元素都必须要去掉 - 换成驼峰命名,如 font-size 换成 fontSize,text-align 换成 textAlign。
用对象作为 style 方便动态设置元素的样式。可以用 props 或者 state 中的数据生成样式对象再传给元素,然后用 setState 就可以修改样式,非常灵活

<h1 style={ {fontSize: ‘12px’, color: this.state.color} }>React.js color</h1>

PropTypes 组件参数类型验证

给组件的配置参数加上类型验证,就可以验证传进组件的参数是否符合预定的数据类型,报错也能定位问题
PropTypes就是react第三方库,用于验证参数类型,即验证传入组件的数据的数据类型
npm install –save prop-types

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React, { Component } from 'react'
import PropTypes from 'prop-types'

class Comment extends Component {
static propTypes = {
comment: PropTypes.object //指定传入comment的类型必须为Object
}

render () {
const { comment } = this.props
return (
<div className='comment'>
<div className='comment-user'>
<span>{comment.username} </span>:
</div>
<p>{comment.content}</p>
</div>
)
}
}

propTypes 指定了参数类型,但是并没有说这个参数一定要传入,事实上,这些参数默认都是可选的。可选参数可以通过配置 defaultProps,让它在不传入的时候有默认值。
但是这里并没有配置 defaultProps,所以如果直接用<Comment /> 而不传入任何参数的话,comment 就会是 undefined,
可以通过 isRequired 关键字来强制组件某个参数必须传入

1
2
3
static propTypes = {
comment: PropTypes.object.isRequired
}

React.js 提供的 PropTypes 提供了一系列的数据类型可以用来配置组件的参数:
PropTypes.array
PropTypes.bool
PropTypes.func
PropTypes.number
PropTypes.object
PropTypes.string
PropTypes.node
PropTypes.element

官方文档

建议命名和定义顺序

组件的私有方法都用 _ 开头
所有事件监听的方法都用 handle 开头
把事件监听方法传给组件的时候,属性名用 on 开头

组件的内容编写顺序如下:

static 开头的类属性,如 defaultProps、propTypes。
构造函数,constructor。
getter/setter。
组件生命周期。
_ 开头的私有方法。
事件监听方法,handle。
render
开头的方法,有时候 render() 方法里面的内容会分开到不同函数里面进行,这些函数都以 render* 开头。
render() 方法。

高阶组件

高阶组件就是一个函数,传给它一个组件,它返回一个新的组件,返回的这个新的组件使用传入的组件作为子组件。

高阶组件内部的包装组件和被包装组件之间通过 props 传递数据。

多层高阶组件使用时,this.props是从外向里传递的,即A组件先后被B,C,D组件组装,则this.props到达A组件的顺序是D,C,B,A

高阶组件的作用是用于代码复用,可以把组件之间可复用的代码、逻辑抽离到高阶组件当中。

高阶组件有助于提高我们代码的灵活性,逻辑的复用性。

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
//高阶组件wrapWithLoadData ,
import React, { Component } from 'react'

export default (WrappedComponent, name) => {
class NewComponent extends Component {
constructor () {
super()
this.state = { data: null }
}

componentWillMount () {
let data = localStorage.getItem(name) //如果改变数据获取方式,可以修改这里
this.setState({ data })
}
//在此之前,可以做很多自定义逻辑
render () {
return <WrappedComponent data={this.state.data} />
}
}
return NewComponent
}

//使用
import wrapWithLoadData from './wrapWithLoadData'

class InputWithUserName extends Component {
render () {
return <input value={this.props.data} />
}
}

InputWithUserName = wrapWithLoadData(InputWithUserName, 'username') //组件InputWithUserName以参数传入wrapWithLoadData,进行包装
export default InputWithUserName

context

一个组件可以通过 getChildContext 方法返回一个对象,这个对象就是子树的 context,提供 context 的组件必须提供 childContextTypes 作为 context 的声明和验证。

如果一个组件设置了 context,那么它的子组件都可以直接访问到里面的内容,它就像这个组件为根的子树的全局变量。任意深度的子组件都可以通过 contextTypes 来声明你想要的 context 里面的哪些状态,然后可以通过 this.context 访问到那些状态。

context 打破了组件和组件之间通过 props 传递数据的规范,极大地增强了组件之间的耦合性。而且,就如全局变量一样,context 里面的数据能被随意接触就能被随意修改,每个组件都能够改 context 里面的内容会导致程序的运行不可预料。

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
//组件树的根组件
class Index extends Component {
static childContextTypes = { //验证getChildContext 返回的对象 必写!!!
themeColor: PropTypes.string
}

constructor () {
super()
this.state = { themeColor: 'red' }
}

getChildContext () { //设置 context,返回的对象就是子组件的this.context
return { themeColor: this.state.themeColor }
}

render () {
return (
<div>
<Header />
<Main />
</div>
)
}
}
//其中一层的子组件
class Title extends Component {
static contextTypes = { //来声明和验证需要获取的contxt内容的类型 必写!!!
themeColor: PropTypes.string
}

render () {
return (
<h1 style={{ color: this.context.themeColor }}>React.js 小书标题</h1> //直接使用this.context里面的值
)
}
}

redux 模式

代码中发现了共享的状态如果可以被任意修改的话,那么程序的行为将非常不可预料,所以提高了修改数据的门槛:必须通过 dispatch 执行某些允许的修改操作,而且必须大张旗鼓的在 action 里面声明。

这种模式挺好用的,就把它抽象出来一个 createStore,它可以产生 store,里面包含 getState 和 dispatch 函数,方便使用。

后来发现每次修改数据都需要手动重新渲染非常麻烦,因此希望自动重新渲染视图。所以后来加入了订阅者模式,可以通过 store.subscribe 订阅数据修改事件,每次数据更新的时候自动重新渲染视图。

接下来发现了原来的“重新渲染视图”有比较严重的性能问题(没有发生改变的数据也进行了渲染),我们引入了“共享结构的对象”来帮我们解决问题,这样就可以在每个渲染函数的开头进行简单的判断避免没有被修改过的数据重新渲染。

我们优化了 stateChanger 为 reducer,定义了 reducer 只能是纯函数,功能就是负责初始 state,和根据 state 和 action 计算具有共享结构的新的 state。

createStore 现在可以直接拿来用了,套路就是:

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
function createStore (reducer) {
let state = null
const listeners = []
const subscribe = (listener) => listeners.push(listener)
const getState = () => state
const dispatch = (action) => {
state = reducer(state, action) // 覆盖原对象
listeners.forEach((listener) => listener())
}
dispatch({}) // 初始化 state
return { getState, dispatch, subscribe }
}
// 定一个 reducer
function reducer (state, action) {
/* 初始化 state 和 switch case */
}

// 生成 store
const store = createStore(reducer)

// 监听数据变化重新渲染页面
store.subscribe(() => renderApp(store.getState()))

// 首次渲染页面
renderApp(store.getState())

// 后面可以随意 dispatch 了,页面自动更新
store.dispatch(...)

例

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
function createStore (reducer) {
let state = null
const listeners = []
const subscribe = (listener) => listeners.push(listener)
const getState = () => state
const dispatch = (action) => {
state = reducer(state, action) // 覆盖原对象
listeners.forEach((listener) => listener())
}
dispatch({}) // 初始化 state
return { getState, dispatch, subscribe }
}

function renderApp (newAppState, oldAppState = {}) { // 防止 oldAppState 没有传入,所以加了默认参数 oldAppState = {}
if (newAppState === oldAppState) return // 数据没有变化就不渲染了
console.log('render app...')
renderTitle(newAppState.title, oldAppState.title)
renderContent(newAppState.content, oldAppState.content)
}

function renderTitle (newTitle, oldTitle = {}) {
if (newTitle === oldTitle) return // 数据没有变化就不渲染了
console.log('render title...')
const titleDOM = document.getElementById('title')
titleDOM.innerHTML = newTitle.text
titleDOM.style.color = newTitle.color
}

function renderContent (newContent, oldContent = {}) {
if (newContent === oldContent) return // 数据没有变化就不渲染了
console.log('render content...')
const contentDOM = document.getElementById('content')
contentDOM.innerHTML = newContent.text
contentDOM.style.color = newContent.color
}

function stateChanger (state, action) {
if (!state) {
return {
title: {
text: 'React.js lalalal',
color: 'red',
},
content: {
text: 'React.js content',
color: 'blue'
}
}
}
switch (action.type) {
case 'UPDATE_TITLE_TEXT':
return { // 构建新的对象并且返回
...state,
title: {
...state.title,
text: action.text
}
}
case 'UPDATE_TITLE_COLOR':
return { // 构建新的对象并且返回
...state,
title: {
...state.title,
color: action.color
}
}
default:
return state // 没有修改,返回原来的对象
}
}

const store = createStore(stateChanger)
let oldState = store.getState() // 缓存旧的 state
store.subscribe(() => {
const newState = store.getState() // 数据可能变化,获取新的 state
renderApp(newState, oldState) // 把新旧的 state 传进去渲染
oldState = newState // 渲染完以后,新的 newState 变成了旧的 oldState,等待下一次数据变化重新渲染
})

renderApp(store.getState()) // 首次渲染页面
store.dispatch({ type: 'UPDATE_TITLE_TEXT', text: '《React.js is so cool!》' }) // 修改标题文本
store.dispatch({ type: 'UPDATE_TITLE_COLOR', color: 'blue' }) // 修改标题颜色

react-redux

使用Context和redux实现根据共享状态进行子组件渲染的工作

index.js

react-redux.js

ThemeSwitch.js

Content.js

Header.js

使用真正的react-redux
即
import { connect } from ‘react-redux’
import { createStore } from ‘redux’
import { Provider } from ‘react-redux’

根据是否需要高度的复用性,把组件划分为 Dumb 和 Smart 组件,约定俗成地把它们分别放到 components 和 containers 目录下。

Dumb 基本只做一件事情 —— 根据 props 进行渲染。而 Smart 则是负责应用的逻辑、数据,把所有相关的 Dumb(Smart)组件组合起来,通过 props 控制它们。

Smart 组件可以使用 Smart、Dumb 组件;而 Dumb 组件最好只使用 Dumb 组件,否则它的复用性就会丧失。

要根据应用场景不同划分组件,如果一个组件并不需要太强的复用性,直接让它成为 Smart 即可;否则就让它成为 Dumb 组件。

还有一点要注意,Smart 组件并不意味着完全不能复用,Smart 组件的复用性是依赖场景的,在特定的应用场景下是当然是可以复用 Smart 的。而 Dumb 则是可以跨应用场景复用,Smart 和 Dumb 都可以复用,只是程度、场景不一样。

例如将ThemeSwitch拆分成

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
43
44
45
46
47
/* src/components/ThemeSwitch.js */
import React, { Component, PropTypes } from 'react'

export default class ThemeSwitch extends Component {
static propTypes = {
themeColor: PropTypes.string,
onSwitchColor: PropTypes.func
}

handleSwitchColor (color) {
if (this.props.onSwitchColor) {
this.props.onSwitchColor(color)
}
}

render () {
return (
<div>
<button
style={{ color: this.props.themeColor }}
onClick={this.handleSwitchColor.bind(this, 'red')}>Red</button>
<button
style={{ color: this.props.themeColor }}
onClick={this.handleSwitchColor.bind(this, 'blue')}>Blue</button>
</div>
)
}
}

/* src/containers/ThemeSwitch.js */

import { connect } from 'react-redux'
import ThemeSwitch from '../components/ThemeSwitch'

const mapStateToProps = (state) => {
return {
themeColor: state.themeColor
}
}
const mapDispatchToProps = (dispatch) => {
return {
onSwitchColor: (color) => {
dispatch({ type: 'CHANGE_COLOR', themeColor: color })
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ThemeSwitch)

学习资料链接

My Little World

关于webpack

发表于 2017-10-08

说明

1.webpack工作内容
Webpack可以看做是模块打包机:它做的事情是,分析你的项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言(Scss,TypeScript等),并将其转换和打包为合适的格式供浏览器使用。
2.webpack工作方式
把你的项目当做一个整体,通过一个给定的主文件(如:index.js),Webpack将从这个文件开始找到你的项目的所有依赖文件,使用loaders,plugins处理它们,最后打包为一个(或多个)浏览器可识别的JavaScript文件。

使用

1.如果在nodejs中全局安装了webpack和webpack-dev-server,可以在git bash直接使用’webpack’命令进行打包,使用’webpack-dev-server’命令启动打包后生成的项目
2.在打包之前,要先配置webpack.config.js文件,’webpack’命令会根据webpack.config.js文件的配置项去打包文件
3.如果想使用不同的配置文件去打包项目,可以使用’webpack –config 文件名’命令去打包
4.如果不想带文件名去打包,可以在package.json的scripts对象中配置命令‘xxx’,然后在git bash中使用命令‘npm run xxx’进行打包

1
2
3
4
5
"scripts": {
"start": "webpack-dev-server",
"build": "webpack --config webpack.config.build.js",
"dev":"webpack --config webpack.config.dev.js"
}

执行 npm run build 则按照webpack.config.build.js文件配置的内容去打包
同样,执行 npm run dev 则按照webpack.config.dev.js文件配置的内容去打包
但要注意,使用配置命令’start’时,直接执行’npm start’,就可以执行其对于的命令,中间不需要加run

webpack.config.js 配置

module.exports = {}
1.配置根路径
配置入口文件前,可以用context配置根路径,这样在配置时,就不用将文件路径写进去,直接将文件配置进去就可以了
可以借助path模块拼接路径字符串

1
2
3
4
5
6
7
8
const path = require('path')

module.exports = {
/**
* The base directory
*/
context: path.join(__dirname, './src'),//这样入口文件就是指src文件夹下面的那些文件
}

2.入口文件
入口文件是指打包后项目生成的可以访问的文件

1
2
3
4
5
6
7
8
9
//单入口:
entry: './index.js',/*打包单个文件*/
entry: ['./index1.js','./index2.js'],/*打包多个文件*/

//多入口:
entry: {
home: './home',
user: ['./user', './account']
},

多入口文件数组的KEY值可用于命名输出文件名,其对应的值即是要打包的源文件

3.输出文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
output: {

/* filename方式一:直接指定文件名*/
filename: 'bundle.js',

/* filename方式二:映射变量[name]和[hash],[name]即定义入口文件的key值,[hash]是webpack在打包的时候按照一定规则生成的值,是MD5加密的值,如果文件没有发生变化,hash值不会变,可用于版本控制
(CDN一般缓存时间比较长,文件发生变化,hash值就会变,文件名就会变,CDN就能及时更新文件)
* 还可以加[chunkhash],这里是作为版本号使用,方便上线时静态资源的版本管理
* 单入口[name]被映射成‘main’
*/
filename: '[name].bundle.[hash].js',

path: '/home/proj/public/assets',/*指定输入文件存放路径*/
libraryTarget:
}

其他配置参数

4.配置加载器loader

loader 是对应用程序中资源文件进行转换。它们是(运行在 Node.js 中的)函数,可以将资源文件作为参数的来源,然后返回新的资源文件。
官方可用loader

Loaders的配置包括以下几方面:
test:一个用以匹配loaders所处理文件的拓展名的正则表达式(必须)
loader:loader的名称(必须)
include/exclude:手动添加必须处理的文件(文件夹)或屏蔽不需要处理的文件(文件夹)(可选);
query:为loaders提供额外的设置选项(可选)

loader被配置在rules数组中,数组每一个对象可配置一个或多个loader
例:

1
2
3
4
5
6
7
8
9
module: {
rules: [
{
test: /\.js$/,
include: path.join(__dirname, './src'),
use: 'babel-loader'
}
]
},

常用loader:
babel-loader:编译js,考虑到babel具有非常多的配置选项,一般把babel的配置选项放在一个单独的名为 “.babelrc” 的配置文件中
css-loader:使你能够使用类似@import 和 url(…)的方法实现 require()的功能,获取css文件里面的样式
style-loader:将css样式插件js文件,与css-loader一起使用能够把样式表嵌入webpack打包后的JS文件中
autoprefixer-loader:自动添加前缀,使css文件更有兼容性
postcss-loader:css会自动根据Can i use里的数据添加不同前缀,可与autoprefixer-loader结合使用
ExtractTextPlugin.extract:将css单独打包成一个.css文件,使用这个插件,需要require 插件extract-text-webpack-plugin

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
43
44
45
46
47
48
49
//配置代码:
1.
module: {
rules: [
{
test: /\.css$/,
include: [
path.join(__dirname, './src')
],
use: ['style-loader', 'css-loader']
//链式调用loader的时候,会以数组逆序的顺序执行,
//先执行css-loder,再执行style-loader
}
]
},

2.
module: {
rules: [{
test: /\.css$/,
include: [
path.join(__dirname, './src'),
],
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: 'css-loader'
})
}]
},

plugins: [
new ExtractTextPlugin('[name].css')
]
3.
module: {
rules: [{
test: /\.css$/,
include: [
path.join(__dirname, './src')
],
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: ['css-loader', 'autoprefixer-loader']
})
}]
},
plugins: [
new ExtractTextPlugin('[name].css')
]

5.配置插件plugin
插件目的在于解决 loader 无法实现的其他事,配置插件都在plugin数组中,数组每一项就是new一个plugin,插件的配置也在new时以参数形式传递进去
例:

1
2
3
4
5
6
7
8
9
//打包公共文件方式一:

plugins: [
new CommonsChunkPlugin({
name: 'commons',
filename: 'commons.js'
}),
new ExtractTextPlugin('[name].css')
]

常用插件:
CommonsChunkPlugin:将多个文件通用的东西打包成一个文件
webpack.optimize.CommonsChunkPlugin:require(‘webpack’),使用webpack的optimize.CommonsChunkPlugin将打包的文件转化为公共文件
HtmlWebpackPlugin:是依据一个简单的index.html模板,生成一个自动引用你打包后的JS文件的新index.html。这在每次生成的js文件名称不同时非常有用(比如添加了hash值)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//打包公共文件方式二:

var path = require('path')
var webpack = require('webpack')

module.exports = {
context: path.join(__dirname, './src'),
entry: {
app: './',
vendor: ['jquery', 'underscore'],//将文件中require的'jquery', 'underscore'打包成公共文件vendor.bundle.js
},
output: {
path: path.join(__dirname, 'dist'),
filename: 'bundle.js'
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
filename: 'vendor.bundle.js'
})
]
}

入门Webpack

写一个webpack 的loader

1.相当于写一个普通的模块

1
2
3
4
5
module.exports = function (source){
.....
...
return xxx
}

2.获取配置参数
方式一
直接使用API:this.query
方式二
使用loader-utils库

1
2
const loaderutils = require('loader-utils')
const options = loaderutils.getOptions(this);

3.处理模块依赖
方式一:转化成require语句
例如,css-loader将@import和url(…)替换会require(…)
方式二:使用this.resolve函数解析路径
例如,less-loader将less编译器进行扩展自定义路径解析逻辑然后利用this.resolve解析依赖

4.不要使用绝对路径
5.如果要包裹另外一个包将这个包作为peerDependency在package.json中引入
6.返回值
如果只是返回一个值可以用return
如果返回多个值需要调用API:this.callback(),但依然要在最后return一个undefined

7.本地配置使用

单个:path.resolve(‘../../loader.js’)

1
2
3
4
5
6
7
8
9
{
test: /\.js$/
use: [
{
loader: path.resolve('path/to/loader.js'),
options: {/* ... */}
}
]
}

多个或一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module: {
rules: [
{
test: /\.js/,
use: [{
loader:'define-loader',
options:{name:'alice'}
}
],
}
]
},
resolveLoader: {
modules: [
'node_modules',
path.resolve(__dirname, 'loaders')
]
}

中文
How to write a loader
loader API

写一个webpack 的plugin

按照格式,根据compiler 和 compilation 对象处理流程上的钩子函数,
在不同阶段插入我们要做的事情
1.格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class DefPlugin {
constructor(name) {
}

apply(compiler) {
// 指定挂载的webpack事件钩子。
compiler.plugin("compile", function(params) {
console.log("The compiler is starting to compile...");
});
// 指定挂载的webpack事件钩子。
compiler.plugin('emit', function(compilation, callback) {
// 现在设置回调来访问编译中的步骤:
compilation.plugin("optimize", function() {
console.log("Assets are being optimized.");
});
//功能完成后调用webpack提供的回调。
callback();

})
}
}
module.exports = DefPlugin

2.使用
直接在webpack.config.js文件中require一下就好

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

const Plugin = require('./myplugin')

module.exports = {
context: path.join(__dirname, 'src'),
entry: './index.js',
output: {
libraryTarget: 'commonjs2',
path: path.join(__dirname, 'dist'),
filename: './bundle.js'
},
plugins: [
new Plugin
]
}

3.compiler
compiler 对象代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性建立,并在所有可操作的设置中被配置,包括原始配置,loader 和插件。当在 webpack 环境中应用一个插件时,插件将收到一个编译器对象的引用。可以使用它来访问 webpack 的主环境。
源码
4.compilation
compilation 对象代表了一次单一的版本构建和生成资源。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,一次新的编译将被创建,从而生成一组新的编译资源。一个编译对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。编译对象也提供了很多关键点回调供插件做自定义处理时选择使用
源码
5.tabable

webpack性能优化

1.将不再使用的依赖及时删除
2.不要为了实现小功能引入大型lib
3.合理配置babel
4.根据使用场景合理使用优化插件
例如使用happypack,webpack-uglify-parallel 在开发阶段进行多核并行压缩
相关链接
学习链接

gulp

api:
task:
src:
watch:用来监视文件的变化;

jshint:js 代码检查
uglify:压缩js
minify-css:压缩css
minfy-html:压缩html
concat:合并文件

babel

babel是一个js转换器
可以将es6,es7,react/jsx的语法通过他的插件转编译成es5的或者说支持在现有环境运行的语法
使用babel,除了可以使用package.json进行配置,一般会为它建立专门的.babelrc文件进行配置

1
2
3
4
{
"presets": [],
"plugins": []
}

使用babel-preset-env转换es5,就将‘env’添加到.babelrc的presents数组中
使用babel-preset-react转换jsx,就将‘react’添加到.babelrc的presents数组中
使用babel-preset-stage-x转换es7不同阶段语法,就将‘stage-x’添加到.babelrc的presents数组中,x目前可以为0,1,2,3
使用babel-core模块,可以在代码中手动调用babelAPI进行转码
使用babel-polyfill模块,可以转换新版es6/es7里面的API,如generator,Symbol,Promise等
安装babel-cli实现命令行进行转码
另外babel经常用于前置转码,比如与eslint结合进行代码检查,与mocha进行脚本测试

My Little World

关于HTTP

发表于 2017-10-05

在浏览器的地址栏输入url后发生了什么

粗略的回答的话,就是浏览器向服务器发送该url,浏览器根据该url找到相应的资源,再把资源返回给浏览器,即使我们看到的页面内容
细致的回答的话,浏览器会在应用层利用DNS协议解析域名生成ip地址,利用http协议生成一个http请求然后传递给传输层,传输层将根据TCP协议将这个http请求报文进行分割并打上序号标记、端口号和三次握手标志后转发给网络层,网络层会利用ARP协议根据ip地址查到MAC地址,IP协议对数据封装MAC地址等信息,将打包好的数据包发送给链路层再封装然后发送出去,经过一系列转发,忽略路由、CDN的缓存策略,中转到达服务器后,服务器再按链路层,网路层,传输层,应用层的顺序,从下到上依次解封数据信息,找到资源后,同样分割,封装打包再传给客户端浏览器。

重新看待这个问题

流程图1
流程图2
browerurl

浏览器渲染过程

解析HTML构建DOM树时,渲染引擎会先将HTML元素标签解析成
由多个DOM元素对象节点组成且具有节点父子关系的DOM树结构,
然后根据DOM树结构的每个节点顺序提取计算使用的CSS规则
并重新计算DOM树结构的样式数据,
生成一个带有样式描述的dom渲染树对象
DOM渲染树生成结束后进入渲染树的布局阶段
即根据每个渲染树节点在页面中的大小和位置,将节点固定到页面对应位置上
这个阶段主要是元素的布局属性(position,float,margin等属性)
即在浏览器中绘制页面上元素节点的位置
绘制阶段将渲染树节点的背景,颜色,文本等样式信息应用到每个节点上
这个阶段主要是元素的内部显示样式(color,background,text-shadow等属性)生效
最终完成整个DOM在页面上的绘制显示

不同浏览器内核渲染过程有区别
Webkit内核(google,safari)中HTML和css解析可以认为是并行的,生成的渲染对象被叫做渲染树(render tree)
Geoko内核(firefox)则是先解析html生成sink后再开始解析CSS,生成的渲染对象称为Frame树(Frame tree)

在渲染树生成阶段,DOM树中的结点会在CSS分析树中根据元素,类,id选择器来提取与之对应元素的一条或多条CSSRule,进行CSS规则的层叠和权重计算,
得到最终生成的样式CSSRule并添加到DOM渲染树上,
当每个Dom节点提取CSS样式完成时,用于页面布局和绘制的DOM渲染树便形成了
在一个已经形成的DOM渲染树中,节点的CSS规则可通过
document.defaultView.getComputedStyle(element,null)
方法获取

关于 TCP/IP 协议

1.不同的硬件、操作系统之间的通信,所有这一切都需要一种规则,这种规则即协议
2.与互联网相关联的协议集合起来就是TCP/IP
3.http协议是TCP/IP协议的一个子集
4.URI(统一资源标识符),用字符串标识某一互联网资源,而url表示资源的地点

分层管理

好处:局部改变设计,只需把变动的层替换,无需整体替换
应用层:应用服务间通信,如FTP,DNS服务,应用http协议
传输层:网络连接中两台计算机之间数据传输,应用TCP和UDP协议
网络层:处理网络上流动的数据包,应用IP协议
链路层:处理连接网络的硬件之间通信,包括操作系统,硬件的设备驱动,光纤等。

三次握手

目的在于确保准确无误的将数据送达目的地
TCP协议在封装数据时会封装SYN和ACK作为握手标识
发送端首先发送一个带SYN标识的数据包给对方,接收端收到后,回传一个带有SYN/ACK标识的数据包以示传达确认信息,最后,发送端再回传一个带ACK标识的数据包,代表‘握手’结束
若在握手过程中某个阶段莫名中断,TCP协议会再次以相同的顺序发送相同的数据包

HTTP协议

1.请求报文由请求方法、请求URI、协议版本、可选的请求首部字段和内容实体构成
2.响应报文基本上由协议版本、状态码(表示请求成功或失败的数字代码)、用以解释状态码的原因短语、可选的响应首部字段以及实体主体构成
3.http是无状态协议,即不保存之前发送过的请求或响应
4.持久链接的特点:只要任意一端没有明确提出断开连接,则保持TCP连接状态,
旨在建立1次TCP连接后进行多次请求和响应的交互
5.管线化技术:在持久链接状态下,不用等待响应亦可直接发送下一个请求,实现并行发送多个请求
6.请求方法
GET:访问已被URI识别的资源,获取响应内容
POST:传输实体的主体
PUT:在请求报文主体中包含文件内容,然后保存到请求URI指定的位置,自身不带验证机制
HEAD:用于确认URI的有效性及资源更新的日期时间等
DELETE:按请求URI删除指定的资源,不带验证机制
OPTION:查询针对请求URI指定的资源支持的方法
TRACE: 将之前的请求通信环回给客户端,可以查询发送出去的请求是怎样被加工修改/篡改的,但容易引发XST攻击
CONNECT:在与代理服务器通信时建立隧道,实现用隧道协议进行TCP通信。主要使用SSL和TLS协议把通信内容加密后经网络隧道传输

COOKIE状态管理

客户端在未携带cookie信息状态下请求服务器时,服务器会生成cookie,并记住是向谁发送的,然后在响应报文头中添加一个叫Set-Cookie的字段信息,通知客户端保存Cookie。
当下次客户端再往该服务器发送请求时,客户端会自动在请求报文中加入Cookie值后发送出去
服务器端发现客户端发送过来的Cookie后,会去检查究竟是从哪一个客户端发来的连接请求,然后对比服务器上的记录,最后得到之前的状态信息.
7.报文由报文首部、空格和报文主体构成

请求报文的报文首部分为:请求行(请求方法,URI,HTTP版本)、请求首部字段、通用首部字段、实体首部字段、其他
响应报文的报文首部分为:状态行(状态码,原因短语,HTTP版本)、响应首部字段、通用首部字段、实体首部字段、其他

报文主体即传输请求或响应的实体主体

提升传输效率采取的方法:传输编码,对实体进行‘内容编码’,压缩传输内容,分块传输编码

发送不同类型数据时,需要首部字段Content-Type
请求报文的Content-Type赋值mulitipart/form-data,
响应报文的Content-Type赋值mulitipart/byteranges,

请求指定范围数据时,可以用首部字段Range指定资源的byte范围
例:请求开始到3000,和5000到7000字节的多重范围
Range:bytes=-3000,5000-7000
响应状态码会返回206,多重范围会在首部添加Content-Type:mulitipart/byteranges
如果服务器无法响应范围请求,则会返回状态码200OK和完整的实体内容

内容协商机制:客户端和服务端就响应的资源内容进行交涉,然后提供给客户端最为适合的资源。
内容协商会以响应资源的语言、字符集、编码方式等作为判断的基准
对应首部字段有:Accept,Accept-Charset,Accept-Encoding,Accept-Language,Content-Language
内容协商技术有三类:
服务器驱动协商:以请求首部字段为参考,在服务器端自动处理
客户端驱动协商:用户在浏览器可选列表中手动选择
透明协商:前两者的结合体,由服务端和客户端各自进行内容协商的一种方法

8.状态码

1XX:请求正在处理
2XX:请求正常那个处理完毕
3XX:重定向,需要进行附加操作以完成请求,
4XX:服务器无法处理请求,客户端有错误发生
5XX:服务器处理请求出错

200 OK 客户端的请求在服务器被正常处理了
204 No Content ,正常处理,但响应不含实体
206 Partial Content 成功处理了范围请求,响应只含指定范围实体

301 Moved Permanently 永久重定向
302 Found 临时重定向
303 See Other 临时重定向,但只能使用get方法访问该资源
304 Not Modified 服务器没有找到符合客户端附带条件(If-Match,If-Range等报文首部)的资源,响应不含主体,与重定向无关,服务器告诉客户,原来缓冲的文档还可以继续使用
307 Temporary Redirect 临时重定向,按照浏览器标准不会从post变成GET。但浏览器不同,出现情况也可能不同

400 Bad Request 请求报文存在语法错误
401 Unauthorized 携带信息未通过HTTP认证,没有权限访问资源
403 Forbidden 请求的资源被服务器拒绝访问
404 Not Found 无法找到请求资源

500 Internal Server Error 服务器在执行请求时发生了错误,也可能是web应用存在bug或临时故障
502 bad gateway 网关错误
503 Service Unavailable 服务器暂时处于超负载或正在进行停机维护

9.通信数据转发程序
代理:接收客户端请求并转发给源服务器,不改变URI,但在转发时会添加via首部
好处:利用缓存技术减少网络带宽的流量,组织内部针对特定网站的访问控制,以获取访问日志为主要目的等等
使用方法分两类:
1.是否使用缓存,例,缓存代理(预先将资源副本存在代理服务器上);
2.是否会修改报文,例,透明代理(对报文不做任何加工,反之非透明代理)

网关:转发其他服务器数据的服务器,接收从客户端发来的请求并对请求进行处理
好处:能使通信线路上服务器提供非HTTP协议服务,能提高通信安全性

隧道:在相隔甚远的客户端和服务器之间进行中转,并保持双方通信连接的应用程序
目的:确保客户端能与服务器进行安全的通信,通信双发断开连接时结束

缓存:代理服务器或客户端本地磁盘内保存的资源副本 ,其实相当于是一种代理服务器
目的:减少对源服务器的访问,从而节省通信流量和通信时间
机制:超过有效期之后,会向源服务器确认资源有效性,若判断缓存失效,则再次从源服务器上获取‘新’资源

10.首部字段
分两类:缓存代理和非缓存代理
缓存代理,即端到端首部,这类首部会被带到客户端或服务端,且会保存在缓存中
非缓存代理,即逐跳首部,只对单词转发有效,会因通过缓存或代理而不再转发

通用首部:客户端和服务端都会用的首部
header1
请求首部
header2
响应首部
header3
实体首部
header4
cookie 相关首部
header5
一些需要注意的字段
connection:close 完成本次响应后,无需等待后续请求,断开链接
connection:keep-alive 完成本次响应后保持链接等待后续请求

connection:upgrade
upgrade:websocket
这两者组合在一起时,表示客户端申请的更换协议,
服务端允许切换,此时报头为101,表示还需要等待完成切换的过程

content-Encoding 表示服务端压缩方法
Transfer-Encoding 表示服务端编码方法

https

http缺点

1.通信使用明文(不加密),内容可能会被窃听
造成原因:通信线路上的网络设备、光纤等不可能是个人私有物,不排除某个环节会遭到恶意窥视或窃听
解决办法:加密处理防止被窃听
方式一:通信加密,使用SSL和TLS建立安全通信线路,再在这条线路上进行HTTP通信
方式二:内容加密,对HTTP协议传输的内容本身加密,前提要求服务端和客户端都具备加密解密机制

2.不验证通信方的身份,因此可能遭遇伪装
造成原因:服务器不管谁发来的请求都会给一个响应,客户端不管是谁的响应都接收
解决办法:查明对方证书,证书由第三发颁发,通信前先确认对方证书,验证时要通信的双方后,再继续通信

3.无法证明报文完整性,所以有可能已遭篡改
中间人攻击:请求或响应在传输途中,遭攻击者拦截并篡改内容的攻击
造成原因:中间人攻击
解决办法:MD5和SHA-1等散列值校验的方法,以及用来确认文件的数字签名方法,但以上方法都需要用户本人自己验证

HTTP+加密+认证+完整性保护 = HTTPS

HTTP通信接口部分用SSL和TLS协议代替,即不再是http与TCP通信,而是http和SSL再和TCP通信
共享/对称密钥加密:加密和解密用同一个密钥
公开密钥加密:
使用一对非对称的密钥,一把叫做私有密钥,另一把叫做公开密钥,
使用公开密钥进行加密发送密文,收信方收到信息后用私有密钥进行解密
公开密钥加密方式比共享密钥加密方式处理起来更麻烦

HTTPS混合加密:在交换密钥环节使用公开密钥加密方式,之后的建立通信交换报文阶段则使用共享密钥加密方式
缺点:无法保证公开密钥本身是真正的公开密钥
解决办法:使用数字证书进行验证,客户端根据数字证书机构的公开密钥验证数字签名,从而验证服务器公开密钥
header6
关于证书:
客户端证书一般由银行等特殊业务颁发,因为客户端证书需要费用支出
自签名证书无法保证通信真实性
中级认证机构的证书可能变成自认证证书
HTTPS的安全通信机制
header7
header8
步骤12结束之后,客户端再发送TCP FIN报文来关闭与TCP的通信
缺点:
1.由于加密解密过程消耗大量CPU以及内存等资源,导致处理速度变慢
2.由于SSL通信部分消耗网络资源,处理通信部分又会消耗一定时间,因此相比http又会变慢
解决办法:使用SSL加速器以分担负载

不使用https原因:
1.客户端要考虑消耗cpu,内存资源,服务器端要考虑负载
2.购买证书需要开销

用户认证

方法有BASIC认证,DIGEST认证,SSL客户端认证,FormBase认证

HTTP/2.O

http2.0协议优点
1.采用二进制进行流式传输,使用HPACK压缩消息头,节省宽带
2.使用tcp多路复用,降低网络请求链接时建立关闭的开销
这里要与区别keep-alive作区分
keep-alive发生在应用层,而tcp复用发生在传输层
keep-alive多文件只能串行传输,一个传完再传一个,而TCP多文件可以并行同时传输
3.可以控制传输流优先级和流量
4.支持服务端推送

多路复用

解决的问题

在http/1.x下,浏览器客户端在同一时间,针对同一域名下的请求有一定数量限制。超过限制数目的请求会被阻塞
突发性和短时性的 HTTP 连接因为三次握手和慢启动的存在,非常浪费时间(–>单一长连接)
(慢启动:TCP连接在一开始会限制连接最大速度,确认数据能够成功传输后,随时间推移才会慢慢提高传输速度)

实现–二进制分帧

在应用层和传输层之间增加二进制分帧层,应用层生成的报文会分割封装为更小的消息和帧,并进行二进制编码
其中首部header和请求主体request body分别被封装为header frame 和data frame
同一请求或响应的帧会有相同标志,会按照一定先后顺序被发送,
所以不同的请求或者响应可以互相穿插着给到对方
最终实现就是
一个包含上百个资源的页面,在http1.x协议下会创建多条TCP链接来进行串行请求
在http/2.0下只会创建一个长TCP链接,所有的资源请求在压缩处理后一次性发出去,并行地在一个TCP连接上双向交换信息
(同一链接上有多个不同方向的数据流在传输。客户端可以一边乱序发送stream,也可以一边接收服务器的响应,而服务器那端同理。)

而且可以对资源设置优先级,优先级能动态的被改变,优先级高的资源能够被更快的下载下来并得到执行

效果

由于 TCP 连接的减少而使网络拥塞状况得以改善,使拥塞和丢包恢复速度更快
单一长连接多资源的方式,减少服务端的链接压力,避免网络开销,内存占用更少,提高连接吞吐量

压缩头部

通信连接建立之后,由于cookie和user agent很容易膨胀,
每次请求的头部如果有相同的部分,还要以纯文本传输重复发送的话,就会造成流量浪费
因此在客户端和服务端都存放一个首部表,来跟踪和存储之前发送的键值对,对于相同的头部,不必再通过请求发送,只需发送一次
如果首部发生变化,就在压缩时只将变化的数据放在header帧里面进行发送,
无论是客户端还是服务器端收到首部帧后,就对首部表进行更新

服务器推送

就是客户端请求一次资源,服务器给多个响应,这些多个响应很有可能是浏览器解析Html后继续要请求的资源,即主动将资源推送给客户端
而客户端可以做的就是可以将他们缓存起来做备用

延伸问题

1.如何解决推送资源重复或不需要的问题?
客户端使用一个简洁的 Cache Digest 来告诉服务器,哪些东西已经在缓存,因此服务器也就会知道哪些是客户端所需要的。
相关链接

2.为什么HTTP2能去掉SSL在HTTP1.x上的开销
单一的长连接,减少了SSL握手的开销
头部被压缩,减少了数据传输量
多路复用能大幅提高传输效率,不用等待上一个请求的响应
不用像http1.x那样把多个文件或者资源弄成一个文件或者资源(http1.x常见的优化手段),
这时候,缓存就能更容易命中啊(http1.x里面你揉成一团的东西怎么命中缓存?)
相关链接

相关链接

遗留疑问

二进制分帧和TCP切割分包是如何具体实现的,二进制分帧优势具体在哪里?
http/2.0各层协议具体工作
待读

补充

从输入 URL 到页面展现中间发生了什么?<另外的角度看问题>

DNS 查询 DNS 缓存
建立 TCP 连接(三次握手)连接复用
发送 HTTP 请求(请求的四部分)
后台处理请求
监听 80 端口
路由
渲染 HTML 模板
生成响应
发送 HTTP 响应
关闭 TCP 连接(四次挥手)
解析 HTML
下载 CSS(缓存
解析 CSS
下载 JS(缓存
解析 JS
下载图片
解析图片
渲染 DOM 树
渲染样式树
执行 JS

七层协议
应用层
表示层
会话层
传输层
网络层
数据链路层
物理层

实时通信协议

webSocket
Poll轮询
Long-poll 长轮询 (二维码登录)
DDP(分布式数据协议)[使用json数据格式在客户端与服务端进行数据传输,但存在兼容问题]

My Little World

关于promise

发表于 2017-08-31

promise

Promise 是异步编程的一种解决方案

简单使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var promise = new Promise(function(resolve, reject) {
// ... some code

if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});

promise
.then(function(value) { //resolve(value);回调函数
// success
}, function(error) { // reject(error);回调函数
// failure
})
.catch(function(error) {
// 处理 getJSON 和 前一个回调函数运行时发生的错误
console.log('发生错误!', error);
});

1、Promise对象是一个构造函数,用来生成Promise实例,
2、Promise构造函数的参数是一个函数,该函数有两个参数,分别是两个函数:resolve和 reject
3、resolve函数在promise对象状态从Pending变为Fulfiled时调用,即异步操作成功的时候调用,参数一般为获取的结果,方便回调函数使用,或者另一个promise实例,继续进行回调
reject函数在promise对象状态从Pending变为Rejected时调用,即异步操作失败的时候调用,参数一般为错误Error对象,报出错误
4、Promise实例生成以后,可以用then方法分别指定Resolved状态和Rejected状态的回调函数
5、then方法有两个参数,均为匿名函数,第一个匿名函数为resolve()的定义,第二个参数为reject()的定义
6、Promise对象状态变为Resolved,则会调用then方法指定的回调函数;如果异步操作抛出错误,状态就会变为Rejected,就会调用catch方法指定的回调函数,处理这个错误。
另外,then方法指定的回调函数,如果运行中抛出错误,也会被catch方法捕获。
注意,如果then方法定义了reject(),将不会再调用catch方法,如果then里面没有reject,发生错误时将会调用catch

执行流程

1.then方法会在与promise同步的任务完成之后再执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let promise = new Promise(function(resolve, reject) {
console.log('Promise');
resolve();
});

promise.then(function() {
console.log('Resolved.');
});

console.log('Hi!');
//打印顺序
"Promise"
"Hi!"
"Resolved."

Promise 新建后立即执行,所以首先输出的是Promise。然后,then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以Resolved最后输出。

2、如果Promise1的resolved返回另一个promise2,那Promise1的then会根据promise2的状态决定执行reject还是resolved

1
2
3
4
5
6
7
8
9
10
11
12
var p1 = new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('fail')), 3000)
})

var p2 = new Promise(function (resolve, reject) {
setTimeout(() => resolve(p1), 1000)
})

p2
.then(result => console.log(result))
.catch(error => console.log(error))
// Error: fail

上面代码中,p1是一个Promise,3秒之后变为rejected。p2的状态在1秒之后改变,resolve方法返回的是p1。由于p2返回的是另一个 Promise,导致p2自己的状态无效了,由p1的状态决定p2的状态。所以,后面的then语句都变成针对后者(p1)。又过了2秒,p1变为rejected,导致触发catch方法指定的回调函数。

3、在Promise构造函数的参数函数里,在调用resolve()或reject()之后,仍会执行后续语句,但如果是抛错语句将不会执行,同样在抛错语句之后的resolve()调用也不会被执行

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
var promise = new Promise(function(resolve, reject) {
resolve('ok');
console.log('1111')
});
promise
.then(function(value) { console.log(value) })
.catch(function(error) { console.log('报错啦') });
//1111
//ok
//因为立即 resolved 的 Promise 是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务,所以会先打印1111,,再打印ok

var promise = new Promise(function(resolve, reject) {
return resolve('ok');
console.log('1111')
});
promise
.then(function(value) { console.log(value) })
.catch(function(error) { console.log('报错啦') });
//ok
//return 方法阻止继续执行后续操作

var promise = new Promise(function(resolve, reject) {
resolve('ok');
throw new Error('test');
});
promise
.then(function(value) { console.log(value) })
.catch(function(error) { console.log('报错啦') });
//ok

var promise = new Promise(function(resolve, reject) {
throw new Error('test');
resolve('ok');

});
promise
.then(function(value) { console.log(value) })
.catch(function(error) { console.log('报错啦') });
//报错啦

4、then方法会返回一个promise对象,因此可以继续在then函数后面加then函数,这时前面then函数应该会return一个结果值作为后面then函数的参数,
前面then函数如果执行resolve()则后面then函数也会执行resolve(),前面then函数如果执行reject()则后面then函数也会执行reject(),
但如果前面then函数return一个promise对象,那后面的then函数将会根据这个promise的执行结果去执行resolve()还是reject()

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
var promise = new Promise(function(resolve, reject){
resolve(123)
});
promise.then(function(json) {
console.log('Contents: ' + json);
return 1223;
}, function(error) {
console.error('出错了', error);
return '2423'
})
.then(function(val){
console.log(val);
},function(val){
console.log(val)
});
//"Contents: 123"
//1223

var promise = new Promise(function(resolve, reject){
reject(456)
});
promise.then(function(json) {
console.log('Contents: ' + json);
return 1223;
}, function(val,error) {
console.log(val);
console.error('出错了', error);
return '2423'
})
.then(function(val){
console.log(val);
},function(val){
console.log(val)
});

//456
//"出错了"
//"2423"

var promise = new Promise(function(resolve, reject){
// resolve(123)
reject(456)
});
var promise1 = new Promise(function(resolve, reject){
resolve(789)
});
promise.then(function(json) {
console.log('Contents: ' + json);
return 1223;
}, function(val,error) {
console.log(val);
console.error('出错了', error);
return promise1
})
.then(function(val){
console.log(111+val);
},function(val){
console.log(222+val)
});

//456
//"出错了"
//900

then 方法链式调用箭头函数格式

1
2
3
4
5
6
promise.then(
param1 => {}
).then(
param2 => {},
err => {}
);

5、promise 如果发生了错误,无论reject()或者catch在哪一层,会一直向后传递,直到被捕获为止,即遇到reject()或者catch就会抛出来,如果后续所有回调函数中都没有reject()或者catch,错误就不会被抛出来
catch方法会返回一个promise,所以可以继续写then(),catch()抛完错之后会继续执行后面这个then()
catch()里面还可以再抛错,如果catch后面没有reject()或者catch(),错误将不会被抛出来
如果catch之前的promise没有遇到错误,catch之后又有then(),则执行流程会跳过catch,继续执行catch后面的then

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
var promise = new Promise(function(resolve, reject) {
throw new Error('test');
});
promise
.then(function(value) { console.log(value); return '2'}, function() { //捕获test
throw new Error('test1');
})
.then(function(value) { console.log(value);return '3' })
.catch(function(error) { console.log(error);return '4'}) //捕获test1
.then(function(value) { console.log(value); }) //4


var promise = new Promise(function(resolve, reject) {
resolve('1')
});
promise
.then(function(value) { console.log(value); return '2'}, function() { //1
throw new Error('test1');
})
.then(function(value) { console.log(value);return '3' })//2
.catch(function(error) { console.log(error) return '4'})
.then(function(value) { console.log(value); })//3 没遇到错误直接跳过来

var promise = new Promise(function(resolve, reject) {
throw new Error('test');
});
promise
.then(function(value) { console.log(value); return '2'}, function(error) { //捕获test
console.log(error)
throw new Error('test1');
})
.then(function(value) { console.log(value);return '3' })
.catch(function(error) { console.log(error);throw new Error('test2'); return '4'}) //捕获test1
.then(function(value) { console.log(value); },function(error){//捕获test2
console.log(error)
})

6.promise resolve return err 并不会被catch 或者reject捕获,会将错误以参数形式传给then的resolve回调函数

1
2
3
4
5
6
7
8
9
10
Promise.resolve()
.then(() => {
return new Error('error!!!')
})
.then((res) => {
console.log('then: ', res) //打印出err
})
.catch((err) => {
console.log('catch: ', err)
})

then 或 .catch 返回的值不能是 promise 本身,否则会造成死循环

1
2
3
4
5
6
7
const promise = Promise.resolve()
.then(() => {
return promise
})
promise.catch(console.error)

//抛错

.then 或者 .catch 的参数期望是函数,传入非函数则会发生值穿透

1
2
3
4
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(console.log) ///打印1

API

Promise.all

用于将多个 Promise 实例,包装成一个新的 Promise 实例
var p = Promise.all([p1, p2, p3])
p的状态由p1、p2、p3决定,分成两种情况。
(1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。

(2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

Promise.race

将多个Promise实例,包装成一个新的Promise实例
var p = Promise.race([p1, p2, p3]);
只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数

Promise.resolve

将现有对象转为Promise对象
var Promise = Promise.resolve(参数);

Promise.reject

返回一个新的 Promise 实例,该实例的状态为rejected。
var p = Promise.reject(‘出错了’);
Promise.reject()方法的参数,会原封不动地作为reject的理由,变成后续方法的参数

done finally

done方法,总是处于回调链的尾端,保证抛出任何可能出现的错误,没有参数
finally方法用于指定不管Promise对象最后状态如何,都会执行的操作
可以接受一个普通的回调函数作为参数,该函数不管怎样都必须执行。

My Little World

对象构造和继承

发表于 2017-08-19

对象特性

对象的属性在定义时,都带有一些特征值,js通过这些特征值定义他们的行为
这些特征值描述对象属性的各种特征,成为对象属性的特性
特性是内部值,放在两对方括号中访问
特性分为数据属性和访问器属性
数据属性:Configurable、Enumerable、Writable、Value
访问器属性:Configurable、Enumerable、Get、Set
定义某个属性的特性:Object.defineProperty(对象名,对象属性名,{特性1:值,特性2:值…})
定义多个属性特性:Object.defineProperties(对象名,{属性1:{特性1:值,特性2:值…},属性2:{特性1:值….}})
读取属性:Object.getOwnPropertyDescriptor(对象名,属性名) 返回一个对象

构造对象

工厂模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function createobj(name,age){
var o=new Object();
o.name = name;
o.age = age;
o.sayName = function(){
console.log(this.name)
};
return o;
}
var person1 = createobj('gray',25);
var person2 = createobj('black',26);
person1.sayName()//gray
person2.sayName()//black
console.log(typeof person1)//object
console.log(person1 instanceof createobj) //false

缺点:无法识别对象类型

寄生构造函数

定义样子同工厂模式,只不过创建实例时用new 操作符
将包装函数叫做 构造函数
实例与构造函数及其原型没有关系

1
2
3
4
5
6
7
8
9
10
11
12
13
function createobj(name,age){  //构造函数
var o=new Object();
o.name = name;
o.age = age;
o.sayName = function(){
console.log(this.name)
};
return o;
}
var person1 = new createobj('gray',25);
person1.sayName()//gray
console.log(person1 instanceof createobj) //false
console.log(person1 instanceof Object) //true

应用:为无法修改构造函数的对象添加方法

1
2
3
4
5
6
7
8
9
10
11
//给数组增加特殊方法
function specialArray(){
var values = new Array();
values.push.apply(values,arguments);
values.toPiedString = function(str){ //特殊方法
return this.join(str);
};
return values;
}
var colors = new specialArray('red','blue');
console.log(colors.toPiedString('|'));//"red|blue"

构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function createobj(name,age){
this.name = name;
this.age = age;
this.sayName = function(){
console.log(this.name)
};
}
var person1 = new createobj('gray',25);
person1.sayName(); //gray

console.log(person1 instanceof createobj)//true
console.log(person1 instanceof Object)//true

console.log(window.name)//''
var person2 = createobj('black',26);
console.log(person2) //undefined
console.log(window.name) //black
console.log(person1.sayName == window.sayName)//false

var person3 = new createobj('orange',25);
console.log(person1.sayName == person3.sayName)//false

console.log(person1.constructor == createobj)//true
console.log(person3.constructor == person1.constructor)//true

1、构造函数的实例均指向构造函数。例:person1、person3的constructor均指向createobj)
2、不使用new 关键字调用构造函数,构造函数的方法和属性会挂到window上面。例:person2
3、定义在构造函数上的方法,创建不同实例后,不同实例各自拥有自己的该方法,不同实例之间构造函数方法不相等,不共享

使用构造函数创建对象,如果相让对象方法在不同实例实现共享,则在定义方法时,采用引用函数的方法

1
2
3
4
5
6
7
8
9
10
11
function createobj(name,age){
this.name = name;
this.age = age;
this.sayName = sayName;
}
function sayName(){
console.log(this.name)
}
var person1 = new createobj('gray',25);
var person3 = new createobj('orange',25);
console.log(person1.sayName == person3.sayName)//true

缺点:若对象需要定义多个方法则需要创建多个全局函数供构造函数引用,失去封装性

原型模式

在对象的prototype中定义的属性和方法,可以在所有实例中共享
定义方法一

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
function createobj(name,age){}
createobj.prototype.name = 'black';
createobj.prototype.age = 26;
createobj.prototype.sayName = function(){
console.log(this.name)
}
var person1 = new createobj();
var person2 = new createobj();
person1.sayName();//black
person2.sayName();//black
console.log(person1.sayName == person2.sayName)//true

console.log(person1.constructor == createobj)//true
console.log(person2.constructor == person1.constructor)//true


console.log(person1.age) //26 来自原型
person1.age = 27; //相当于创建了一个实例属性
console.log(person1.age) //27 来自实例属性
console.log(person2.age) //26 来自原型
delete person1.age; //删除实例属性
console.log(person1.age) //26 来自原型
delete person1.age; //不能删除原型属性
console.log(person1.age) //26
console.log(person2.age) //26

console.log(person1 == createobj.prototype)//false
console.log(person1.prototype == createobj.prototype)//false
console.log(createobj.prototype.isPrototypeOf(person1))//true
console.log(person1.prototype) //undefined
console.log(Object.getPrototypeOf(person1));//createobj.prototype

console.log(person1.hasOwnProperty('name'));//false
console.log('name' in person1);//true
console.log(Object.keys(createobj.prototype));//["name", "age", "sayName"]
console.log(Object.getOwnPropertyNames(createobj.prototype));//["constructor", "name", "age", "sayName"]

obj1
1、实例的内部指针[[Prototype]] 会指向构造函数的原型对象,它是不能直接访问的
2、构造函数的prototype也会指向构造函数的原型对象,但他是构造函数的一个属性,可以直接访问
3、Object.getPrototypeOf(实例名); 返回实例的原型对象 .即[[Prototype]]指向的对象
4、实例名.hasOwnProperty(属性名字符串) ; 只检查实例的实例属性中是否有该属性,没有的话,返回false,可用于检查该属性在实例属性中,还是在原型属性中
5、在获取属性值时,实例属性中有该属性就取该实例属性的值,没有则用原型属性值,原型属性也没有的话则返回undefined
6、实例属性可用delete删除,实例无法更改原型属性的值
7、属性名字符串 in 实例名; 检查实例和原型所有属性中是否有该属性,有的话就返回true;for-in循环可枚举的属性,包括实例属性和原型属性
8、Object.keys(对象名);返回对象可枚举属性的字符串数组
9、Object.getOwnPropertyNames(对象名);返回所有对象的实例属性,无论它是否枚举

定义方法二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function createobj(){}
createobj.prototype={
name:'seven',
sayName:function(){
console.log(this.name)
}
};
var person1 = new createobj();
person1.sayName();//seven
console.log(person1.constructor == createobj);//false
console.log(person1.constructor == createobj.prototype);//false
console.log(person1.constructor == Object);//true
console.log(person1.constructor == createobj.prototype.constructor);//true
console.log(person1.constructor);//function object(){}

console.log(person1 instanceof Object);//true
console.log(person1 instanceof createobj);//true

1、将挂在构造函数原型上的属性以一个对象的形式,一起赋给构造函数原型,即构造函数原型被赋值为一个对象
2、构造函数原型被Object创建,实例、构造函数的原型对象的构造函数为Object对象
3、若在定义原型对象时,指定原型对象的constructor,则对象实例的constructor也会跟着指到相应的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function createobj(){}
createobj.prototype={
constructor:createobj, //明确指出,同时特性的枚举属性会被设置为true,默认为false
name:'seven',
sayName:function(){
console.log(this.name)
}
};
var person1 = new createobj();
person1.sayName();//seven
console.log(person1.constructor == createobj);//true
console.log(person1.constructor == createobj.prototype);//false
console.log(person1.constructor == Object);//false
console.log(person1.constructor == createobj.prototype.constructor);//true
console.log(person1.constructor);//function createobj(){}

console.log(person1 instanceof Object);//true
console.log(person1 instanceof createobj);//true

4、 实例中的指针指向原型,不指向构造函数

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

var person1 = new createobj();
console.log(person1 instanceof createobj);//true
createobj.prototype={
constructor:createobj,
name:'seven',
sayName:function(){
console.log(this.name)
}
};
//person1.sayName();//person1.sayName is not a function
console.log(person1.constructor == createobj);//true
console.log(person1.constructor == createobj.prototype);//false
console.log(person1.constructor == Object);//false
console.log(person1.constructor == createobj.prototype.constructor);//true
console.log(person1.constructor);//function createobj(){}

console.log(person1 instanceof Object);//true
console.log(person1 instanceof createobj);//false

obj2
对象生成实例后,再去重写构造函数的原型对象,会将构造函数的原型对象指向后来被赋的对象,
切断与旧的原型对象之间的关系,新的原型对象与实例没有任何关系,实例仍引用旧的原型对象

原型方法可用于修改添加原生对象的属性方法
缺点:在原型对象prototype中的属性方法全都共享,当更改一个实例中的属性值时,其他实例一起变

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
//定义一
function createobj(name,age){}
createobj.prototype.name = [1,2];
createobj.prototype.sayName = function(){
console.log(this.name)
}
var person1 = new createobj();
var person2 = new createobj();
person1.sayName();//[1, 2]
person2.sayName();//[1, 2]
person1.name.push(3);
person1.sayName();//[1, 2, 3]
person2.sayName();//[1, 2, 3]
//定义二
function createobj(){}
createobj.prototype={
constructor:createobj,
name:[1,2],
sayName:function(){
console.log(this.name)
}
};
var person1 = new createobj();
var person2 = new createobj();
console.log(person1.name)//[1, 2]
console.log(person2.name)//[1, 2]
person1.name.push(3);
console.log(person1.name)//[1, 2, 3]
console.log(person2.name)//[1, 2, 3]

构造函数+原型模式

构造函数定义实例属性,
原型模式定义方法和共享属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function createobj(name,age){
this.name = name;
this.age = age;
this.friends = [1,2];
}
createobj.prototype={
constructor:createobj,
sayName:function(){
console.log(this.name)
}
};
var person1 = new createobj('halo',24);
var person2 = new createobj('jack',25);
person1.friends.push(3);
console.log(person1.friends);//[1, 2, 3]
console.log(person2.friends);//[1, 2]

是用来定义引用类型的一种默认模式

动态原型模式

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
function createobj(name,age){
this.name = name;
this.age = age;
if(typeof this.sayName !='function'){
createobj.prototype.sayName = function(){
console.log(this.name)
}
}
}
var person1 = new createobj('halo',24);
var person2 = new createobj('jack',26);
person1.sayName();//halo
person2.sayName();//jack

//修改原型
createobj.prototype.sayName = function(){
console.log('111')
}
person1.sayName();//111
person2.sayName();//111

//使用字面方式重写
createobj.prototype={
constructor:createobj,
sayName:function(){
console.log(222)
}
};
var person3 = new createobj('kitty',24);
person1.sayName();//111
person2.sayName();//111
person3.sayName();//222
console.log(person1.prototype);//undefined

//重写一个原型方法
person1.sayName=function(){
console.log('333');
}
person1.sayName();//333
person2.sayName();//111

1、判断sayName函数是否存在的语句,只会在初次调用构造函数的时候运行,即第一次创建实例的时候运行,
运行过后,构造函数原型中就会存在sayName函数,即完成初始化,之后就不会在运行
2、对原型模式定义的方法属性能够在所有实例中立即得到放映
3、如果对构造函数原型使用字面方式重写,将切断已有实例与构造函数原型的联系,
已有实例会指向就原型对象,新建实例会指向新原型对象
4、在一个实例中重写一个原型方法,不会影响原型对象方法,其他实例和新建实例仍会调用原型对象方法

稳妥构造函数

创建对象的实例方法,不使用this
不使用new调用构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function createobj(name,age){  //构造函数
var o=new Object();
o.name = name;
o.age = age;
o.sayName = function(){
console.log(this.name);
console.log(this == o)
};
return o;
}
var person1 = createobj('gray',25);
person1.sayName()//gray true

console.log(person1 instanceof createobj) //false

只用通过sayName函数访问传入到构造函数中的原始成员

继承

实例原型链模式

继承函数的原型被赋值为祖先函数的实例

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
//祖先函数
function Superfn(){
this.pro = true;
this.color = [1,2];
this.getsuper = function(){
console.log('aaa');
}
}
Superfn.prototype.getsuper=function(){
console.log('bbb');
}
//继承函数
function Subfn(){
this.subpro = false;
this.getsuper = function(){
console.log('000');
}
}
Subfn.prototype = new Superfn();
Subfn.prototype.getsuper = function(){
console.log('ccc')
}
var ins = new Subfn();
ins.getsuper();

console.log(ins instanceof Object); //true
console.log(ins instanceof Superfn); //true
console.log(ins instanceof Subfn); //true
console.log(Object.prototype.isPrototypeOf(ins)); //true
console.log(Superfn.prototype.isPrototypeOf(ins)); //true
console.log(Subfn.prototype.isPrototypeOf(ins)); //true

var ins1 = new Subfn();
console.log(ins.color)//[1,2]
console.log(ins1.color)//[1,2]
ins1.color.push(4);
console.log(ins.color)//[1,2,4]
console.log(ins1.color)//[1,2,4]

1.继承函数实例的属性/方法调用顺序:
自己定义的->继承函数实例->继承函数prototype->祖先函数实例属性->祖先函数prototype
2.因为所有引用类型默认继承object,所以调用toString等方法时,其实调用的的是object的prototype
3.重写原型中祖先函数的方法,一定要在继承函数原型被赋值祖先函数实例之后,相当于用实例方法覆盖原型方法
但注意不能用字面量方法重写继承函数原型,这样会导致继承函数原型重新指向新对象,切断与祖先函数联系
4.因为继承函数prototype指向祖先函数实例时,祖先函数所有属性相当于继承函数prototype属性,
构造函数原型的属性会被所有实例共享,所以创建继承函数实例时,
不能像祖先函数的构造函数中传递参数,因为会导致其他实例同时共享这些参数导致的后果

借用构造函数

使用apply()或者call()方法。在继承函数构造函数中调用祖先函数构造函数,这样,
在new 一个继承函数实例时,就会去执行祖先函数中所有对象初始化代码,每个实例都会具有自己的祖先函数属性的副本

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
function Superfn(time){
this.color = [1,2];
this.time = time;
}

Superfn.prototype.getsuper=function(){
console.log('bbb');
}

function Subfn(){
Superfn.call(this,'11:23:30');
this.age = 29;
}

var ins = new Subfn();
var ins1 = new Subfn();
console.log(ins.color) //[1, 2]
console.log(ins1.color)//[1, 2]
ins1.color.push(4);
console.log(ins.color)//[1, 2]
console.log(ins1.color)//[1, 2, 4]

console.log(ins.time);//"11:23:30"
console.log(ins.age);//29

ins.getsuper();//ins.getsuper is not a function

1、可以像祖先函数构造函数中传递参数;
2、定义继承函数自己的属性必须放在调用祖先函数之后,防止祖先函数重写继承函数属性
缺点: 所有的属性只能使用构造函数模式定义,而且,继承函数实例,不能调用祖先函数原型上的方法

组合继承

在继承函数构造函数中调用祖先函数,实现属性继承
将继承函数的原型赋值为祖先函数实例,实现方法复制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function Superfn(time){
this.color = [1,2];
this.time = time;
}
Superfn.prototype.getsuper=function(){
console.log('bbb');
}
function Subfn(){
Superfn.call(this,'11:23:30');
this.age = 29;
}
Subfn.prototype= new Superfn();
var ins = new Subfn();
var ins1 = new Subfn();
console.log(ins.color)
console.log(ins1.color)
ins1.color.push(4);
console.log(ins.color)
console.log(ins1.color)

console.log(ins.time);//"11:23:30"
console.log(ins.age);//29

ins.getsuper();//bbb

缺点:调用了两次Superfn(),初始化时,Subfn.prototype中的Superfn的实例属性会被构造函数中创建的Superfn的实例属性覆盖,
重复创建,没必要

原型式继承

Object.creat()方法原理像如下函数
function object(o){
function F(){
F.prototype = o;
}
return new F();
}
原型式继承 即使用Object.creat(祖先对象)定义实例,实例会共享祖先对象属性方法,通过添加第二个参数,覆盖祖先对象属性,或新增属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Superfn={
color:['red','green'],
getsuper:function(){
console.log(222);
}
}

var test = Object.create(Superfn,{getsuper:{value:function(){console.log(111)}}});
var test1 = Object.create(Superfn,{color:{value:[1,2]},name:{value:'jack'}});
console.log(test.color);//['red','green']
console.log(test1.color,test1.name); //[1,2] jack
console.log(test.name);//undefined
test.getsuper(); //111
test1.getsuper();//222

寄生式继承

1
2
3
4
5
6
7
8
9
10
11
12
13
function createAnthoer(oriobj){
var clone = Object(oriobj);//创建oriobj对象副本
clone.sayHi=function(){//以某种方式增强这个对象,例如定义自己的方法
console.log('hi')
}
return clone;
}
var Superfn={
color:['red','green'],
}
var test = createAnthoer(Superfn);
test.sayHi()//hi
console.log(test.color);//["red", "green"]

最理想继承-寄生组合式继承

对比组合式继承
仅调用一次superfn函数,避免在Subfn.prototype中创建多余superfn实例属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function inhertPrototype(subfn,superfn){
var prototype = Object(superfn.prototype);//创建对象 ,仅继承祖先函数原型
prototype.constructor = subfn;//增强对象
subfn.prototype = prototype;//指定对象
}
function Superfn(name){
this.color = [1,2];
this.name = name;
}
Superfn.prototype.getsuper=function(){
console.log('bbb');
}
function Subfn(name,age){
Superfn.call(this,name); //仅继承祖先函数实例属性,不会继承祖先函数原型
this.age = age;
}
Subfn.prototype.getsuper=function(){
console.log('ccc');
}
inhertPrototype(subfn,superfn);

类

类的定义相当于es5 对象构造函数的另一种写法,一种新语法
es5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面
es6 类的继承,实质是先创造父类的实例对象this(必须先调用super方法),然后再用子类的构造函数修改this

类的定义

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
//es5
function Point(x, y) {
this.x = x;
this.y = y;
}

Point.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
};

var p = new Point(1, 2);
//es6
let methodName = 'getArea';
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
this.toString = function(){
console.log(111);
}
}

toString() {
return '(' + this.x + ', ' + this.y + ')';
}

[methodName]() {
console.log(666);
}
}

var temp = new Point('helo','world');
console.log(temp)// constructor定义的内容
console.log(temp.toString()) //先找实例属性,在去原型找属性

console.log(typeof Point) // "function"
console.log(Point === Point.prototype.constructor) // true

console.log(Object.keys(Point.prototype));//[]
console.log(Object.getOwnPropertyNames(Point.prototype));//["constructor", "toString"]
Object.assign(Point.prototype, {
toString222(){console.log(222);},
toValue(){console.log(333);}
});
temp.toString222()
console.log(Object.keys(Point.prototype));//["toString222", "toValue"]
console.log(Object.getOwnPropertyNames(Point.prototype));//["constructor", "toString", "toString222", "toValue"]


temp[methodName]() //666

class Foo {
constructor() {
return Object.create(null);
}
}
console.log(Foo.name)//Foo
console.log(new Foo() instanceof Foo)//false
var tt = new Foo();
console.log(tt.__proto__ == Point) //TRUE

var p1 = new Point(2,3);
var p2 = new Point(3,2);
console.log(p1.__proto__ === p2.__proto__)//true
p1.__proto__.printName = function () { console.log('Oops') };
p1.printName() // "Oops"
p2.printName() // "Oops"
var p3 = new Point(4,2);
p3.printName() // "Oops

//这个类的名字是MyClass而不是Me,Me只在 Class 的内部代码可用,指代当前类。
const MyClass = class Me {
getClassName() {
console.log(Me.name);
}
};
let inst = new MyClass();
inst.getClassName() // Me
console.log(MyClass.name)//me

//立即执行的 Class
let person = new class {
constructor(name) {
this.name = name;
}

sayName() {
console.log(this.name);
}
}('张三');

person.sayName(); // "张三"

class MyClass {
constructor() {
// ...
}
get prop() {
return 'getter';
}
set prop(value) {
console.log('setter: '+value);
s}
let inst = new MyClass();
inst.prop = 123;// setter: 123
console.log(inst.prop)// 'getter'

1.constructor函数定义实例属性,this代表实例对象,生成对象实例时,自动调用该方法,
默认返回实例对象this,也可以指定返回另外一个对象,则生成的实例将是另外这个对象的实例
如果没有显式定义,一个空的constructor方法会被默认添加。
其他在constructor函数之外定义的函数全都挂在类的prototype对象上(没有定义在this上的全都定义在class上)
其实constructor函数也挂在prototype对象上
2.定义原型方法时,不需要加上function这个关键字,方法之间不需要逗号分隔
3.类的数据类型就是函数,类本身就指向构造函数
4.使用Object.assign()新增原型方法,定义类时定义的原型方法是不可枚举的,但Object.assign()新增的可枚举
5.可以使用变量值做原型方法名
6.定义实例仍使用new关键字,且必须使用new定义,否则会报错
7.类的所有实例共享一个原型对象,使用实例的proto属性改写原型,会改变“类”的原始定义,影响到所有实例
8.类也可以使用表达式的形式定义,采用 Class 表达式,可以写出立即执行的 Class
9.类必须先定义再使用,无论是生成实例还是进行继承
10.类的name属性总是返回紧跟在class关键字后面的类名。
11.对某个属性设置对应的存值函数和取值函数,因此赋值和读取行为都被自定义了。存值函数和取值函数是设置在属性的 Descriptor 对象上的。
12.静态方法,该方法不会被实例继承,而是直接通过类来调用
如果静态方法包含this关键字,这个this指的是类,而不是实例
父类的静态方法,可以被子类继承。
静态方法也是可以从super对象上调用的
13.
ES6 为new命令引入了一个new.target属性,该属性一般用在构造函数之中,返回new命令作用于的那个构造函数。如果构造函数不是通过new命令调用的,new.target会返回undefined,因此这个属性可以用来确定构造函数是怎么调用的。

类的继承

定义

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
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
console.log(new.target.name);
}
}

class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y);
this.color = color;
}
}

let cp0 = new Point(25, 8);//"Point"
let cp = new ColorPoint(25, 8, 'green');//"ColorPoint" super()使用

console.log(cp instanceof ColorPoint)//true
console.log(cp instanceof Point)//true
console.log(Object.getPrototypeOf(ColorPoint) === Point)

console.log(ColorPoint.__proto__ === Point) // true
console.log(ColorPoint.prototype.__proto__ === Point.prototype) // true

console.log(cp.__proto__.__proto__ === cp0.__proto__ )// true
cp.__proto__.__proto__.printName = function () {
console.log('Ha');
};

cp0.printName() // "Ha"

1、使用extends关键字进行继承:class 子类名 extends 父类名{}
2、在子类构造函数constructor中使用super关键字继承父类this对象,必需在super语句之后使用this
3、Object.getPrototypeOf方法可以用来从子类上获取父类
4、子类的proto属性,表示构造函数的继承,总是指向父类。
子类prototype属性的proto属性,表示方法的继承,总是指向父类的prototype属性。
5、子类的原型的原型,是父类的原型,通过子类实例的proto.proto属性,可以修改父类实例的行为

关于super关键字

1、用作函数
作为函数时,super()只能用在子类的构造函数之中,用在其他地方就会报错,代表调用父类构造函数
super()在这里相当于parent.prototype.constructor.call(this),this指子类

2、用作对象

作为对象使用时,在普通方法中指向父类原型对象

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
class A {
constructor() {
this.p = 2;
}
q() {
console.log(this.p)
}
}

class B extends A {
constructor() {
super();
this.p =3
super.q();
super.p = 5;
console.log(super.p); // undefined
console.log(this.p); // 5
super.tt = 6;
console.log(this.tt); // 6
console.log(super.valueOf() instanceof B); // true
}
get m() {
return super.p;
}
}

let b = new B();//3
console.log(b.m) // undefined

在普通方法中指向父类原型对象,即使用super.xxxx(),相当于使用parent.prototype.xxxx(),
所以只能调用父类原型属性方法,不能使用实例属性方法

通过super调用父类的方法时,super会绑定子类的this,
即在子类使用super.xxxx()时,xxxx()里面的this指子类
通过super对某个属性赋值,赋值的属性会变成子类实例的属性

在静态方法中使用super将指向父类,而不是父类的原型对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Parent {
static myMethod(msg) {
console.log('static', msg);
}

myMethod(msg) {
console.log('instance', msg);
}
}

class Child extends Parent {
static myMethod(msg) {
super.myMethod(msg);
}

myMethod(msg) {
super.myMethod(msg);
}
}

Child.myMethod(1); // static 1 super在静态方法之中指向父类

var child = new Child();
child.myMethod(2); // instance 2 在普通方法之中指向父类的原型对象。

extends特殊对象

第一种特殊情况,子类继承Object类。

1
2
3
4
5
class A extends Object {
}

A.__proto__ === Object // true
A.prototype.__proto__ === Object.prototype // true

这种情况下,A其实就是构造函数Object的复制,A的实例就是Object的实例。

第二种特殊情况,不存在任何继承。

1
2
3
4
5
class A {
}

A.__proto__ === Function.prototype // true
A.prototype.__proto__ === Object.prototype // true

这种情况下,A作为一个基类(即不存在任何继承),就是一个普通函数,所以直接继承Function.prototype。但是,A调用后返回一个空对象(即Object实例),所以A.prototype.proto指向构造函数(Object)的prototype属性。

第三种特殊情况,子类继承null。

1
2
3
4
5
class A extends null {
}

A.__proto__ === Function.prototype // true
A.prototype.__proto__ === undefined // true

这种情况与第二种情况非常像。A也是一个普通函数,所以直接继承Function.prototype。但是,A调用后返回的对象不继承任何方法,所以它的proto指向Function.prototype,即实质上执行了下面的代码。

1
2
3
class C extends null {
constructor() { return Object.create(null); }
}

第四种情况,允许继承原生构造函数定义子类,但无法通过super方法向父类Object传参

1
2
3
4
5
6
7
class NewObj extends Object{
constructor(){
super(...arguments);
}
}
var o = new NewObj({attr: true});
console.log(o.attr === true) // false

Mixin 模式

使用如下mix函数将多个对象合成为一个类

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
function mix(...mixins) {
class Mix {}

for (let mixin of mixins) {
copyProperties(Mix, mixin);
copyProperties(Mix.prototype, mixin.prototype);
}

return Mix;
}

function copyProperties(target, source) {
for (let key of Reflect.ownKeys(source)) {
if ( key !== "constructor"
&& key !== "prototype"
&& key !== "name"
) {
let desc = Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, desc);
}
}
}

//使用:继承返回的合成类
class DistributedEdit extends mix(Loggable, Serializable) {
// ...
}

几种创建方式对比

let a = null ===> null
let b = Object.create(null) ===> {} 但没有任何属性
let c = {} ===> {} proto指向Object
let d = new Object() ===> {} proto指向Object
let e = new Object(null) ===> {} proto指向Object
let f = Object.create(c) ===> {} proto指向c, proto.proto指向Object
!(obj4)[/image/ob4.png]
c instanceof Object , d instanceof Object ,e instanceof Object ===> true

let dd = {a:1}
let cc = new Object(dd)
let ee = Object.create(dd)
console.log(cc,ee)
!(obj3)[/image/ob3.png]
dd.isPrototypeOf(cc) ===>false
dd.isPrototypeOf(ee) ===>true

1…141516…25
YooHannah

YooHannah

246 日志
1 分类
21 标签
RSS
© 2025 YooHannah
由 Hexo 强力驱动
主题 - NexT.Pisces