es6引入css

假设我们有一个名为style.css的css文件

1
2
3
4
5
6
7
8
9
10
11
.default {
cursor:null;
}

.drag {
cursor:move;
}

.rotate {
cursor: url('../../assets/mouse/closedhand.cur') 8 8, default;
}

在其它js文件中如何引入并使用这个css文件呢,总不能一直只使用js来编写吧

假设我们在EventManager.js文件中要使用,有两种方式引入:

1
import './style/style.css';
1
require('./style/style.css');

然后在代码中就可以直接使用了,如:

1
2
3
4
...
this._dom.className='rotate';
...
this._dom.className='drag';

此时,css的效果就出来了。

drawArrays和drawElements

在使用drawArrays时,我们通常是

1
2
3
const bufferPosition = this.gl.createBuffer();
gl.bindBuffer(this.gl.ARRAY_BUFFER, bufferPosition);
gl.bufferData(this.gl.ARRAY_BUFFER,new Float32Array(positions),this.gl.STATIC_DRAW);

然后再绑定vao时,指定一下数据就可以了

1
2
gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(positionLocation, size, type, normalize, stride, offset);

这里说明一下:ARRAY_BUFFER绑定的数据属于全局状态。

而使用drawElements时,通常配合绑定ARRAY_BUFFER,还需要创建索引buffer

1
2
3
4
5
6
7
8
9
10
//创建索引
const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
const indices = this.earth.getFillIndices(64,64);
gl.bufferData(
gl.ELEMENT_ARRAY_BUFFER,
new Uint16Array(indices),
gl.STATIC_DRAW
);
const indexCount=indices.length;

这里的ELEMENT_ARRAY_BUFFER是当前定点数组的一部分

webpack的使用

一、工程初始化

1.1 入门

  • 创建目录catirl

  • 进入目录,执行初始化

  • 1
    2
    3
    cd catirl
    npm init -y
    npm install webpack webpack-cli --save-dev
  • 创建源文件夹,并添加文件index.js

  • 1
    2
    3
    4
    5
    catirl
    |- package.json
    + |- index.html
    + |- /src
    + |- index.js

注意:此时我们在package.json中保留"main": "index.js"即可,没有必要改为"private": true,保留的好处是,我们可以随时调试源代码,但是这样的不支持源代码中带有importexport语法 。

1.2 支持import语法

为了支持importexport语法 ,webpack可以做转,在package.json文件,修改如下:

1
2
+   "private": true,
- "main": "index.js",

然后创建一个dist的目录,将index.html移动进去,并执行命令

1
npx webpack

此时会将我们的脚本作为入口起点,然后 输出main.js,将main.js引入index.html就可以继续访问页面了。

通过npm进行打包,在package.json中补充

1
2
3
"scripts": {
"build": "webpack"
},

就可以使用npm命令进行构建了

1
npm run build

1.3 进一步定制webpack

以上使用的都是默认的webpack的配置,如何定制呢,比如修改源文件入口文件、输出文件路径、输出文件名称,这时候就得配置webpack.config.js文件了。

新建webpack.config.js文件:

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

module.exports = {
entry: './src/index.js', //入口文件
output: {
filename: 'bundle.js', //输出文件名称
path: path.resolve(__dirname, 'dist')//输出文件路径
}
};

还有很多更复杂的配置,以后再讲

1.4 可调试源码

此时碰到一个问题,打包出来的 main.jsbundle.js是压缩过的代码,很难调试,这时候怎么办呢?参考

此时就需要配置webpack.config.js了,在里面添加

1
2
3
4
5
6
7
module.exports = {
devtool: 'source-map',
entry:"",
output: {
}
....
}

再次执行npm run build就可以看到,生成的文件多了一个bundle.js.map文件,

有了它,就可以调试源码的页面了。

但是这样还不是很方便,在编写代码过程中,我们往往需要实时热更新加载。这该怎么办呢?

1.5 热更新加载

参考

首先需要一个本地web服务器,可以使用webpack-dev-server,安装该模块,并启动

1
2
npm install webpack-dev-server
npx webpack-dev-server

这样就启动了,打开浏览器输入http://localhost:8080/就可以访问当前目录了

当然,我们可以定制启动的路径,端口号等。

webpack.config.js中添加:

1
2
3
4
5
6
7
8
9
10
11
module.exports = {
devServer: {//开发服务器的配置
//端口号配置,默认为8080
port: 3000,
//进度条
progress: true,
//指定打开浏览器显示的目录,默认为根目录(项目目录)
contentBase: './dist'
},
...
}

这时候就可以通过输入http://localhost:3030/ 直接访问dist根目录了,

此时我们在源码中直接修改代码,就可以在页面中实时更新了。

疑问

这里我有个疑问,如果之前我没有构建bundle.jsbundle.js.map文件,该server还能继续使用吗,我在dist文件夹下删除其余的文件,启动server,照样可以看到内容,奇怪。

然后我将html文件中对bundle.js的引用给注销掉,再次启动server就不行了,说明server自动构建了临时bundle.js了。这个构建规则遵从之前的打包配置规则。验证了一下,确实如此。

webpack.config.js中将bundle.js名字改为main.js

1
2
3
4
5
output: {
// filename: 'bundle.js',
filename: 'main.js',
...
}

再次启动server,页面就不正常显示了。但是在浏览器中输入http://localhost:3030/main.js,是可以看到代码内容的,说明web服务器中生成了`main.js`文件。

结论:说明通过server启动访问的页面,优先访问web服务器内部生成的js文件。

如何让这个html文件中对js文件的引用也自动化起来呢,不至于这么摸不着头脑。

1.6 通过模版html自动配置js文件

使用html-webpack-plugin插件

首先安装该插件

1
npm install html-webpack-plugin

然后在webpack.config.js文件中配置

1
2
3
4
5
6
7
8
9
10
11
12
13
//引入
let HtmlWebpackPlugin = require('html-webpack-plugin');
...
module.exports = {
...
plugins: [ //数组:放着所有的webpack插件
// 配置
new HtmlWebpackPlugin({
template: './dist/template.html',
filename: './dist/index.html'
})
]
}

其中template是模版路径,template.html是模版内容,内容为

1
2
3
4
5
6
7
8
<!doctype html>
<html>
<head>
<title>Catirl</title>
</head>
<body>
</body>
</html>

filename属性的意思就是所生成HTML文件名,内容只是比template所指的HTML文件多引入你之前在output-filename中输出的js文件。

注意: 通过server生成的html和js文件都没有保存在本地。

至此就完成了热加载的配置工作。

坐标参考

最近在用webgl手撕一个三维GIS引擎,引擎内部使用4326的参考系,切片使用的3857的数据源,出现的效果就是经度正常,但是纬度方向被压缩了,如下图所示。

这是一个可以预见的情况,但是也引出了一些长期困扰自己的问题,趁这次机会深入理清一下。

一、Mercator投影

mercator投影是一种”等角正切圆柱投影”,以地球为例,假设地球被围在一中空的圆柱里,其基准纬线与圆柱相切(赤道)接触,然后再假想地球中心有一盏灯,把球面上的图形投影到圆柱体上,再把圆柱体展开,这就是一幅选定基准纬线上的“墨卡托投影”绘制出的地图,如下图:

当然,这是一个对于任何一个giser都知道的概念,在这里重点强调的一点是:墨卡托投影的对象不仅仅是正球,对椭球也一样通用

更准确的来说,椭球才是标准对象。我们的地球是一种更接近与梨形的球体,如下

每个地区高低不平,所以各个地区都会根据本区域特点,选取一个最适合本区域的椭球体作为参考了对象,往往一个地区的参考椭球体并不使用于另外一个区域。实际上,对于局部小区域,也可用会用圆锥切面投影,或是高斯-克吕格投影。

Mercator 投影坐标系统,全球范围尺度上其基准面可以是 WGS 1984

WGS 1984定义如下:

1
2
3
4
5
6
7
8
9
GCS_WGS_1984
WKID: 4326 Authority: EPSG
Angular Unit: Degree (0.0174532925199433)
Prime Meridian: Greenwich (0.0)
Datum: D_WGS_1984
Spheroid: WGS_1984
Semimajor Axis: 6378137.0
Semiminor Axis: 6356752.314245179
Inverse Flattening: 298.257223563

墨卡托投影的“圆柱”特性,保证了南北(纬线)和东西(经线)都是平行直线,并且相互垂直。而且经线间隔是相同的,纬线间隔从标准纬线(此处是赤道,也可能是其他纬线)向两级逐渐增大。

既然是投影了,肯定会涉及到投影的换算,它的投影计算公式应该是椭球的投影公司如下:

  • x、y是投影展开成平面后以赤道本初子午线交点为原点的平面坐标系的坐标
  • a 是椭球体长半轴,b是短半轴
  • L是经度(弧度制),B是纬度(弧度制)

二、WebMercator投影

Web Mercator 坐标系使用的投影方法不是严格意义的墨卡托投影,而是一个被 EPSG(European Petroleum Survey Group)称为伪墨卡托的投影方法,这个伪墨卡托投影方法的大名是 Popular Visualization Pseudo Mercator,PVPM。

因为这个坐标系统是 Google Map 最先使用的,或者更确切地说,是Google 最先发明的。在投影过程中,将表示地球的参考椭球体近似的作为正球体处理(正球体半径 R = 椭球体半长轴 a)。这也是为什么在 ArcGIS 中我们经常看到这个坐标系叫 WGS 1984 Web Mercator (Auxiliary Sphere)。Auxiliary Sphere 就是在告知你,这个坐标在投影过程中,将椭球体近似为正球体做投影变换,虽然基准面是WGS 1984 椭球面。

后来,Web Mercator 在 Web 地图领域被广泛使用,这个坐标系就名声大噪。尽管这个坐标系由于精度问题一度不被GIS专业人士接受,但最终 EPSG 还是给了 WKID:3857

三、切片和切片原点

我们知道通过墨卡托投影后,x轴是经度投影的结果,经度区间是[-π,π];y轴时纬度投影的结果,而纬度区间为[-π/2,π/2]。而我们的3857的切片服务,通常切片原点是[-2.0037508342787E7,2.0037508342787E7]。

这就不好理解了,x轴是以赤道周长来算的,x轴的范围是[-2.0037508342787E7,2.0037508342787E7]可以计算出来,可是为啥y轴的的范围也是[-2.0037508342787E7,2.0037508342787E7]呢?另一个与此相光的问题是为什么在切片的每个级别上分辨率是一个值呢?常规理解应该是x轴一个分辨率,y轴一个分辨率的。并且由于切片是正方形,x轴的分辨率应该是y轴分辨率的2倍才对呀。

其实虽然纬度区间[-π/2,π/2]只是经度区间[-π,π]的二分之一,但是在投影后同一纬度上的经度属于等间距投影,但是纬度却是随着纬度的增加投影距离不断增大,直到纬度为90度时,投影点趋于无穷大。纬度上的距离区间应该远远大于经度区间才对,如下图

此时有两点可以明确:

  • 同一纬度上,经度投影分布是均匀的,线性的
  • 纬度的投影,随着纬度的增加,投影距离是不断增大,是非线性的。

那么如何让纬度纬度线的区间分布也是均匀的呢?这场计算纬度线的投影距离公式是y=f(φ)=R*tan(φ);有一个聪明的家伙模拟了一个近似函数

1
2
3
		-ln(tan(π/4 + abs(φ/2)))     φ<0时
y=g(φ)= +ln(tan(π/4 + abs(φ/2))) φ=0时
+ln(tan(π/4 + abs(φ/2))) φ>0时

这种投影算法使得赤道附近的纬线较密,极地附近的纬线较稀。极点被投影到无穷远,所以这种投影不适合在高纬度地区使用。Google Maps的选取的范围为 -π<y<π ,这样近似的有 -85°<Φ<85°。这也就得到了y轴上的取值区间和x轴上的取值区间一致,一张方形的世界地图就出来了。

这时就注意了,在做x轴的切片计算时,可以采用二分的方式直接计算,但是对于y轴切片的计算则不能使用二分了,因为y轴不是线性切片的,必须严格按照该界别切片提供的分辨率进行计算。这么做就将问题分离开了,可视化人员只需要按照既定的规则进行切片展示;制图人员可以自定义经纬度两个方向切片的计算方法(只需最终输出满足既定规则的切片即可);

四、像素坐标

提到分辨率,就需要解释像素坐标了。

分辨率表示一个像素代表的实际长度,可以同过分辨率和像素坐标来计算该像素点所在的实际位置。

将地球表面通过墨卡托投影到一个方形平面时,依据展示内容的精细度,这个方形平面可大可小。通常全球范围内就不需要展示特别精细的内容,只需要轮廓就可以,而在一个工业园区级别就需要将具体的厂房道路展示出来。这样最终精细度的提高,我们这张世界地图的尺寸就越来越大。

不过好在,我们通常在精细化程度较高时,往往只需要看到很小的一块区域,为了切分的方便,一般切分方式都是以2的次幂来切分。这方面的内容此处不再赘述。只强调一点:既定的级别都有自己的分辨率,无论经度还是纬度都可以依据这个分辨率来准确的计算某一像素点的实际位置,而不必再考虑该图是如何将经度和纬度统一起来制图的

地图学中一个重要的概念,就是比例尺,即地图上的一厘米代表着实际上的多少厘米;到了web地图中我们把比例尺转换成另一个概念,分辨率(Resolution),即图上一像素代表实际多少米

假设地图的坐标单位是米,整张地图的dpi为96,当前地图在赤道处的比例尺为1:125000000(即图上1米等于实地125000000米),1英寸=2.54厘米; 1英寸=96像素。那么计算可得地图赤道上1像素代表实地距离是 125000000*0.0254/96 = 33072.9166666667米。为什么要强调某一条纬度线(上述例子为赤道)的比例尺?因为根据墨卡托投影的特性,同一张地图中不同纬度线的比例尺是变化的,越靠近两极,图上1米相当于实地的距离越小。

参考:

https://www.jianshu.com/p/778fc3e9f889

https://blog.csdn.net/mr_jianrong/article/details/72625811

https://blog.csdn.net/kikitamoon/article/details/46124935

https://www.maptiler.com/google-maps-coordinates-tile-bounds-projection/

https://blog.csdn.net/qq_35732147/article/details/83856513

https://baike.baidu.com/item/%E5%A2%A8%E5%8D%A1%E6%89%98%E6%8A%95%E5%BD%B1/5477927?fr=kg_qa)

https://www.maptiler.com/google-maps-coordinates-tile-bounds-projection/

WebGL绑定多纹理

我们的场景时实时渲染网络切片地图栅格数据。如果每次在渲染阶段进行数据的绑定,必定会降低帧率,什么卡顿。该如何优化呢

一、思考

对于非图片数据,我们的处理方式是:

  • 将各attribute数据绑定到vbo中;
  • 将各vbos绑定给指定的vao中;
  • 绘制时,切换vao即可。

但是这个方式对绘制大量图片并不适用,原因是webgl中可绑定的纹理数量一定。这就导致对于有限树数量的纹理单元(textureUnit)我们必须要重复使用。

如何破解这个问题呢?

虽然可绑定的纹理单元数量有限,但是我们可创建的纹理却不受限制。

这样我们就可以继续将各vbo绑定给vao,然后在绘制时,实时将已经创建好纹理绑定给指定的纹理单元即可。

二、方案

  • 计算要渲染切片的position、textureCoord、index等数据
  • 创建各个attribute的buffer
  • 创建一个空纹理对象texture,可以是纯色
  • 将各buffers绑定给vao,并且将空纹理texture与该vao关联起来
  • 异步请求img的实际数据,然后更新纹理的真实数据,并给texture标记需update
  • 绘制时,若texture的update为false,则直接绑定空纹理数据绘制;若update为true,则重新绑定纹理数据,进行绘制

三、其它

需要注意:

  • 请求切片可以放在子线程中执行;
  • 这个频繁切换的纹理,要使用可变纹理的方式

四、创建空纹理的代码

对于网络图片,首先需要先把图片数据下载下来:

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
// creates a texture info { width: w, height: h, texture: tex }
// The texture will start with 1x1 pixels and be updated
// when the image has loaded
function loadImageAndCreateTextureInfo(url) {
//创建纹理对像,默认设置填充为蓝色
var tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
// Fill the texture with a 1x1 blue pixel.
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE,
new Uint8Array([0, 0, 255, 255]));

gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

var textureInfo = {
width: 1, // we don't know the size until it loads
height: 1,
texture: tex,
};
var img = new Image();
img.addEventListener('load', function() {
textureInfo.width = img.width;
textureInfo.height = img.height;

gl.bindTexture(gl.TEXTURE_2D, textureInfo.texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
gl.generateMipmap(gl.TEXTURE_2D);
});
img.src = url;

return textureInfo;
}
  • 思路1

    先将顶点坐标、纹理坐标、纹理预处理好,可以直接绘制。并且绑定给vao。在绘制时,默认绘制vao,直到真实纹理加载完成,直接再次绑定新的纹理即可。

课外:https://webgl2fundamentals.org/webgl/lessons/webgl-texture-units.html

Hello again

前沿

作为程序员的悲哀,大家以我为鉴。

缘由

使用vim莫名的创建一个“~”的文件夹,就想着删除它,执行代码:

1
rm ~

发现居然删不了,果断换成:

1
rm -rf ~

然后就是输入权限密码,然后就悲剧了,事情的结果是我的整个根目录几乎被删除殆尽。

亡羊补牢

在mac中 “~”表示home目录,如果直接删除名为“~”的目录,相当于删除整个home目录下的所有文件(这也是为什么要管理员权限的原因),几乎无论这个“~”在什么位置都不要直接删除它。
如果非要删除,该怎么办呢?答案是使用绝对路径,比如我将“~”创建在了“/home/afei/Desktop/”下,那么执行下列命令即可:

1
rm -rf /home/afei/Desktop/~

WebGL跨域图片

一、问题

通常我们加载图片时使用这种方式:

1
2
3
4
5
6
7
const url='https://t6.tianditu.gov.cn/DataServer?T=img_w&x=2&y=1&l=3&tk=8971e4c7b3640d506c2dc111221af6a0';//天地图的一张切片
const image = new Image();
image.addEventListener('load', function() {
//拿到image,做纹理数据
...
});
image.src=url;

但是当图片为跨域图像时,就出现问题了。常规浏览器报错

1
Access to image at 'https://t6.tianditu.gov.cn/DataServer?T=img_w&x=2&y=1&l=3&tk=8971e4c7b3640d506c2dc111221af6a0' from origin 'http://localhost:3030' has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header has a value 'https://map.tianditu.gov.cn' that is not equal to the supplied origin.

接下来将浏览器设置为可跨域模式(方法自行百度),此时控制台是不报错了,但是也没有任何图像展示出来。

???这他娘的不是说好的img标签允许跨域的吗?

二、探索

通过添加错误的监听,可以看到image没有进入到load回调中,而是进入到了error的回调中。

1
2
3
image.addEventListener('error', ()=>{
console.log("error");
});

直接使用<img src="private.jpg">是没什么问题的,因为图像尽管被浏览器显示, 便签对象并不能获取图像的内部数据。而Webgl使用的就是image的内部数据,就获取不到了。

有什么方法可以获取到跨域图像内部的数据呢?

三、解决方式

对于访问跨域资源必须同时满足两方面的许可:

  • 1 跨域站点允许跨域访问(需要服务器设置)
  • 2 本地可以使用跨域的资源的内容(例如设置浏览器可跨域访问)

下面所讲的都只是解决2的问题。

3.1 设置crossOrigin

最简单的方式

1
2
3
4
5
6
7
8
const url='';
const image = new Image();
image.crossOrigin = "anonymous"; //允许
image.addEventListener('load', function() {
//拿到image,做纹理数据
...
});
image.src=url;

crossOrigin的值有三个可选择:

  • undefined:默认值,表示不需要请求许可;
  • anonymous:表示请求许可,但是不发送任何信息;
  • use-credentials:表示发送cookies和其它可能需要的信息,服务器会根据这些信息决定是否授予许可。

注意:设置为其他任意值则相当于 “anonymous”

设置完crossOrigin属性后,浏览器从服务器请求图像时,对于不同域名,会请求CROS许可。由于请求需求需要2个http请求,资源消耗多一些。所以同域的不需要设置请求许可,只需对跨域的资源的img标签或canvas2d设置crossDomain属性,这样就不会使请求变慢了。

可以添加这个一个适配函数:

1
2
3
4
5
function requestCORSIfNotSameOrigin(img, url) {
if ((new URL(url)).origin !== window.location.origin) {
img.crossOrigin = "";//相当于anonymous
}
}
3.2 使用canvas

第二种方法就是先将图片绘制在画布上,然后读取画布上的数据。

1
2
3
4
5
6
<canvas id="canvas"></canvas>
<div style="display:none;">
<img id="source"
src="https://mdn.mozillademos.org/files/5397/rhino.jpg"
width="300" height="227">
</div>
1
2
3
4
5
6
7
8
const canvas=document.getElementById('canvas');
const ctx=canvas.getContext('2d');
const image = document.getElementById('source');
image.addEventListener('load', e => {
ctx.drawImage(image, 33, 71, 104, 124, 21, 20, 87, 104);
//ctx.drawImage(someImg, 0, 0);
//const data = ctx.getImageData(0, 0, width, heigh);
});

在WebGL中 gl.readPixelsctx.getImageData 是相似的, 所以你可能以为把这个接口封闭就好了,但事实是即使不能直接获取像素值, 也可以创建一个基于图像颜色的着色器,虽然效率低但是可以等同于获取到了图像信息。

参考:

https://webgl2fundamentals.org/webgl/lessons/webgl-cors-permission.html

https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage

ES7新特性

一、ES7新特性

常用的更新有两个:

  • 求幂运算符
  • 数组的includes方法
1.1 求幂运算符(**)

基本使用方式,求5的2次幂

1
2
3
5**2  //25
//等价于
Math.pow(5,2);

复合使用的话依然遵循从右向左的优先级

1
2
3
2**4**2
//灯架
2**(4**2)
1.2 Array.prototype.includes()方法

includes()查找一个值在不在数组里,若在,则返回true,反之返回false

1
2
['a', 'b', 'c'].includes('a')     // true
['a', 'b', 'c'].includes('d') // false

include也可以接收两个参数,要搜索的值和起始索引。当第二个参数被传入时,该方法会从索引处开始往后搜索(默认索引值为0)。若搜索值在数组中存在则返回true,否则返回false

1
2
3
['a', 'b', 'c', 'd'].includes('b')         // true
['a', 'b', 'c', 'd'].includes('b', 1) // true
['a', 'b', 'c', 'd'].includes('b', 2) // false

Includes方法与indexof的区别在于:

  • includes更轻便,无需再判断返回值是否大于-1;

  • includes可以判断Nan,而indexof不能判断Nan

    1
    2
    3
    4
    5
    > let demo = [1, NaN, 2, 3]
    >
    > demo.indexOf(NaN) //-1
    > demo.includes(NaN) //true
    >
  • Include 对于+0和-0按相等处理的

    1
    2
    3
    > [1, +0, 3, 4].includes(-0)    //true
    > [1, +0, 3, 4].indexOf(-0) //1
    >

二、ES8新特性

2.1 async/await

传统的JavaScript中,对于异步的处理通常是通过回调函数处理的,但是一旦出现回调函数的嵌套,就很容易陷入回调地狱(callback hell)

1
2
3
4
5
this.$http.jsonp('/login', (res) => {
this.$http.jsonp('/getInfo', (info) => {
// ...
})
})

一种改进方式是使用promise,通过then方法将回调嵌套改为了链式的,尽管如此,当请求任务过多时一堆then也会造成语义的难以理解。

1
2
3
4
5
ar promise = new Promise((resolve, reject) => {
this.login(resolve)
})
.then(() => this.getInfo())
.catch(() => { console.log("Error") })

另一种异步机制时使用Generator函数,它通过* 、yield和next的来执行分段操作。Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}

var hw = helloWorldGenerator();

hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }

虽然Generator将异步操作表示得很简洁,但是流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)。此时,我们便希望能出现一种能自动执行Generator函数的方法。我们的主角来了:async/await。

ES8引入了async函数,使得异步操作变得更加方便。简单说来,它就是Generator函数的语法糖。

1
2
3
4
async function asyncFunc(params) {
const result1 = await this.login()
const result2 = await this.getInfo()
}

处理单个异步结果

1
2
3
4
async function asyncFunc() {
const result = await otherAsyncFunc();
console.log(result);
}

顺序处理多个异步结果

1
2
3
4
5
6
async function asyncFunc() {
const result1 = await otherAsyncFunc1();
console.log(result1);
const result2 = await otherAsyncFunc2();
console.log(result2);
}

并行处理多个异步结果

1
2
3
4
5
6
7
async function asyncFunc() {
const [result1, result2] = await Promise.all([
otherAsyncFunc1(),
otherAsyncFunc2()
]);
console.log(result1, result2);
}

处理错误

1
2
3
4
5
6
7
async function asyncFunc() {
try {
await otherAsyncFunc();
} catch (err) {
console.error(err);
}
}
2.2 Object.entries()和Object.values()
2.2.1Object.entries()

如果一个对象是具有键值对的数据结构,则每一个键值对都将会编译成一个具有两个元素的数组,这些数组最终会放到一个数组中,返回一个二维数组

1
2
Object.entries({ one: 1, two: 2 })    //[['one', 1], ['two', 2]]
Object.entries([1, 2]) //[['0', 1], ['1', 2]]

注意:键值对中,如果键的值是Symbol,编译时将会被忽略。

1
Object.entries({ [Symbol()]: 1, two: 2 })       //[['two', 2]]

Object.entries()返回的数组的顺序与for-in循环保持一致,即如果对象的key值是数字,则返回值会对key值进行排序,返回的是排序后的结果。

1
Object.entries({ 3: 'a', 4: 'b', 1: 'c' })    //[['1', 'c'], ['3', 'a'], ['4', 'b']]
2.2.2 Object.values()

它的工作原理跟Object.entries()很像,顾名思义,它只返回自己的键值对中属性的值。它返回的数组顺序,也跟Object.entries()保持一致。

1
2
Object.values({ one: 1, two: 2 })            //[1, 2]
Object.values({ 3: 'a', 4: 'b', 1: 'c' }) //['c', 'a', 'b']
2.3 padStart和padEnd

ES8提供了新的字符串方法-padStart和padEnd。padStart函数通过填充字符串的首部来保证字符串达到固定的长度,反之,padEnd是填充字符串的尾部来保证字符串的长度的。该方法提供了两个参数:字符串目标长度和填充字段,其中第二个参数可以不填,默认情况下使用空格填充。

1
2
3
'Vue'.padStart(10)           //'       Vue'
'React'.padStart(10) //' React'
'JavaScript'.padStart(10) //'JavaScript'

那么我们现在来看看第二个参数,我们可以指定字符串来代替空字符串。

1
2
3
4
'Vue'.padStart(10, '_*')           //'_*_*_*_Vue'
'React'.padStart(10, 'Hello') //'HelloReact'
'JavaScript'.padStart(10, 'Hi') //'JavaScript'
'JavaScript'.padStart(8, 'Hi') //'JavaScript'

padEnd函数作用同padStart,只不过它是从字符串尾部做填充

1
2
3
4
'Vue'.padEnd(10, '_*')           //'Vue_*_*_*_'
'React'.padEnd(10, 'Hello') //'ReactHello'
'JavaScript'.padEnd(10, 'Hi') //'JavaScript'
'JavaScript'.padEnd(8, 'Hi') //'JavaScript'
2.4 Object.getOwnPropertyDescriptors()

2.5 共享内存和原子(Shared memory and atomics)

内存管理碰撞课程:https://segmentfault.com/a/1190000009878588

图解 ArrayBuffers 和 SharedArrayBuffers:https://segmentfault.com/a/1190000009878632

用 Atomics 避免 SharedArrayBuffers 竞争条件:https://segmentfault.com/a/1190000009878699

参考:

作用域绑定

一、作用域绑定

先看下面的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
var name="afei";
var age="28";

var A={
name:"alan",
A_age:this.age,
getInfo:function(){
console.log(this.name+"的年龄是"+this.age);
}
};


A.getInfo();//alan的年龄是undefined

这就是作用域改变的例子,在getInfo中的this指向A,而A中没有age属性。

再看下一个例子

1
2
3
4
5
var name="afei";
function B(){
console.log(this.name);
}
B();//afei

这时的this指向的是全局变量window。

那么如何能改变this指向的作用域呢,传统的方式有bind、apply、call。应用如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var name="afei";
var age="28";

var Test={
name:"alan",
A_age:this.age,
getInfo:function(from,to){
console.log(this.name+"的年龄是"+this.age+"来自"+from+"去往"+to);
}
};

var A={
name:"A",
age:11,
}
var B={
name:"B",
age:22,
}

Test.getInfo.call(A,"河南","浙江");//A的年龄是11来自河南去往浙江
Test.getInfo.call(this,"河南","浙江");//afei的年龄是28来自河南去往浙江
Test.getInfo.apply(A,["河南","浙江"]);//B的年龄是22来自河南去往浙江
Test.getInfo.bind(A,"河南","浙江")();//A的年龄是11来自河南去往浙江

简单可知:

  • call、bind、apply接受第一个参数就是this指向的上下文
  • call的接收的原始参数不变,apply接收的原始参数以数组形式传递
  • bind处理返回的是函数外其余的与call一样
  • bind返回的是函数,还需要执行

还有一种不需要局部绑定this的方法,那就是使用箭头函数

二、箭头(arros)函数

2.1 语法更简洁

常规语法定义函数

1
2
3
function func(arg1){
return arg1*5;
}

使用箭头函数则只需要一行代码

1
var func=(args)=>args*2;
2.2 箭头函数语法

箭头函数的语法如下

1
(parameters)=>{statements}

如果没有参数则可以直接写成

1
()=>{statements}

如果只有一个参数

1
parameter=>{statements}

如果返回值仅仅是一个表达式(expression),可以省略大括号

1
2
3
4
5
parameter=>expression
//等价于
function (parameter){
return expression;
}
2.3 与this的关系

箭头函数不会绑定this,即箭头函数不会改变this的本来绑定

例如,想再函数内部实现递增效果,需要有this的局部绑定,不然的话setInterval调用中的this就会绑定给全局变量,从而不能得到正确的count。

1
2
3
4
5
6
7
8
9
10
11
function CounterTest(){
this.count=0;
let that=this;
this.timer=setInterval(function add(){
that.count++;
console.log(that.count);
},1000);
}

//执行new CounterTest(),就会一直执行累加计算
//如果不绑定this,会打印Nan

如果不要局部绑定,不用call、apply、bind等方式,还有什么方法呢,就是箭头函数

1
2
3
4
5
6
7
function CounterTest(){
this.count=0;
this.timer=setInterval(()=>{
this.count++;
console.log(this.count);
},1000);
}

可以看出,CounterTest构造函数绑定的this会被保留,在setInterval函数中的this依然是CounterTest的作用域。

参考:

闭包

闭包

一、什么是闭包

闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的常用方式就是在一个函数内部创建另一个函数。

首先来对比一下下面的代码,理解一下闭包

1
2
3
4
5
6
7
8
9
function outer(){
var temp=10;
function inner(){
console.log(a);
};
bar();
}

//上面的函数inner对temp的引用是词法作用域的查找规则,这些规则只是闭包的一部分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function outer(){
var temp=10;
function inner(arg1){
temp--;
if(arg1<temp){
return 0;
}else{
return 1;
}
}
return inner;
}

var test=outer();
test(8);//0
test(8);//1

//上面的函数inner的词法作用域能够访问outer内部的作用域,然后将inner()函数本身当作一个值类型进行传递。在outer()执行后,其返回值赋值给test并调用test(),实际上只是通过不同的标识符引用调用了内部的函数inner()

一个普通函数outer(),通常会期待在outer()执行后,整个内部作用域都被销毁,因为引擎右垃圾回收器来释放不再使用的内存空间。outer()的内容不会再被使用,所以很自然的会考虑将其回收。

而闭包的神奇之处就是可以阻止这件事情的发生,实际上outer内部作用域依然被引用着,因此没有被回收。是谁在 引用呢,就是inner本身。inner拥有涵盖outer内部作用域的闭包,使得该作用域一直存活,inner的这个引用就是闭包。

由于闭包会携带包含它的函数的作用域,因此会比其它函数占用更多的内存。过度使用闭包可能会导致内存占用过多,我们建议只在绝对必要的时刻载考虑使用闭包

二、闭包与变量

1
2
3
4
5
6
7
8
9
10
11
12
13
function createFunction(){
var result=new Array();
for(var i=0;i<10;i++){
result[i]=function(){
return i;
};
}
return result;
}

var test=createFunction();
test[0]();//10
test[1]();//10

三、函数的类型

创建函数的几种方式

  1. 声明函数

    1
    function fn(){}
  2. 匿名函数表达式

    1
    2
    var fn1=function(){}
    getFunctionName(fn1).length;//这种方式创建的函数为匿名函数,没有函数name

    注意:在对象内定义函数如var o={ fn : function (){…} },也属于函数表达式

  3. 具名函数

    1
    var fn2=function XXX(){}

    注意:具名函数表达式的函数名只能在创建函数内部使用

  4. Function构造函数

    1
    2
    //可以给 Function 构造函数传一个函数字符串,返回包含这个字符串命令的函数,此种方法创建的是匿名函数。
    Function("alert(1)");
  5. 自执行函数

    1
    2
    //让匿名函数自执行
    (function(){alert(1);})();
  6. 其它方法

    1
    eval\setTimeout\setInterval

四、注意

  1. 全局函数中的this等于window
  2. 当函数被某个对象的方法调用时,this等于哪个对象
  3. 匿名函数的执行环境具有全局性,this通常指向window