0%

记录一下从 Sass 官网学习到的 Sass 的一些高级应用。

@mixin

@content 是一个特殊的占位符,可以把 @includes 的 body 中的内容传递进入

语法

特殊函数

CSS 定义了许多函数,一些在 sass 中工作的很好,他们被解析为函数调用,然后转换为普通传递 css 函数。但是有一些例外,他们被解析为 Sass 脚本表达式。所有特殊的函数调用返回 unquoted 字符串

url

他可以接受 quoted 或者 unquoted 的 URL,当他接受一个有效的 unquoted URL 的时候,Sass 按照原样解析它,如果它包含变量或函数调用,它被解析为一个普通的 CSS 函数调用。

at-rules

use 是 scss 模块化语法,未来将代替 import,导入的文件必须以_开头

forward 可以传递模块,但是在本文件中无法使用,要使用还需要额外使用 use 加载,不用担心的是模块只会被加载一次

属性嵌套

有些 CSS 属性遵循相同的命名空间 (namespace),比如 font-family, font-size, font-weight 都以 font 作为属性的命名空间。为了便于管理这样的属性,同时也为了避免了重复输入,Sass 允许将属性嵌套在命名空间中,例如:

1
2
3
4
5
6
7
.funky {
font: {
family: fantasy;
size: 30em;
weight: bold;
}
}

变量

变量支持块级作用域,嵌套规则内定义的变量只能在嵌套规则内使用(局部变量),不在嵌套规则内定义的变量则可在任何地方使用(全局变量)。将局部变量转换为全局变量可以添加 !global 声明

@extend

用于扩展继承样式

可以继承一个 html 元素的样式,你对于该元素添加的样式都会继承

变量默认值

使用 !default 用于变量

内置函数

普通 CSS 函数

任何不再内置和用户定义函数列表中的函数会被编译为 css 函数,除非它使用了 scss 的参数语法

数值函数

abs, ceil, floor,max,min,percentage,round 顾名思义

comparable($number1,$number2) //=> boolean 返回数值单位是否具有可比较性

1
2
3
@debug comparable(2px, 1px); // true
@debug comparable(100px, 3em); // false
@debug comparable(10cm, 3mm); // true

random($number) 生成随机数,如果 limit 是 null,返回一个 0-1 之间的随机数

unitless($number) //=> boolean 判断 $number 是否具有单位

字符串函数

quote($string) //=> string 加上双引号, unquote 去掉双引号

str-index,str-insert,str-length,str-slice,to-upper-case,to-lower-case 顾名思义

unique-id() //=> string 生成随机字符串

颜色函数

1
2
3
4
5
6
7
8
9
10
11
12
13
// color 综合调整
adjust-color($color,$red: null, $green: null, $blue: null,$hue: null, $saturation: null, $lightness: null,$alpha: null)
// color 色调调整
adjust-hue($color, $degrees)
alpha($color)
opacity($color)

// blue,red,green,hue,saturation,lightness,aplha 用法类似

//color 调整颜色参数的某一项数值
change-color($color,$red: null, $green: null, $blue: null,$hue: null, $saturation: null, $lightness: null,$alpha: null)
// 返回补色
complement($color)

太多了就不罗列了 详见官方文档 sass 颜色函数

列表函数

1
2
3
4
5
append,index,length,nth,set-nth 这些函数顾名思义
// 合并两个列表
join($list1, $list2, $separator: auto, $bracketed: auto)
// unquoted string 返回列表的分隔符
list-separator($list)

映射函数

选择器函数

实用性不大,略过

跟着掘金小册学习如何在工作中使用 git

准备工作

多人合作基本工作模型 2.0

写完所有的 commit 后,不用考虑中央仓库是否有新的提交,直接 push 就好

如果 push 失败,就用 pull 把本地仓库的提交和中央仓库的提交进行合并,然后再 push 一次

head,master

HEAD 指向的 branch 不能删除,如果要删除,需要先用 checkout 把 HEAD 指向其他地方

没有被合并到 master 过的 branch 在删除时会失败 如果需要删除 使用 -D

push

push 是把当前的分支上传到远程仓库,并把这个 branch 的路径上的所有 commits 也一并上传。

push 的时候,如果当前分支是一个本地创建的分支,需要指定远程仓库名和分支名,用 git push origin branch_name 的格式,而不能只用 git push;或者可以通过 git config 修改 push.default 来改变 push 时的行为逻辑。

push 的时候之后上传当前分支,并不会上传 HEAD;远程仓库的 HEAD 是永远指向默认分支(即 master)的。

merge

从目标 commit 和当前 commit (即 HEAD 所指向的 commit)分叉的位置起,把目标 commit 的路径上的所有 commit 的内容一并应用到当前 commit,然后自动生成一个新的 commit。

fast-forward

当你试图合并两个分支时,如果顺着一个分支走下去能够到达另一个分支,那么 Git 在合并两者的时候,只会简单的将指针向前推进(指针右移),因为这种情况下的合并操作没有需要解决的分歧——这就叫做 “快进(fast-forward)

待填坑中

cron

安装

cron 在类 Unix 系统中是一种基于时间的的任务调度器(job scheduler),其有多种实现,在 archlinux 中,可以使用 pacman -S cronie 安装该软件。

配置

开启启动 systemctl enable cronie
编辑,推荐使用命令,crontab -e,默认的编辑器是 vim,可以使用 EXPORT EDITOR=XXX 覆盖设置

sass 有多个版本,目前官方主推的是 dart-sass,因为 dart 在处理大型样式表的时候性能更佳。dart-sass 没有支持完整的 sass 语法功能。今天在使用 sass 的颜色运算的时候,发现 webpack 报错,一时摸不着头脑。搜索报错信息,也得不到什么相关信息。后来在 dart-sass 的 github 主页才发现原来 dart-sass 并不支持颜色的通道运算,以下是 dart-sass 与 ruby-sass 的主要不同点。

  • @extend 仅仅接收一个选择器,并且将其作为 selector-extend() 的第二个参数
1
2
3
4
5
6
7
8
9
10
11
12
13
.a {
x: y;
}
.b {
x: y;
}
.a.b {
x: y;
}

.c {
@extend .a.b;
}
  • 增加了对于 :has() 伪类的支持
  • 缩进语法 (sass) 更灵活,不再要求全部文档保持同样的缩进
  • 颜色不支持通道运算
  • 无单位的数字不再 == 有单位的数字,map 的 键值比对遵循同样的逻辑
  • rgba() and hsla() 的百分比单位只支持百分比的形式
1
2
3
4
.card{ color: rgba(255,255,255,0.5); }
should fail, but
.card{ color: rgba(255,255,255,50%); }
should pass
  • 给函数传递多于函数定义的参数数量会导致错误
  • 支持 :maches()
  • selector-unify() 的结果现在具有对称性
  • 只支持 UTF-8 格式的文档

打包文件资源问题

filename 指列在 entry 中,打包后输出的文件的名称。
chunkFilename 指未列在 entry 中,却又需要被打包出来的文件的名称。

项目缓存问题

当项目上线新版本的时候,用户浏览器可能仍然使用的是之前 html 和 js 文件的缓存。通过以下几个方法可以解决这个问题。

  • 文件命名使用哈希值,这样当发布新版本就会请求新版本的资源文件。vue-cli 中默认有这个功能,webpack 在每次构建的时候都会生成 hash,contenthash,chunkhash 三个哈希值
    • hash 这是工程级别的,即每次修改任何一个文件,所有文件名的 hash 至都将改变。所以一旦修改了任何一个文件,整个项目的文件缓存都将失效。
    • chunkhash 根据不同的入口文件(Entry)进行依赖文件解析、构建对应的 chunk,生成对应的哈希值。在生产环境里把一些公共库和程序入口文件区分开,单独打包构建,接着我们采用 chunkhash 的方式生成哈希值,那么只要我们不改动公共库的代码,就可以保证其哈希值不会受影响。并且 webpack4 中支持了异步 import 功能,chunkhash 也作用于此。
    • contenthash 一般来说,样式是作为模块 import 到 js 文件中,他们的 chunkhash 是一致的,这导致只要对应的 css 或者 js 改变,chunkhash 就会改变。而 contenthash 是针对文件内容级别的,只有自身模块的内容变了,hash 值才改变
  • html 加上
1
2
3
4
<meta
http-equiv="cache-control"
content="no-cache, no-store, must-revalidate"
/>
  • 配置 nginx,禁止 html 入口文件的缓存
1
2
3
4
5
6
location = /index.html {
add_header Cache-Control "no-cache, no-store";
}
location = /admin_index.html {
add_header Cache-Control "no-cache, no-store";
}
  • 使用 HtmlWebpackPlugin 插件,一个典型的配置如下
1
2
3
4
5
6
7
8
9
10
11
12
new HtmlWebpackPlugin({
filename: 'admin_index.html',
template: 'web-admin/admin_index.html',
inject: true,
hash: false,
chunks: ['adminApp', 'vendor~adminApp', 'vendor~adminApp~userApp'], // 这个数组是html要引入的打包生成的chunk文件,chunkname 在 webpack 打包输出信息中
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
}
}),

chunkname
图中最右边一列就是资源的 chunkname

MiniCssExtractPlugin

之前项目使用的是 ExtractTextPlugin 对 css 进行抽出并打包,然而 ExtractTextPlugin 在 4.0 版本后不再支持对 css 进行打包 (会在生成的 css 文件末尾附加 webpack 运行时的代码),后学习得知现在应使用 MiniCssExtractPlugin 对 css 进行抽离

CleanWebpackPlugin

这个插件的作用是每次构建的时候,清理重复构建的文件,因为使用哈希方式构建的时候,文件名不同,更改的文件不会直接替换。个人感觉使用 rm -rf dist/* 清理也可以 当然这可以避免你神志不清时误把整个项目文件删了

1
new CleanWebpackPlugin(["dist"]);

optimization.splitChunks

非常实用的功能,抽出代码中的公共组件到独立的文件,举个例子

1
2
3
4
5
6
7
8
9
cacheGroups: {
vendor: {
test: /node_modules/,
chunks: "initial",
// name:"vendor",
priority: 10,
enforce: true
}
}

如果没有 name 属性,a 入口文件引用了 vue 和 axios, b 入口文件引用了 react 和 axios,那么 webpack 就会把 axios 单独抽离出来为独立的 chunk,同时 vue 和 react 在各自的独立 chunk 中
而如果有 name 属性,那么所有依赖文件都会打包到命名的 chunk 文件中,这个原理同样适用于 CSS 的打包。

1
2
3
4
5
optimization: {
splitChunks: {
chunks: "all",
}
}

可以将 node_modules 中代码单独打包一个 chunk 最终输出

通过 js 代码让文件打包成单独 chunk

1
2
3
4
5
import(/*webpackChunkName:'test',webpackPrefetc:true*/,"./test")
.then(module => {
// 返回 es6 module
})
.catch(() => {});

懒加载 当文件需要时才加载
预加载 prefetch 会在使用之前,提前加载 js 文件,等其他资源加载完毕了,再偷偷加载
正常加载认为是并行加载

OptimizeCssAssetsPlugin

我在打包时发现 node_modules 中的 css 文件无法被压缩。css loader 中的 minimize 选项早已被废弃,尝试使用了这个插件后解决了问题。

devserver

  • proxy
    代理的只能是本地 如果本身请求的目的地址已经指定不是本地,那么这个请求不会被 webpack 代理
  • HMR
    • 样式文件 可以使用 因为 style-loader 内部实现了 HMR
    • js 文件 默认不能使用 HMR 功能
    • html 文件不能热更新 将 html 文件 加入 entry 这样会重新加载页面 不需要作 HMR 功能

sourcemap

类型 描述
source-map 外部 错误代码的准确信息访问到源代码的准确位置
inline-source-map 内联 只生成一个 sourcemap
hidden-source-map 外联 错误代码错误原因,但是没有错误位置,不能追踪源代码错误,只能提示到构建后代码的错误位置
eval-source-map 内联 每一个文件都生成对应的 source-map,都在 eval 错误代码准确信息,和源代码的错误位置
nosources-source-map 外部 错误代码准确信息,但是没有任何源代码信息
cheap-source-map 外部 错误代码准确信息和源代码的错误位置 只能精确到行
cheap-module-source-map 外部 错误代码准确信息 和 源代码错误位置

module 会将 loader 的 source map 加入

开发环境:速度快,调试更友好
速度快(eval>inline>cheap)
eval-cheap-source-map
eval-source-map
调试更友好
source-map
cheap-moudule-source-map
cheap-source-map

eval-cheap-source-map

  • 内联和外部的区别 1. 外部生成了文件,内联没有 2. 内联构建速度更快

开发环境:速度快,调试更友好
eval>inline>cheap
source-map
cheap-module-source-map
cheap-source-map
eval-source-map / eval-cheap-moudule-source-map

生产环境:源代码要不要隐藏?调试友好程度
内联会让代码变大,所以再生产环境不用内联
nosources-source-map 全部隐藏
hidden-source-map 只隐藏代码,会提示构建代码错误信息

babel-loader

babel 缓存 cacheDirectory: true

tree shakin 去除无用代码

  • 必须使用 ES6 模块化

  • 开启 production 环境

  • 在 package.json 中配置 “sideEffects”:false 所有的代码都没有副作用,都可以进行 tree shaking 可能会把 css/ @bable/polyfill 文件干掉

  • 定义 nodejs 环境变量:决定使用 browserslist 的哪个环境 process.env.NODE_ENV = ‘production’

PWA

workbox->workbox-webpack-plugin

1
2
3
4
5
new WorkboxWebpackPlugin.GenerateSW {
// 生成 serverworker.js
clientClaim: true,
skipWaiting:true
}

eslint 不认识 window,navigator 全局变量,需要修改

1
2
3
env: {
browser: true;
}

多进程打包 thread-loader

开启多进程打包,进程启动大概 600ms,进程通信也有开销
多用在 babel-loader

1
2
3
4
5
6
{
loader:'thread-loader',
options:{
workers:2
}
}

externals 忽略打包

1
2
3
4
externals: {
// 忽略库名 -- npm 包名
jQuery: "jQuery";
}

dll

打包 dll

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const { resolve } = require("path");
module.exports = {
entry: {
jquery: ["jquery"],
},
output: {
filename: "[name].js",
path: resolve(__dirname, "dll"),
library: "[name]_[hash]",
},
plugins: [
// 打包生成一个 manifest.json --> 提供和jquery映射
new webpack.DLLPlugin({
name: "[name]_[hash]",
path: resolve(__dirname, "dll/manifest.json"),
}),
],
};

定义 dll

1
2
3
new webpack.DLLReferencePlugin({
manifest: resolve(__dirname, "dll/manifest.json"),
});

引用 dll

1
2
3
new AddAssetHtmlWebpackPlugin({
filepath: resolve(__dirname, "dll/jquery.js"),
});

配置详解

  • module

    • enforce:’pre’, // 优先执行
    • enforce:’post’, // 延后执行
  • resolve 解析模块的规则

    • alias 配置解析模块的别名
    • extensions:[‘.js’,’json’] 配置省略文件路径的后缀名
    • modules:[resolve(__dirname,’../../node_modules’),’node_modules’] 解析模块去找哪个目录
  • optimization

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
optimization: {
// 将当前模块记录的其他模块的hash单独打包为一个文件 runtime 一定要加上 保证未改动模块缓存不失效
runtimeChunk: {
name: entrypoint => `runtime-${entrypoint.name}`;
},
minimize:{
// 配置生产环境的压缩方案: js和css
new TerserWebpackPlugin({
// 开启缓存
cache:true,
// 开启多进程打包
parallel:true,
// 启动source-map
sourceMap:true
})
}
}

核心

树状数组是一种用来解决区间单点更新,区间求和问题的简单数据结构。其核心思想在于二进制特殊性质。

1
2
3
4
int lowbit(int x)
{
return x & (-x);
}

lowbit

lowbit 函数的作用是取得 x 最右边一个 1 对应的数值。 如果 x 的二进制可以表示为 00010010,那么 lowbit 的结果为 2。怎么做到的呢?-x 的补码是 x 按位取反加 1,0010 变成 1101 +1 = 1110 ,-x 从右数第一个 1 的位置对应 x 从右数第一个 x 的位置,其他均取反。
如此便可以取得所要得到的数。
得到了这个数又有什么用呢。可以看下表

普通数组 a 树状数组 c 解释 二进制
1 1 a[1] 0001
2 3 a[1]+a[2] 0010
3 3 a[3] 0011
4 10 a[1]+a[2]+a[3]+a[4] 0100
5 5 a[5] 0101
6 11 a[5]+a[6] 0110
7 7 a[7] 0111
8 36 a[1]+…+a[8] 1000
9 9 a[9] 1001

对于一个位置,看它的二进制,这个位置保存的元素个数就是其二进制最右边 1 代表的大小,保存从去掉最右边 1 到加上最右边 1 的位置之间的数据(左开右闭)。
这样表述很不清楚,直接距离,看 c[6]位置,6 的二进制是 0110,去掉最右边 1 是 0100,即十进制的 4,它保存的是(4,6]之间的数据。

那么对于求和操作,就变成了不断找右边的 1 的过程。

对于更新操作,我们要同时更新其之后的父节点。
当我们修改 a[i]的值时,可以从 c[i]往根节点一路上溯,调整这条路上的所有 C[]即可,对于节点 i,父节点下标 p=i+lowbit(i)

树状数组多用于求逆序对,原理是将序列按顺序插入树状数组时,每次统计比当前数小的元素数量 sum,则与当前数构成逆序的对数有 i-sum
可以离散化数据,我们不关心具体元素的数值,只关心其对应关系。

代码实现

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
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const double PI = acos(-1.0);
const double eps = 1e-6;
const int INF = 0x3f3f3f3f;
const int maxn = 1e5 + 7;
int n;
int c[maxn];
int lowbit(int x)
{
return x & (-x);
}
void update(int i, int v)
{
while (i < n)
{
c[i] += v;
i += lowbit(i);
}
}
int getsum(int i)
{
int sum = 0;
while (i > 0)
{
sum += c[i];
i -= lowbit(i);
}
}

int main()
{
freopen("data.in", "r", stdin);
n = 10;
for (int i = 1; i <= n; i++)
{
int tmp;
cin >> tmp;
update(i, tmp);
}
for (int i = 1; i <= 10; i++)
{
cout << c[i] << endl;

}
}

扩展到二维

问题:一个由数字构成的大矩阵,能进行两种操作

  • 对矩阵里的某个数加上一个整数(可正可负)
  • 查询某个子矩阵里所有数字的和,要求对每次查询,输出结果。

一维树状数组很容易扩展到二维,在二维情况下:数组 A[][]的树状数组定义为:

其相应的更新与求和操作也很容易类比出来,这里就不做说明了。

开发

浏览器兼容性

如果有依赖需要 polyfill,你有几种选择:

  • 如果该依赖基于一个目标环境不支持的 ES 版本撰写: 将其添加到 vue.config.js 中的 transpileDependencies 选项。这会为该依赖同时开启语法语法转换和根据使用情况检测 polyfill。
  • 如果该依赖交付了 ES5 代码并显式地列出了需要的 polyfill: 你可以使用 @vue/babel-preset-app 的 polyfills 选项预包含所需要的 polyfill。注意 es6.promise 将被默认包含,因为现在的库依赖 Promise 是非常普遍的。
  • 如果该依赖交付 ES5 代码,但使用了 ES6+ 特性且没有显式地列出需要的 polyfill (例如 Vuetify):请使用 useBuiltIns: ‘entry’ 然后在入口文件添加 import ‘@babel/polyfill’。这会根据 browserslist 目标导入所有 polyfill,这样你就不用再担心依赖的 polyfill 问题了,但是因为包含了一些没有用到的 polyfill 所以最终的包大小可能会增加。

环境变量和模式

你可以替换你的项目根目录中的下列文件来指定环境变量:
.env # 在所有的环境中被载入
.env.local # 在所有的环境中被载入,但会被 git 忽略
.env.[mode] # 只在指定的模式中被载入
.env.[mode].local # 只在指定的模式中被载入,但会被 git 忽略

模式是 Vue CLI 项目中一个重要的概念,默认情况下,一个 Vue CLI 项目有三个模式:

  • development 模式用于 vue-cli-service serve
  • production 模式用于 vue-cli-service build 和 vue-cli-service test:e2e
  • test 模式用于 vue-cli-service test:unit
    可以通过传递 —mode 选项参数为命令行覆写默认的模式
  • “dev-build”: “vue-cli-service build —mode development”

只有以 VUE_APP 开头的变量会被 webpack.DefinePlugin 静态嵌入到客户端侧的包中。你可以在应用的代码中这样访问它们:

  • console.log(process.env.VUEAPP_SECRET)
    除了 VUE_APP
    * 变量之外,在你的应用代码中始终可用的还有两个特殊的变量:
  • NODE_ENV - 会是 “development”、”production” 或 “test” 中的一个。具体的值取决于应用运行的模式。
  • BASE_URL - 会和 vue.config.js 中的 publicPath 选项相符,即你的应用会部署到的基础路径。
    所有解析出来的环境变量都可以在 public/index.html 中以 HTML 插值中介绍的方式使用。

静态资源处理

URL 转换规则

如果 URL 是一个绝对路径 (例如 /images/foo.png),它将会被保留不变。
如果 URL 以 . 开头,它会作为一个相对模块请求被解释且基于你的文件系统中的目录结构进行解析。
如果 URL 以 ~ 开头,其后的任何内容都会作为一个模块请求被解析。这意味着你甚至可以引用 Node 模块中的资源:

1
<img src="~some-npm-package/foo.png" />

如果 URL 以 @ 开头,它也会作为一个模块请求被解析。它的用处在于 Vue CLI 默认会设置一个指向 projectRoot/src 的别名 @。(仅作用于模版中)

Debug 配置

效果:在 vscode 中的终端输出 Chrome 的控制台信息,可以在 vscode 中进行断点调试
首先需要安装插件 Debugger for Chrome
新增 Debug 配置项

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"name": "attach to chrome",
"type": "chrome",
"request": "attach",
"port": 9222,
"address": "localhost",
"url": "http://localhost:8080",
"webRoot": "${workspaceFolder}/src", // 这个webRoot应该是指webpack打包js文件的目录
"sourceMaps": true,
"sourceMapPathOverrides": {
"webpack:///src/*": "${webRoot}/*"
}
}

启动 chrome,附加启动参数

1
google-chrome-stable  --remote-debugging-port=9222

如果使用的是 vue-cli,需要在 vue.config.js 中添加相应配置,如

1
2
3
4
5
6
module.exports = {
configureWebpack: {
devtool: 'source-map'
}
...
}

其实我向来不是一个学习努力的人,高中时就不喜欢记笔记,高中的时候课堂管理严格,多多少少还是能记录下一些东西。
上了大学以后,我更是很少去做课堂笔记了,主要原因是我根本不听 O(∩_∩)O~。不过上了大学以后,我能明显感受到记忆力的衰退。记笔记确实是一种有效的学习方法,在学习技术的时候,我也是有在记录笔记的,一开始是记在本子上的,后来觉得这样很麻烦,开始使用有道云,最后开了这个博客,倒不是希望能被人看到,只是 hexo 用 VSCode 打开真的很好管理和编辑,然后哪天高兴了,随手 push 上去就好。
记录笔记其实是一件节省时间的事情,一般我只会记录我在学习的时候遇到的重难点的核心知识,自己已经理解的就不会记录了,之后遇到问题,只要搜索自己的笔记,省去了翻阅原书/文档的时间。

Class

  • 不存在变量提升
  • 构造函数的 prototype 属性,在 ES6 的“类”上面继续存在。事实上,类的所有方法都定义在类的 prototype 属性上面。
  • prototype 对象的 constructor 属性,直接指向“类”的本身,constructor()方法默认返回实例对象(即 this),完全可以指定返回另外一个对象。

类的实例

  • 与 ES5 一样,实例的属性除非显式定义在其本身(即定义在 this 对象上),否则都是定义在原型上(即定义在 class 上)。与 ES5 一样,类的所有实例共享一个原型对象。

Class 表达式

与函数一样,类也可以使用表达式的形式定义。

1
2
3
4
5
const MyClass = class Me {
getClassName() {
return Me.name;
}
};

上面代码表示,Me 只在 Class 内部有定义。
采用 Class 表达式,可以写出立即执行的 Class。

1
2
3
4
5
6
7
8
9
10
11
let person = new (class {
constructor(name) {
this.name = name;
}

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

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

上面代码中,person 是一个立即执行的类的实例。

静态方法

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

实例属性的新写法

  • 实例属性除了定义在 constructor()方法里面的 this 上面,也可以定义在类的最顶层,这时不需要在之前加 this。

静态属性

  • 静态属性指的是 Class 本身的属性,即 Class.propName,而不是定义在实例对象(this)上的属性。
1
2
3
4
class Foo {}

Foo.prop = 1;
Foo.prop; // 1

new.target 属性

new 是从构造函数生成实例对象的命令。ES6 为 new 命令引入了一个 new.target 属性,该属性一般用在构造函数之中,返回 new 命令作用于的那个构造函数。如果构造函数不是通过 new 命令或 Reflect.construct()调用的,new.target 会返回 undefined,因此这个属性可以用来确定构造函数是怎么调用的。

1
2
3
4
5
6
7
8
9
class Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle);
this.length = length;
this.width = width;
}
}

var obj = new Rectangle(3, 4); // 输出 true

子类继承父类时,new.target 会返回子类。

Object.getPrototypeOf()

用于获取父类

super

有两种用法 当做函数代表父类的构造函数,当做对象,在普通方法中,指向父类的原型对象,在静态方法中指向父类
需要注意的是,由于 super 指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过 super 调用的。在子类普通方法中通过 super 调用父类的方法时,方法内部的 this 指向当前的子类实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class A {
constructor() {
this.x = 1;
}
print() {
console.log(this.x);
}
}

class B extends A {
constructor() {
super();
this.x = 2;
}
m() {
super.print();
}
}

let b = new B();
b.m(); // 2

继承链

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

第一部分 作用域和闭包

第一章 作用域是什么

理解作用域

变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值。

如果 RHS 查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出 ReferenceError 异常。值得注意的是, ReferenceError 是非常重要的异常类型。

相较之下,当引擎执行 LHS 查询时,如果在顶层(全局作用域)中也无法找到目标变量,
全局作用域中就会创建一个具有该名称的变量,并将其返还给引擎。

如果 RHS 查询找到了一个变量,但是你尝试对这个变量的值进行不合理的操作,比如试图对一个非函数类型的值进行函数调用,或着引用 null 或 undefined 类型的值中的属性,那么引擎会抛出另外一种类型的异常,叫作 TypeError 。

js 没有块级作用域,而其他语言 if while for 等块级结构一般都有独立的作用域,js 的函数的作用域是独立的

js 没有块作用域的例子

1
2
3
4
if (true) {
var c = 10;
}
console.log("c", c); // 输出10
作用域嵌套

作用域是根据名称查找变量的一套规则。当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此,在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域(也就是全局作用域)为止。

异常

如果 RHS 查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出 ReferenceError
异常。

ES5 中引入了“严格模式”。在严格模式中 LHS 查询失败时,并不会创建并返回一个全局变量,引擎会抛出同 RHS 查询失败时类似的 ReferenceError 异常。

如果 RHS 查询找到了一个变量,但是你尝试对这个变量的值进行不合理的操作,比如试图对一个非函数类型的值进行函数调用,或着引用 null 或 undefined 类型的值中的属性,那么引擎会抛出另外一种类型的异常,叫作 TypeError 。

第二章 词法作用域

词法阶段

大部分标准语言编译器的第一个工作阶段叫作词法化,词法作用域就是定义在词法阶段的作用域,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变。

可以大致认为每一个函数都会创建一个新的作用域。

欺骗词法

eval 可以欺骗词法作用域,对词法作用域进行修改,但是严格模式下,eval 有自己的作用域。

1
2
3
4
5
6
function foo(str, a) {
eval(str); // 欺骗!
console.log(a, b);
}
var b = 2;
foo("var b = 3;", 1); // 1, 3

with 的用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var obj = {
a: 1,
b: 2,
c: 3,
};
// 单调乏味的重复 "obj"
obj.a = 2;
obj.b = 3;
obj.c = 4;
// 简单的快捷方式
with (obj) {
a = 3;
b = 4;
c = 5;
}

尽管 with 块可以将一个对象处理为词法作用域,但是这个块内部正常的 var 声明并不会被限制在这个块的作用域中,而是被添加到 with 所处的函数作用域中。

第三章 函数作用域和块作用域

函数中的作用域

函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)。

IIFE 的另一个非常普遍的进阶用法是把它们当作函数调用并传递参数进去。

块作用域

try / catch 的 catch 分句会创建一个块作
用域,其中声明的变量仅在 catch 内部有效。

块作用域与内存垃圾收集机制也有相关。

1
2
3
4
5
6
7
8
9
function process(data) {
// 在这里做点有趣的事情
}
var someReallyBigData = { .. };
process( someReallyBigData );
var btn = document.getElementById( "my_button" );
btn.addEventListener( "click", function click(evt) {
console.log("button clicked");
}, /*capturingPhase=*/false );

click 函数的点击回调不需要 someReallyBigData ,但是由于 click 函数形成了一个覆盖整个作用域的闭包,js 引擎极有可能依然保存着这个结构。

使用块作用域来解决这个问题

1
2
3
4
5
6
7
8
9
10
11
12
function process(data) {
// 在这里做点有趣的事情
}
// 在这个块中定义的内容可以销毁了!
{
let someReallyBigData = { .. };
process( someReallyBigData );
}
var btn = document.getElementById( "my_button" );
btn.addEventListener( "click", function click(evt){
console.log("button clicked");
}, /*capturingPhase=*/false );

第四章 提升

var a = 1 实际 js 引擎会当作两部分处理

1
2
var a; // 这一部分在编译阶段进行
a = 1;

函数声明和变量声明都会被提升。函数会首先被提升,然后才是变量。

js 不存在函数重载,后声明的函数会覆盖掉前面。

函数表达式不会被提升。

第五章 作用域闭包

for 循环头部的 let 声明还会有一个特殊的行为。这个行为指出变量在循环过程中不止被声明一次,每次迭代都会声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。

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

输出 1 2 3 4 5

单例模式的实现,可以将函数转换为 IIFE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var foo = (function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log(something);
}
function doAnother() {
console.log(another.join(" ! "));
}
return {
doSomething: doSomething,
doAnother: doAnother,
};
})();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3

当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包

附录

附录 C

箭头函数的 this 指向的是当前的词法作用域

第二部分 this 和对象原型

第一章 关于 this

为什么要使用 this

this 提供了一种优雅的方式隐式传递一个对象引用,因此可以将 api 设计的更加简洁和易于复用。

误解

this 在任何情况下都不指向函数的词法作用域

this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用 调用位置就在当前正在执行的函数的前一个调用

在浏览器环境下,默认 this 指向 window 对象,在全局作用域下定义的变量和函数也会绑定到 window 下,用 this 可以访问,但是在 node 的环境下就截然不同

独立函数调用采用默认绑定

而对象的方法调用会将 this 隐式绑定到对象上下文

1
2
3
4
5
6
7
8
function foo() {
var a = 2;
this.bar();
}
function bar() {
console.log(this.a);
}
foo(); // ReferenceError: a is not defined

不能把 this 和词法作用域的查找混合使用时,这是无法实现的。

1
2
3
4
5
6
7
8
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo,
};
obj.foo(); // 2

显式绑定 apply 和 call 方法,call 接受参数列表,apply 接受一个参数数组。 Function.prototype.bind(..) 会返回一个硬编码的新函数,它会把参数设置为 this 的上下文并调用原始函数。

关于 var self = this 的写法的解释,在内部作用域无法取到外部作用域的 this, 所以要使用一个变量保存起来。

实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。

使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。

  1. 创建(或者说构造)一个全新的对象。
  2. 这个新对象会被执行 [[ 原型 ]] 连接。
  3. 这个新对象会绑定到函数调用的 this 。换句话说,函数内的 this 指向的是 这个新对象
  4. 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象
this 到底是什么

当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。 this 就是记录的其中一个属性,会在函数执行的过程中用到。

第二章 this 全面解析

下面这个例子中,this 运行时绑定的是 settimeout(该函数在 settimeout 中调用),使用箭头函数可以绑定为 foo 的词法作用域

1
2
3
4
5
6
7
8
function foo() {
setTimeout(function () {
console.log(this.a);
}, 100);
}
var obj = {
a: 2,
};

第五章 原型

属性设置和屏蔽
  • 如果 foo 不直接存在于 myObject 中而是存在于原型链上层时 myObject.foo = “bar” 赋值操作会出现的三种情况
    如果在 [[Prototype]] 链上层存在名为 foo 的普通数据访问属性(参见第 3 章)并且没
    有被标记为只读( writable:false ),那就会直接在 myObject 中添加一个名为 foo 的新
    属性,它是屏蔽属性。
    如果在 [[Prototype]] 链上层存在 foo ,但是它被标记为只读( writable:false ),那么
    无法修改已有属性或者在 myObject 上创建屏蔽属性。如果运行在严格模式下,代码会
    抛出一个错误。否则,这条赋值语句会被忽略。总之,不会发生屏蔽。 * 如果在 [[Prototype]] 链上层存在 foo 并且它是一个 setter(参见第 3 章),那就一定会
    调用这个 setter。 foo 不会被添加到(或者说屏蔽于) myObject ,也不会重新定义 foo 这
    个 setter。
  • 一些随意的对象属性引用,比如 a1.constructor ,实际上是不被信任的,它们不一
    定会指向默认的函数引用

part6 行为委托

待完成