Koa2从零到脚手架

什么是 Koa2

由 Express 原班人马打造的新生代 Node.js Web 框架,它的代码很简单,没有像 Express 那样,提供路由、静态服务等等,它是为了解决 Node 问题(简化了 Node 中操作)并取代之,它本身是一个简单的中间件框架,需要配合各个中间件才能使用

文档

中文文档 (野生)

最简单的 Koa 服务器

const Koa = require("koa");

const app = new Koa();

app.use((ctx) => {
  ctx.body = "Hello World";
});

app.listen(3000, () => {
  console.log("3000端口已启动");
});

洋葱模型

洋葱模型

这是 Koa 的洋葱模型

看看 Express 的中间件是什么样的:

Express的中间件

请求(Request)直接依次贯穿各个中间件,最后通过请求处理函数返回响应(Response)。再来看看 Koa 的中间件是什么样的:

koa的中间件

可以看出,Koa 中间件不像 Express 中间件那样在请求通过了之后就完成自己的使命;相反,中间件的执行清晰地分为两个阶段。我们看看 Koa 中间件具体是什么样的

Koa 中间件的定义

Koa 的中间件是这样一个函数:

async function middleware(ctx, next) {
  // 先做什么
  await next();
  // 后做什么
}

第一个参数是 Koa Context,也就是上图中贯穿中间件和请求处理函数的绿色箭头所传递的内容,里面封装了请求体和响应体(实际上还有其他属性),分别可以通过 ctx.requestctx.response 来获取,以下是一些常用的属性:

ctx.url; // 相当于 ctx.request.url
ctx.body; // 相当于 ctx.response.boby
ctx.status; // 相当于 ctx.response.status

更多 Context 属性请参考 Context API 文档

中间件的第二个参数便是 next 函数:用来把控制权转交给下一个中间件。但它与 Express 的 next 函数本质的区别在于, Koa 的 next 函数返回的是一个 Promise ,在这个 Promise 进入完成状态(Fulfilled)后,就会去执行中间件中第二个阶段的代码。

有哪些常见的中间件

路由中间件——koa-router 或@koa/router

下载 npm 包

npm install koa-router --save

有些教程使用 @koa/router,现如今这两个库由同一个人维护,代码也一致。即 koa-router === @koa/router(写自 2021 年 8 月 23 日)

NPM 包地址:koa-router@koa/router

如何使用

在根目录下创建 controllers 目录,用来存放控制器有关的代码。首先是 HomeController,创建 controllers/home.js,代码如下:

class HomeController {
  static home(ctx) {
    ctx.body = "hello world";
  }
  static async login(ctx) {
    ctx.body = "Login Controller";
  }
  static async register(ctx) {
    ctx.body = "Register Controller";
  }
}

module.exports = HomeController;

实现路由

再创建 routes 文件夹,用于把控制器挂载到对应的路由上面,创建 home.js

const Router = require("koa-router");
const { home, login, register } = require("../controllers/home");

const router = new Router();

router.get("/", home);
router.post("/login", login);
router.post("/register", register);

module.exports = router;

注册路由

在 routes 中创建 index.js,以后所有的路由都放入 routes,我们创建 index.js 的目的是为了让结构更加整齐,index.js 负责所有路由的注册,它的兄弟文件负责各自的路由

const fs = require("fs");
module.exports = (app) => {
  fs.readdirSync(__dirname).forEach((file) => {
    if (file === "index.js") {
      return;
    }
    const route = require(`./${file}`);
    app.use(route.routes()).use(route.allowedMethods());
  });
};

注:allowedMethods 的作用

  1. 响应 option 方法,告诉它所支持的请求方法
  2. 相应地返回 405 (不允许)和 501 (没实现)

注:可以看到 @koa/router 的使用方式基本上与 Express Router 保持一致

引入路由

最后我们需要将 router 注册为中间件,新建 index.js,编写代码如下:

const Koa = require('koa')
const routing = require('./routes')

// 初始化 Koa 应用实例
consr app = new Koa()

// 注册中间件
// 相应用户请求
routing(app)

// 运行服务器
app.listen(3000);

使用 postman 测试一下

测试路由

其他中间件

  • koa-bodyparser ——请求体解析
  • koa-static —— 提供静态资源服务
  • @koa/cors —— 跨域
  • koa-json-error —— 处理错误
  • koa-parameter —— 参数校验
cnpm i koa-bodyparser -S
cnpm i koa-static -S
cnpm i @koa/cors -S
cnpm i koa-json-error -S
cnpm i koa-parameter -S
const path = require("path");
const Koa = require("koa");
const bobyParser = require("koa-bodyparser");
const koaStatic = require("koa-static");
const cors = require("@koa/cors");
const error = require("koa-json-error");
const parameter = require("koa-parameter");
const routing = require("./routes");

const app = new Koa();

app.use(
  error({
    postFormat: (e, { stack, ...rest }) =>
      process.env.NODE_ENV === "production" ? rest : { stack, ...rest },
  })
);
app.use(bobyParser());
app.use(koaStatic(path.join(__dirname, "public")));
app.use(cors());
app.use(parameter(app));
routing(app);

app.listen(3000, () => {
  console.log("3000端口已启动");
});

实现 JWT 鉴权

JSON Web Token(JWT)是一种流行的 RESTful API 鉴权方案

先安装相关的 npm 包

cnpm install koa-jwt jsonwebtoken -S

创建 config/index.js ,用来存放 JWT Secret 常量,代码如下:

const JWT_SECRET = "secret";

module.exports = {
  JWT_SECRET,
};

有些路由我们希望只有已登录的用户才有权查看(受保护路由),而另一些路由则是所有请求都可以访问(不受保护的路由)。在 Koa 的洋葱模型中,我们可以这样实现:

加入JWT后的洋葱模型

可以看出,所有的请求都可以直接访问未受保护的路由,但是受保护的路由都放在 JWT 中间件的后面,我们需要再创建几个文件来做 JWT 的实验

我们知道,所谓的用户(users)是个最常见的需要鉴权的路由,所以我们现在 controllers 中创建 user.js ,写下如下代码:

class UserController {
  static async create(ctx) {
    ctx.status = 200;
    ctx.body = "create";
  }
  static async find(ctx) {
    ctx.status = 200;
    ctx.body = "find";
  }
  static async findById(ctx) {
    ctx.status = 200;
    ctx.body = "findById";
  }
  static async update(ctx) {
    ctx.status = 200;
    ctx.body = "update";
  }
  static async delete(ctx) {
    ctx.status = 200;
    ctx.body = "delete";
  }
}

module.exports = UserController;

注册 JWT 中间件

用户的增删改查都安排上了,语义很明显了,其次我们在 routes 文件中创建 user.js,这里展示与 users 路由相关的代码:

const Router = require("koa-router");
const jwt = require("koa-jwt");
const {
  create,
  find,
  findById,
  update,
  delete: del,
} = require("../controllers/user");

const router = new Router({ prefix: "/users" });
const { JWT_SECRET } = require("../config/");

const auth = jwt({ JWT_SECRET });

router.post("/", create);
router.get("/", find);
router.get("/:id", findById);
router.put("/:id", auth, update);
router.delete("/:id", auth, del);

module.exports = router;

综上代码,routes 文件下的 home.js 都不需要 JWT 中间件的保护,user.js 中的 更新和删除需要 JWT 的保护

测试一下,能看出 JWT 已经起作用了

测试JWT

我们到目前为止,完成了对 JWT 的验证,但是验证的前提是先签发 JWT,怎么签发,你登录的时候我给你一个签好名的 token,要更新/删除时在请求头中带上 token,我就能校验…

这里牵扯到登录,我们先暂停一下,先补充数据库的知识,让项目更加完整

Mongoose 加入战场

如果要做一个完整的项目,数据库是必不可少的,与 Node 匹配的较好的是 NoSql 数据库,其中以 Mongodb 为代表,当然如果我们要使用这一数据库,需要按照相应的库,而这个库就是 mongoose

下载 mongoose

cnpm i mongoose -S

连接及配置

config/index.js 中添加 connectionStr 变量,代表 mongoose 连接的数据库地址

const JWT_SECRET = "secret";
const connectionStr = "mongodb://127.0.0.1:27017/basic";

module.exports = {
  JWT_SECRET,
  connectionStr,
};

创建 db/index.js

const mongoose = require("mongoose");
const { connectionStr } = require("../config/");

module.exports = {
  connect: () => {
    mongoose.connect(connectionStr, {
      useNewUrlParser: true,
      useUnifiedTopology: true,
    });

    mongoose.connection.on("error", (err) => {
      console.log(err);
    });

    mongoose.connection.on("open", () => {
      console.log("Mongoose连接成功");
    });
  },
};

进入主文件 index.js,修改配置并启动

...
const db = require('./db/')
...

db.connect()

启动服务 npm run serve,即 nodemon index.js,能看出 mongoose 已经连接成功了

nodemon

创建数据模型定义

在根目录下创建 models 目录,用来存放数据模型定义文件,在其中创建 User.js,代表用户模型,代码如下:

const mongoose = require("mongoose");

const schema = new mongoose.Schema({
  username: { type: String },
  password: { type: String },
});

module.exports = mongoose.model("User", schema);

具体可以看看 Mongoose 这篇文章,这里我们就看行为,以上代码表示建立了一个数据对象,供操作器来操作数据库

在 Controller 中操作数据库

然后就可以在 Controller 中进行数据的增删改查操作。首先我们打开 constrollers/user.js

const User = require("../models/User");

class UserController {
  static async create(ctx) {
    const { username, password } = ctx.request.body;
    const model = await User.create({ username, password });
    ctx.status = 200;
    ctx.body = model;
  }
  static async find(ctx) {
    const model = await User.find();
    ctx.status = 200;
    ctx.body = model;
  }
  static async findById(ctx) {
    const model = await User.findById(ctx.params.id);
    ctx.status = 200;
    ctx.body = model;
  }
  static async update(ctx) {
    const model = await User.findByIdAndUpdate(ctx.params.id, ctx.request.body);
    ctx.status = 200;
    ctx.body = model;
  }
  static async delete(ctx) {
    await User.findByIdAndDelete(ctx.params.id);
    ctx.status = 204;
  }
}

module.exports = UserController;

以上代码中,

  • User.create({xxx}):在 User 表中创建一个数据
  • User.find():查看所有的 User 表中的数据
  • User.findById(id):查看 User 表中的其中一个
  • User.findByIdAndUpdate(id, body):更新 User 表中的其中一个数据
  • User.findByIdAndDelete(id):删除 User 表中的其中一个数据

以上就是对数据库的增删改查

加盐

这个我们需要对密码进行一下加密,无它,安全。

进数据库一查,就能看到密码,这说明数据对开发人员是公开的,开发人员可以拿着用户的账号密码做任何事,这是不被允许的

数据库中的用户表

下载 npm 包——bcrypt

cnpm i bcrypt --save

我们前往 models/User.js 中,对其进行改造

...
const schema = new mongoose.Schema({
  username: { type: String },
  password: {
    type: String,
    select: false,
    set(val) {
      return require('bcrypt').hashSync(val, 10)
    },
  },
})
...

添加 select:false 不可见,set(val) 对值进行加密,我们来测试一下

创建李四

能看到 password 被加密了,即使在数据库里,也看不出用户的密码,那用户输入的密码难道输入这么一串密码,显然不是,用户要是输入的话,我们要对其进行验证,例如我们的登录

我们进入 constrollers/home 文件中,对其进行改造,

...
class HomeController {
  static async login(ctx) {
    const { username, password } = ctx.request.body
    const user = await User.findOne({ username }).select('+password')
    const isValid = require('bcrypt').compareSync(password, user.password)
    ctx.status = 200
    ctx.body = isValid
  }
  ...
}
  • User.findOne({ username }) 能查到到没有 password 的数据,因为我们人为的把 select 设为 false,如果要看,加上 select(‘+password’) 即可
  • require(‘bcrypt’).compareSync(password, user.password) 将用户输入的明文密码和数据库中的加密密码进行验证,为 true 是正确,false 为密码不正确

回到 JWT

在 Login 中签发 JWT Token

我们需要提供一个 API 端口让用户可以获取到 JWT Token,最合适的当然是登录接口 /login ,打开 controllers/home.js,在 login 控制器中实现签发 JWT Token 的逻辑,代码如下:

const jwt = require('jsonwebtoken')
const User = require('../models/User')

const { JWT_SECRET } = require('../config/')

class HomeController {
  static async login(ctx) {
    const { username, password } = ctx.request.body

    // 1.根据用户名找用户
    const user = await User.findOne({ username }).select('+password')
    if (!user) {
      ctx.status = 422
      ctx.body = { message: '用户名不存在' }
    }
    // 2.校验密码
    const isValid = require('bcrypt').compareSync(password, user.password)
    if (isValid) {
      const token = jwt.sign({ id: user._id }, JWT_SECRET)
      ctx.status = 200
      ctx.body = token
    } else {
      ctx.status = 401
      ctx.body = { message: '密码错误' }
    }
  }
  ...
}

login 中,我们首先根据用户名(请求体中的 name 字段)查询对应的用户,如果该用户不存在,则直接返回 401;存在的话再通过 (bcrypt').compareSync 来验证请求体中的明文密码 password 是否和数据库中存储的加密密码是否一致,如果一致则通过 jwt.sign 签发 Token,如果不一致则还是返回 401。

在 User 控制器中添加访问控制

Token 的中间件和签发都搞定之后,最后一步就是在合适的地方校验用户的 Token,确认其是否有足够的权限。最典型的场景便是,在更新或删除用户时,我们要确保是用户本人在操作。打开 controllers/user.js

const User = require('../models/User')

class UserController {
  ...
  static async update(ctx) {
    const userId = ctx.params.id
    if (userId !== ctx.state.user.id) {
      ctx.status = 403
      ctx.body = {
        message: '无权进行此操作',
      }
      return
    }
    const model = await User.findByIdAndUpdate(ctx.params.id, ctx.request.body)
    ctx.status = 200
    ctx.body = model
  }
  static async delete(ctx) {
    const userId = ctx.params.id

    if (userId !== ctx.state.user.id) {
      ctx.status = 403
      ctx.body = { message: '无权进行此操作' }
      return
    }

    await User.findByIdAndDelete(ctx.params.id)
    ctx.status = 204
  }
}

module.exports = UserController

添加了一些用户并登录,将 Token 添加到请求头中,使用 DELETE 删除用户,能看到 状态码变成 204,删除成功

删除用户操作

断言处理

在做登录时、更新用户信息、删除用户时,我们需要 if else 来判断,这看起来很蠢,如果我们能用断言来处理,代码在看上去会优雅很多,这个时候 http-assert 就出来了

// constrollers/home.js
...
const assert = require('http-assert')


class HomeController {
  static async login(ctx) {
    const { username, password } = ctx.request.body
    // 1.根据用户名找用户
    const user = await User.findOne({ username }).select('+password')
    // if (!user) {
    //   ctx.status = 401
    //   ctx.body = { message: '用户名不存在' }
    // }
    assert(user, 422, '用户不存在')
    // 2.校验密码
    const isValid = require('bcrypt').compareSync(password, user.password)
    assert(isValid, 422, '密码错误')
    const token = jwt.sign({ id: user._id }, JWT_SECRET)
    ctx.body = { token }
  }
   ...
}

同理,处理 controllers/user

...
  static async update(ctx) {
    const userId = ctx.params.id
    assert(userId === ctx.state.user.id, 403, '无权进行此操作')
    const model = await User.findByIdAndUpdate(ctx.params.id, ctx.request.body)
    ctx.status = 200
    ctx.body = model
  }
  static async delete(ctx) {
    const userId = ctx.params.id
    assert(userId === ctx.state.user.id, 403, '无权进行此操作')
    await User.findByIdAndDelete(ctx.params.id)
    ctx.status = 204
  }
...

代码看起来就是整洁清爽

参数校验

之前我们加了一个中间件——koa-parameter,我们当初只是注册了这个中间件,但是未使用,我们在创建用户时需要判断用户名和密码的数据类型为 String 类型且必填,进入 controllers/user.js 添加代码如下:

...
class UserController {
  static async createUser(ctx) {
    ctx.verifyParams({
      username: { type: 'string', required: true },
      password: { type: 'string', required: true },
    })
    const { username, password } = ctx.request.body
    const model = await User.create({ username, password })
    ctx.status = 200
    ctx.body = model
  }
  ...
}

Github 地址:koa-basic

参考资料

一杯茶的时间,上手 Koa2 + MySQL 开发

芝麻开门,显示全文!

GitHub Actions + Github Page

主要用到了 Github Actions 中的别人写的好的动作

#  https://github.com/crazy-max/ghaction-github-pages
	- name: Deploy to GitHub Pages
      uses: JamesIves/github-pages-deploy-action@3.7.1
      with:
          ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
          # 部署到 gh-pages 分支
          BRANCH: gh-pages
          # 部署目录当前目录
          FOLDER: .

共两步,先配置 main.yml,再配置 ACCESS_TOKEN,就可以 先配置 ACCESS_TOKEN

使用场景

如果我要做 css 效果、做一个 Demo 供别人看,最好是放在网站上,其中 GitHub Page 是最好的选择 做好 Github Actions,就能做到即写即看,而且能一直保留到 GitHub 关门为止

注:其实还可以用 codepen 之类在线编码

在 gh-pages 上加上 CNAME,会将 CNAME 中的 gh-pages 指向 CNAME 中的域名,当然前提是你需要在域名解析那里把你的域名指向 github.com

所以只能放在另一个自己的二级域名上, https://github.azhubaby.com

随便一说,我这域名买了 10 年,2030 年才到期,所以很长一段时间内是有效的

成功案例

我的 demo 集合: https://demo.azhubaby.com

项目 Github 地址: github-actions-github-pages

芝麻开门,显示全文!

GitHub Actions 部署前端项目

我要部署一个项目,我大喊!!

怎么做?

先把代码 npm run build,把生成的 dist 拿来,登录服务器,到指定目录页面,打开 ftp,手动上传 dist 中的文件

其中你要开启你的 nginx 服务

你每一次想部署项目,就要先在本地打包,然后登录服务器,再打开 ftp 服务,再手动上传,这一来一回太麻烦,而且这种情况不利于多人开发,依赖的环境也许受操作系统的影响等等

总之,这不便于开发者以及不科学

我们需要持续集成

在公司中我们可以搭建 GitLab 亦或是 用 Jenkins,但是对个人,独立开发者或者只有五人以下的小公司来说,有什么可以帮我们实现自动化工作流的呢?

Github 推出了 Github Actions

GitHub Actions 是什么

GitHub Actions 是 GitHub 的持续集成服务,于 2018 年 10 月推出

持续集成由很多操作组成,比如抓取代码、运行测试、登录远程服务器,发布到第三方服务等等。GitHub 把这些操作就称为 actions

GitHub 做了一个官方市场,可以搜索到他人提交的 actions。另外,还有一个 awesome actions 的仓库,也可以找到不少 action。

基本概念

GitHub Actions 有一些自己的术语。

(1)workflow (工作流程):持续集成一次运行的过程,就是一个 workflow。

(2)job (任务):一个 workflow 由一个或多个 jobs 构成,含义是一次持续集成的运行,可以完成多个任务。

(3)step(步骤):每个 job 由多个 step 构成,一步步完成。

(4)action (动作):每个 step 可以依次执行一个或多个命令(action)。

具体可以前往 阮一峰的这篇 GitHub Actions 入门教程 了解一二

实操

我们的目的,通过 Github Action 把一个单页面应用部署到 云服务器上

具体可参考 使用 Github Action 部署项目到云服务器 中的操作,我讲讲在使用这位大哥写的 SFTP-Deploy-Action@v1.0 action 时遇到的坑

常见问题:

首先遇到 Load key "../private_key.pem": invalid format 的错误。

解决方案 ssh-keygen -m PEM 生成 新 key

我遇到类似的问题,以为要更新我的私钥,后来发现不是,是因为我的私钥是 openssh,改成 rsa 就没报这个错误了

错误二:Permission denied (publickey,gssapi-keyex,gssapi-with-mic,password).

免登录问题

具体步骤较多,简单来说,就是 .ssh 文件下新建一个 authorized_keys 文件,将公钥放到其中

并去 /etc/ssh/sshd_config 中修改一些配置,实现免登录

这里不细讲,具体可以 Google 查一下

错误三:

Warning: Permanently added '_\*\*' (ECDSA) to the list of known hosts.
sftp> put -r ./build/_ /home/johan/www/react-app
Multiple paths match, but destination "/home/johan/www/react-app" is not a directory

文件路径不对

错误四:

Couldn't canonicalize: No such file or directory
Unable to canonicalize path "/home/johan/www/react-app/static"

找不到 static 文件,新生成的 static 文件不能放入项目中,那就新建一个

去项目目录下 mkdir static

再编译,成功

编译成功

附上 Github 地址:React-Deploy

参考资料

芝麻开门,显示全文!

linux安装node

在 Linux 上安装 node ,方便以后部署 node 服务

一、下载并解压 node

# cd /usr/local/src
# wget https://nodejs.org/dist/v14.16.1/node-v14.16.1-linux-x64.tar.xz
# tar node-v14.16.1-linux-x64.tar.xz
# cd node-v14.16.1-linux-x64/
# ./bin/node -v
v14.16.1

下载地址:https://nodejs.org/en/download/

二、对 node 、npm 设置软连接

# ln -s /usr/local/src/node-v14.16.1-linux-x64/bin/node /usr/bin/node
# node -v
v14.16.1

# ln -s /usr/local/src/node-v14.16.1-linux-x64/bin/npm /usr/bin/npm
# npm -v
6.14.12

三、下载 cnpm、pm2 并设置软连接

# npm install -g cnpm --registry=https://registry.npm.taobao.org
# npm install -g pm2 --registry=https://registry.npm.taobao.org
# ln -s /usr/local/src/node-v14.16.1-linux-x64/bin/cnpm /usr/bin/cnpm
# ln -s /usr/local/src/node-v14.16.1-linux-x64/bin/pm2 /usr/bin/pm2

注意:如果你的软连接设置错了,即报”ln: failed to create symbolic link ‘/usr/local/bin/npm’: File exists“

可以使用 ln -sf 来重新设置

# ln -sf /usr/local/src/node-v14.16.1-linux-x64/bin/npm /usr/bin/npm

芝麻开门,显示全文!

阿里、得物、涂鸦、途虎面试心得(二)

之前在 2021-08-17-阿里、得物、涂鸦、途虎面试心得 里说了面试的考点,这篇主要是讲面试官,个人认为一个好的面试官应该是按照简历展开面试,针对简历上的技能点来展开,这样能从面试者最熟悉的点切入,由浅入深全方位的考量一个面试者

得物一面男

看打扮就明白不是组长级别的,后得知是同事面,问了比较详细,从简历上的种种到部署,可以看出是个面试生手,因为一个熟手不会像他这样一直问一些技术点,生怕我不会一样

得物二面男

是个 30 多岁的说话比较快、发音不标准的男人。和他对话后,我不太喜欢在他底下做事。他会问一些开放型的题目,比如他们要用微前端(我只知道这个技术,但没用过),问我为什么要用微前端,我针对微前端展开,说了几点。他不满意,说微前端最好的好处是方便运营人员,避免他们在两个系统里跳来跳去。惊了,原来是让我跳出程序员的思维,早说,后面就引着这个思路乱说,到现在一个问题印象也没有,因为大多数在临时起意胡说的

涂鸦电话面(女)

我当时有点傻,去了涂鸦上海本地,明明是电话面试,竟然没注意到,去了之后没前台,联系 HR,说是约电话面,ok,我蠢。到楼下看有没有充电宝,电不到 20%,找不到出门找全家,没想到电话来了,让我自我介绍,左右为难下回到大楼坐在沙发上打电话

与她交流,能看出她的实力,她是个很会抓知识点的面试官

开始讲针对项目中登录的问题,简单交流,后续只见她问

她问:“你们怎么做到锁单,就像用户进去下单页后,点击下单后锁单,你们怎么做?“

我说:后端会把这个用户 id 和商品 id 关联锁住,(我知道她想考前端要做什么事情)前端方面会在请求点击后就把按钮置灰并让他不能点击,而且还可以通过防抖、节流优化

她问:“那这个场景是用防抖还是节流?”

我想了想说:“节流”

她说:“那你能手写一个节流吗?“

我无语了,我 TM 都在准备手写节流,让我口喷,怎么喷,吞吞吐吐下说的很差,但说到几个关键字

function debounce(func, wait, flag) {
  let timer = null;
  if (timer && flag) {
    return function (...args) {
      timer = setTimeout(() => {
        func.apply(args);
      }, wait);
    };
  }
  return function (...args) {
    timer = setTimeout(() => {
      func.apply(args);
    }, wait);
  };
}

PS:差不多就是上面的回答,错的,错的,错的

她说:ok,你回答里说到三个点,你能说说三个点是什么吗?

我说:展开运算符,他的用法是能让数组的值展开

她问:除了数字,还能展开哪些数据类型

我开始不知道了,没想到 ES6 还有这么多讲究,我胡扯到 String 类型,Object 类型

她说:String ?你确定吗?

我怂了,这个知识点我不知道

她问你刚才说的 setTimeout 中用箭头函数,你能说说箭头函数与普通函数的区别吗?

这个题在得物二面的时候就问到了,我回答

阿里面试官

与他聊天更像是在与监考老师打交道

阿里:我先问你几个 CSS 方面的问题

我:好…

阿里:我再问你几个 React 方面的问题

我:巴拉巴拉

阿里:最后问你一个 JS 方面的问题

我:

途虎面试官

react 方面

this.setState

异步还是同步?

出题 class C

其中有 promise

链式调用

出题 链式调用

简单化的 链式调用

了解 Fiber 吗

说完 fiber 后,是多了调度器

调度的顺序是如何?

谁的优先级在前,谁的优先级在后

最后一个问题

性能优化怎么做

我说从工程方面讲,webpack 的打包

从 React 方面讲,有 React.memo 、React.useCallback 等代码优化的点

从 网络方面考虑,做 缓存 机制

这里说了下强缓存、协商缓存

又问我图片懒加载怎么做

不知道?

引导

通过图片本身距离顶部的距离,通过监听 scroll …

芝麻开门,显示全文!

阿里、得物、涂鸦、途虎面试心得

五月份搬到安静、相对较大的房间后,决定每天早起来复习,在这之前我是通过下班后待公司里学习和晚上在家复习的方式学习,这种方式不合理,因为经过一天的工作自身精力已经消耗的七七八八了,再去学习,效率已经不高了

经过三个月的努力,于 8 月 8 日奥运结束后,我投了简历,这次共投了 6 家公司,分别为阿里、得物、涂鸦、途虎、拼多多、TapTap。其中 TapTap 是想去的,其他五家各有千秋,属于可以拿到 offer 等待公司

其中阿里、得物、涂鸦、途虎四家给了面试机会。周五下午得物面试,面完后电话面试涂鸦,周六阿里电话面试,周一晚上途虎视频面试,结果是都被刷下去了。问过得物的两个面试官”觉得自己今天的表现如何“,一面面试官(同事面)说觉得一般,简历写的不好,应该把自己项目中有用到的技术都写进技能点里,而不是让面试官去找;二面(组长面)说优点是涉猎面广,但是对原理不够深入

虽然没有被面上,但或许是一个好事,因为这次面试透露出我的一些短板。例如对知识的表达,说到底还是对这块知识不了解

面试考点

JS

  • 防抖与节流

    口喷不出来,之前做过准备,手写可以,但是没喷出来,还是对原理什么的理解的不够深刻

  • 捕获错误怎么做?

    知道 try catch,但是没说来,面试官引导到后面才明白要我说这个

  • 图片懒加载

    没做准备,PASS

  • 类数组与数组的区别

    说不出个所以来

  • 如何释放闭包

    把引用的闭包设为 null

  • 事件循环

    这题高频

  • HTTP 缓存

    就途虎的面试官问了,之前做过这方面的准备

CSS

  • BFC 是什么?

    阿里面试官问的,老掉牙的问题,但是他一直让我说,我说了几个点还不够,要说全,触发 BFC 的元素有哪些,我当时说了四五个,他说还有呢?淦

  • flex: 1 表示什么

    MD,flex-grow 读错了,英语口语还是重要

  • 左边宽度已知、右边自适应方案

    阿里面试官问的,说了四种,问还有吗

React

  • 函数式组件与 class 组件的区别

    这竟然是最常见的问题,说了好几个点,没抓到重要,重点是 函数式组件能捕获每次渲染时的值,这个俗称 Capture Value。React 的主要开发者 Dan 写过一篇文章函数式组件与类组件有何不同?

    周五面试得物、涂鸦,都问到了这个问题

  • 为什么不能在循环中调用 hooks?react 中为什么不能在 for 循环、if 语句里使用 hooks,说下 react hooks 实现原理。

  • 虚拟列表,1000 条数据 插入不卡的那种

  • Fiber 是什么?

    • 多了调度器,有优先级,优先级的顺序是怎么个排序?
  • React.memo、React.useCallback、React.usememo 的作用

  • this.setState 是异步还是同步?为什么?

    • 什么事件可以触发异步,什么会触发同步
  • 如下的代码,它的 a 的值是多少?又 render 了几次
class C extends Component {
   this.state = {
       a: 0;
   }
   componentDidMount() {
       this.setState({ a: 1 })
       setTimeout(() => {
           	this.setState({a: 2})
       }, 0)
       new Promise((resolve) => {
           resolve(this.setState({ a: 3}))
       }).then(() => { this.setState({a:4}) })
   }
}
  • useState 中的值是个对象,改变对象中的值,demo 会渲染吗?如果用 React.memo() 包裹住呢
function demo() {
  const [data, setData] = useState({ foo: { bar: { baz: 1 } } });
  return <div onClick={() => setData(1)}>改变</div>;
}
  • react 从本页面跳转至其他站点页是否会执行 unmount?为什么

    A:不会,但不知道为什么

  • react 中的 错误捕获

  • React.Router 的模式

    手写一个

    三种模式

  • Dva 中与 namespce 同层的参数有哪些

ES6

  • Promise 链式调用

    • 链式调用怎么写
  • 展开运算符的特性

    • 能展开 string、array、object
  • commonJS 与 ES 模块的区别

  • webpack 打包后,commonJS 中的引用会被引用吗?ES6 模块的呢?为什么

移动端

  • 适配方案

  • JsBridge

  • 移动端 ios 输入时 input 被挡住, 安卓键盘回落后留白问题

  • 弹出款滚动问题(没人问,但是我想写一篇)

组件

  • 主题色

TS

  • any 用的多吗?什么时候会用 any?什么时候不用

Webpack

  • webpack 打包后,commonJS 中的引用会被引用吗?ES6 模块的呢?为什么

  • split code 代码分割的原理是什么

CI/CD

  • 你是如何打包项目的

    A:我的情况是,项目在本地打包,然后把 dist 文件提交到 git 上,再登录服务器,手动 git pull

    我当然知道确定这种方式不科学,公司不提供服务器,没资源,组长也不愿意做变化

    我上家公司是把代码放在 gitlab 上,也方便各位 review 代码

目前看,我后续会把个人开发者如何利用 GitHub 来做 CI/CD,可以看看这篇 2021-08-19-GitHubActions-部署前端项目 以及这篇 2021-08-19-GitHubActions+GitHubPage

Docker

  • dockerfile 中的参数有哪些

    FROM、RUN、BUILD…


疑惑

很神奇,五个面试官都没有问原型、闭包、作用域、执行上下文之类的问题,也许是因为现在是高级前端开发面,所以不问这些基础的?

后记

女朋友看到我努力了三个月,每天早上 6 点起来学习,周末也会抽出时间来看一些技术方面的知识,但还是没有面上。

她对我说:你的经历让我看到不是所有的努力都会有回报,所以让我觉得努力没用

我不懂这有什么好伤感的。面试,面不上,很正常,毕竟不是小公司,对人的要求本身就很高,我已经在这次经历中知道自己的不足,这很重要,美团还没面,这也很重要,我也有时间可以复习,再进步,这就足够了…

不是努力有没有用的问题,是成长。能进大厂是结果,学到知识是过程。而且去了大厂我还是会每天早起学习,这是拉开人与人之间距离最好的方式,别人在休息的时候你再学习,你学习时间比别人多,勤能补拙。不要觉得”勤能补拙“这个词有多么不好,我觉得很好。做到勤就比很多人优秀了

但我也不是什么奋斗逼,口号喊得很响亮,但是身体却没力行,面试-不足-克服,就像打游戏一样,打不过 BOOS,先去打大头兵,涨经验,就这么简单

芝麻开门,显示全文!

一道关于解构赋值和参数默认值的编程题

前端俱乐部QQ群中,有朋友发出这样的题目,说最近面试中遇到了,如下所示

function fun( ? ) {
    return {a,b}
}
console.log(fun( )) // {a:1,b:2}
console.log(fun({a:3})) // {a:3,b:456}
console.log(fun({})) // {a:123,b:456}

问,fun 的参数应该填什么?

提示:利用结构赋值和参数默认值

经过我一番测试,其结果如下所示:

function fun({ a = 123, b = 456 } = { a: 1, b: 2 }) {
  return { a, b };
}

解题思路

先看第一个执行: console.log(fun( )) // {a:1,b:2}

fun() 不传参数,直接执行,结果 a 为 1,b 为 2。说明默认值为 a : 1,b : 2。

可以得出

function fun(a = 1, b = 2) {
  return { a, b };
}

再看第二个执行:console.log(fun({a:3})) // {a:3,b:456}

fun({a: 3}) ,参数传入一个对象,对象中 a 为 3,其结果 a 为 3,b 为 456。说明其参数默认值为一个对象,对象中的值又有默认参数 a 与 b。

结合“执行 1”,如果不传参数,默认用 a = 1, b = 2 的选项;如果传入参数;则用对象中的默认参数。即

function fun({ a = XX, b = 456 } = { a: 1, b: 2 }) {
  return { a, b };
}

最后看第三个执行:console.log(fun({})) // {a:123,b:456}

很明显,我们还不知道对象中的默认 a 代表什么。第三个执行告诉我们它为 123

所以最后我们的答案是

function fun({ a = 123, b = 456 } = { a: 1, b: 2 }) {
  return { a, b };
}

难点

在做这道题的时候,我被赋值的 =: 迷惑了。这里做笔记记录

  • : 针对对象赋值

  • = 为默认值

如图所示:

const obj = {
  a: 1,
  b: 2,
  c: 3,
  d: 4,
};
const { a, b } = obj; // 解构赋值 a,b, a为1,b为2
const { a = 11, b = 21, e = 51 } = obj; // 给解构赋值的 设置默认值,a为1,b为2,e为51,得默认值是当你对象中没有值时,赋予该变量的默认值

= 赋予变量默认值

那么 : 何处使用,当解构赋值中的值为一个对象的时,设置对象中的值就用

const obj = {
  a: {
    aa: 11,
    bb: 22,
  },
  b: 2,
  c: 3,
  d: 4,
};
const { a, b } = obj; // a={aa: 11, bb: 22} b=2
const { a = { aa: 111, bb: 222 }, b = 22, e = { aa: 111, bb: 222 } } = obj;
// a={aa: 11, bb: 22},b = 22, e={aa: 111, bb: 222}

这里我们要注意:当解构的值在对象中,即使设置默认值,还是会以值为准;如果解构的值不在对象中,则会以默认值的形式出现在结果值中。

回头看题

function fun( ? ) {
    return {a,b}
}
console.log(fun( )) // {a:1,b:2}
console.log(fun({a:3})) // {a:3,b:456}
console.log(fun({})) // {a:123,b:456}

为什么 a = 123, b = 456 要用等于号=,而不是用冒号:呢,因为它原本是”键“,只能赋予默认值而不能将键重命名

总结

解构赋值时,冒号: 是重命名,等于号= 是赋值默认值

芝麻开门,显示全文!

移动端疑难杂症(持续更新)

写在前面

本来记录在印象笔记中,但放着老是不看,为加深印象以及搜索方便,立一文统一记录移动端的兼容性问题

前人总结

司徒正美-mobileHack

总结几个移动端 H5 软键盘的大坑

input 中输入不要有空格

onChange(replace(/\s+/g, '')

<input onChange={onChange(e.target.value.replace(/\s+/g, ""))} />

input 去除 ios 端输入法首字母大写状态

<input type="text" autocapitalize="off" autocorrect="off" />

css 一行省略

普通版本

overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;

有赞版本

display: -webkit-box;
overflow: hidden;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;

CSS 多行省略

-webkit-line-clamp: 3; // 文本行数
display: -webkit-box;
-webkit-box-orient: vertical;
text-overflow: ellipsis;
word-break: break-all;
overflow: hidden;

ios 使用 border-radius 时失效

原因:ios 端,该元素使用 transform 属性会导致 border-radius 失效

解决方法:在使用动画效果(使用 transform)元素的上一级的 css 上加上以下属性:

-webkit-transform: rotate(0deg);

开发H5营销页面遇到的问题

ios端不能自动播放音乐、视频(但之前看过一个网易云音乐的 H5 能自动播放音乐)

微信里打开的H5不支持点击下载,需要长按保存

ios 端高度超过一屏,input 输入完后不回弹

这个问题老生常谈,但目前我的 iPhone X 手机 ios 15系统未遇到这种情况,而 iPhone 7 手机遇到了这个问题。猜测是老的手机用的 webview 的 bug,解决方法是在监听 input 的 onblur 事件,在失去焦点的时候,拿到它滑动后的高度,使用 window.scrollTo 回到原来的 scrollHeight

如代码:

<input
  type="text"
  onBlur={() => {
    setTimeout(() => {
      if (util.system.ios) {
        const scrollHeight =
          document.documentElement.scrollTop || document.body.scrollTop || 0;
        window.scrollTo(0, Math.max(scrollHeight - 1, 0));
      }
    }, 100);
  }}
/>

看网上答疑:在 IOS12 ,微信版本 v6.7.4 及以上,输入框获取焦点,键盘弹起,页面(webview)整体往上滚动,当键盘收起后,不会到原位,导致键盘原来所在位置是空白的

安卓手机键盘弹出,希望表单跟着向上移动

ios 手机会跟着选中目标后滚动到中间位置,但是安卓系统不会,但你点击下方的表单时,表单不会向上滚动,键盘弹出后会遮住目标表单

安卓中的可视高度=我们看到的页面高度+软键盘的高度,而 IOS 的可视高度与软键盘无关

在 componentDidMount 或者 useEffect 中监听 resize,以 函数式组件为例

const App = () => {
  const originHeight =
    document.documentElement.clientHeight || document.body.clientHeight;
  useEffect(() => {
    window.addEventListener("resize", resizeWindow);
    return () => {
      window.removeEventListener("resize", resizeWindow);
    };
  }, []);
  const resizeWindow = () => {
    if (util.system.android) {
      const resizeHeight =
        document.documentElement.clientHeight || document.body.clientHeight;
      const activeElement = document.activeElement;

      if (resizeHeight < originHeight) {
        if (activeElement && activeElement.tagName === "INPUT") {
          setTimeout(() => {
            activeElement.scrollIntoView({ block: "center" });
          }, 100);
        }
      }
    }
  };
  return <div id="app"></div>;
};

思路如下:

在组件初始化时监听 resize 事件,如果在 android 系统的话,获取文档调整后的高度以及选择的组件,通过对比原先高度,如果文档调整后的高度小于原先的高度,意味着有键盘弹出,我们就使用scrollIntoView,让目标选中组件滚动到页面中间

更多H5软键盘兼容性问题可以看看这这个帖子:可能这些是你想要的H5软键盘兼容方案

如何实现页面刷新后不定位到之前的滚动位置

history.scrollRestoration

if (history.scrollRestoration) {
  history.scrollRestoration = "manual";
}

源自:https://www.zhangxinxu.com/wordpress/2022/05/history-scrollrestoration/

检测 passive 是否支持

var passiveSupported = false;

try {
  var options = Object.defineProperty({}, "passive", {
    get: function () {
      passiveSupported = true;
    },
  });

  window.addEventListener("test", null, options);
} catch (err) {}

https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget/addEventListener

Input 几个有趣的属性

<input
  autocomplete="false"
  autoCorrect="off"
  autocapitalize="off"
  autofocus="false"
/>

autoComplete="false":自动记录输入的值

浏览器不允许为此字段自动输入或选择一个值

https://developer.mozilla.org/zh-CN/docs/Web/HTML/Attributes/autocomplete

autoCorrect="off":自动纠错

autoCapitalize="off":自动大小写

控制用户输入/编辑文本输入时文本输入是否自动大写,以及如何自动大写

https://developer.mozilla.org/zh-CN/docs/Web/HTML/Global_attributes/autocapitalize

autoFocus=false:自动对焦

自动对焦

微信开发

ios右上角消失

document.addEventListener("WeixinJSBridgeReady", function onBridgeReady() {
  WeixinJSBridge.call("showOptionMenu");
});

https://qydev.weixin.qq.com/wiki/index.php?title=%E5%BE%AE%E4%BF%A1_JS%E6%8E%A5%E5%8F%A3

微信开发

最新版的微信已经不支持通过 debugx5.qq.com 打开 vconsole 了。要调试H5可以通过chrome远程:

① 把手机和电脑用usb连起来 ② 在手机微信中访问 http://[喵喵] debugxweb.qq.com/?inspector=true ③ 在电脑浏览器中访问 chrome://inspect/#devices

隐藏微信网页右上角的按钮

document.addEventListener("WeixinJSBridgeReady", function onBridgeReady() {
  `
// 通过下面这个API隐藏右上角按钮`;
  WeixinJSBridge.call("hideOptionMenu");
});
document.addEventListener("WeixinJSBridgeReady", function onBridgeReady() {
  // 通过下面这个API显示右上角按钮`

  WeixinJSBridge.call("showOptionMenu");
});

https://www.jianshu.com/p/d7f5f5131783

input ios端高度问题

芝麻开门,显示全文!

张潇雨的人生信念笔记

1.无论如何定义[成功],能达到这个状态的人都是极少数

2.一个人是 TA 打交道最多的五个人的平均水平

3.一个人水平的下限由 TA 学习的最差的五个对象决定

4.只向最好的人学习,其他人都不知道自己在干什么,不要理会它们的[建议] 心得:听自己敬佩的人的建议,其他人不用听。父母也好,朋友也罢,不要因为这层关系加重判断,要听,就听成功的人的意见,并放大此人意见的权重

打个比方,一个男生和一个女生,男生小镇青年,家境普通,学历普通,身高长相也普通。女生城市户口,父母公务员,学历比男生高,长相尚可(从小就有人追)。这样一对比,大多数网友的第一反应是不门当户对,这就是网友的意见。多数人靠着提供者提供的信息来分析事物,如果提供信息者隐藏或者没意识到重要信息导致没说出来,那么人给出的意见就不同。 所以微博上的热搜,这类事不用太较真,真真假假,假假真真。公说公有理,婆说婆有理 遇到人生重大事情时该怎么做?这让我想起了《白鹿原》里主角白嘉轩,他每次遇到决定不了的事情时,就会去请教姐夫朱先生,朱先生是私塾老师,是小说中的灵魂人物。姐夫每每点几句,白嘉轩就知道怎么做了 当你遇到一个重要事情时,不妨问问你敬佩的人,客观描述事情,让他分析。不要感情用事,不要代入,不要带有色眼镜

5.在没达到信息手机两的门槛之前,不轻易做判断和决策。多对自己和他人说[我不知道]

6.不是生活中的每一个问题都要解决,和问题共处是人生常态。把精力用在重要的事情上

7.寻找 [非对称回报] 的机会,即那种 [失败了损失很低,单一旦成功回报巨大] 的机会

8.更好的机会是 [失败了有一点线性回报,但成功理由巨大的指数回报],当然这中机会会非常稀少;

9.人境遇的改变往往是非线性的。积累和等待的过程很难熬,这是很多人无法改变的原因之一

10.耐心是非常值得拥有的品质

11.一个人的境遇绝大部分都受运气支配,但运气是可以被影响的

12.人很难靠出卖自己的单位时间获得财富

13.财富来自于对杠杆的使用,常见的杠杆有:互联网、人力、资本、声望、时间等等

14.财富的积累来源自不可代替性。学者让自己很难被替代,就像公司要建立自己的护城河。 心得:成为解决问题的人

15.很多时候最好的竞争优势就是 [别人觉得麻烦而你不觉得]

16.凡是都反过来想:想要投资成功,先弄清什么让人投资失败;想要生活幸福,先看什么事会导致人生不幸福。

17.在混沌、开放、随机的系统内,努力 [减少错误] 要比 [追求正确] 有效得多

18.人是无法用一套思维方式正确认知世界的。广泛地吸收知识,建立多远思维模型是成功的前提

19.当两件事看起来有些矛盾的时候,几乎总有一个更高层面的东西把它们统一起来。不断寻找,你就可以发现游戏的元规则是什么

20.将大部分的时间用在掌握游戏的元规则上

21.详细大数定律。一件事哪怕只有 10% 的把握,连试 20 次之后成功了也有九城

22.大多数的所谓心情问题、状态问题、创造力问题,都是身体健康和精力的问题 心得:大多数人不会主动解决,但身体会给你反馈 我检验精力是否饱满的两个技巧, 一在地铁上坐着看视频,精力指数小于 10,无论看什么都会发困,以至于闭上眼睛就能睡着 二找个安静的地方(图书馆或者半夜)看书,精力指数小于 50 就会打哈气 一般早上的精力比较好,所以早上的效率会比下午,晚上高

23.吃得健康、持续锻炼、保证睡眠,几乎可以待人走出任何困境

24.日常中积累小的信息和正反馈,这样在大的选择面前就会更加从容淡定

25.意志力这种东西几乎不存在。与其努力提高自己的意志力,不如给自己创造更好的环境于试无需调用意志力 心得:鄙人就是,一个好的环境对自己很重要 所以才想和女友搬到一起,这样能省下时间去学习

26.多和 [真、善、美]的东西在一起。它们的珍贵程度叶恒实这个顺序

27.在解决生存问题之后,人的幸福感主要由身边关系 的质量决定

28.爱不是一种情侣之间特有的东西,而是一种生命状态,也是你选择与世界沟通的方式

29.[对自己诚实]试最被低估的品质。一个人如果嫉妒坦诚,TA 就是无坚不催的

30.学会不带评判地自我觉察,这是一切改变的起点

31.人们懂得很多道理但仍然无法改变,是因为 [大脑知道] 和 [身体知道] 完全是两件事 心得:韩寒的“听过很多道理,依然过不好这一生”也是大脑知道。如果通过一些小事让自己觉知很重要

芝麻开门,显示全文!

this 关键字

先说结论:谁调用它,this就指向谁

前言:在讲作用域的时候,我们讲到了this,因为JavaScript中的作用域是词法作用域,在哪里定义,就在那里形成作用域。而与词法作用域相对应的还有一个作用域叫动态作用域,调用时去寻找它所处的位置。那个时候我就说道 this机制 和动态作用域很像。

关于this

为什么使用 this

我们解释一下为什么要使用this,用一个例子

function identify() {
  return this.name.toUpperCase();
}

function speak() {
  var greeting = "Hello, I'm" + identify.call(this);
  console.log(greeting);
}

var me = {
  name: "johan",
};

var you = {
  name: "elaine",
};

identify.call(me); // JOHAN
identify.call(you); // ELAINE

speak.call(me); // Hello, I'm JOHAN
speak.call(you); // Hello, I'm ELAINE

这段代码可以在不同的上下文对象(me 和 you)中重复使用函数identity() 和 speak(),不用针对每个对象编写不同版本的函数

如果不适用 this,那就需要给identify() 和 speak() 显式传入一个上下文对象

function identify(context) {
  return context.name.toUpperCase();
}

function speak(context) {
  var greeting = "Hello, I'm" + identify(context);
  console.log(greeting);
}

identify(you); // ELAINE
speak(me); // Hello, I'm JOHAN

看到这里你也许明白了,this 是一种更为优雅的”传递”对象引用的方式。这个例子还过于简单,当你遇到7.8个甚至10几个函数(或叫方法)之间的调用时,显式传值无疑会变得混乱。除此之外,在原型中,函数自动引入合适的上下文对象是极为重要的,这个我们放在原型章中在讲。

this到底是什么

this到底是一种什么样的机制。

  1. this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。
  2. this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式
  3. 当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。this就是这个记录的一个属性,会在函数执行的过程中用到。

调用位置

正如上面所讲,this是在运行时绑定的,它的上下文取决于函数调用时的各个条件。在JavaScript中函数的调用有以下几种方式:作为对象方法调用,作为函数调用,作为构造函数调用,和使用apply或者call调用。下面我们按照调用方式不同,分别讨论 this 的含义

作为对象方法调用

在 JavaScript中,函数也是对象,因此函数可以作为一个对象的属性,此时该函数被称为该对象的方法,在调用这种调用方式时,this 被自然绑定到该对象

var people = {
  name: "elaine",
  age: 26,
  sayName: function () {
    console.log(this.name);
  },
};
people.sayName(); // elaine

作为函数调用

函数也可以直接被调用,此时 this 绑定到全局对象。在浏览器中,window 就是该全局对象。比如下面的例子:函数被调用时,this 被绑定到全局对象,接下来执行赋值语句,相当于隐式的声明了一个全局变量,这显然不是调用者希望的

function sayAge(age) {
  this.age = age;
}
sayAge(5);
// age 已经成为一个值为 5 的全局变量

对于内部函数,即声明在另外一个函数体内的函数,这种绑定到全局对象的方式会产生另外一个问题。我们仍然以前提到的 people 对象为例,这次我们希望在 sayName 方法内定义一个函数,函数打印年龄。结果可能出乎大家意料,不仅年龄没有打印出,反而多了一个全局变量 age

var people = {
  name: "elaine",
  age: 26,
  sayName: function (age) {
    var sayAge = function (age) {
      this.age = age;
    };
    sayAge(age);
  },
};
people.sayName(5);
people.age; // 26
age; // 5

这属于 JavaScript 的设计缺陷,正确的设计方式是内部函数的 this 应该绑定到其外层函数对应的对象上,为了规避这一设计缺陷,聪明的 JavaScript 程序员想出了变量代替的方式,约定俗成,该变量一般被称为 that

var people = {
  name: "elaine",
  age: 26,
  sayName: function (age) {
    var that = this;
    var sayAge = function (age) {
      that.age = age;
    };
    sayAge(age);
  },
};
people.sayName(5);
people.age; // 5
age; // 没有定义

当然,当我们使用ES6中的箭头函数时,我们会发现箭头函数也能做到相同的效果

var people = {
  name: "elaine",
  age: 26,
  sayName: (age) => {
    var sayAge = function (age) {
      this.age = age;
    };
    sayAge(age);
  },
};
people.sayName(5);
people.age; // 26
age; // 5

可答案却让我匪夷所思,箭头函数难道不应该把this指向它的上一层吗?这个我们在后面会解释

作为构造函数调用

JavaScript 支持面向对象式编程,与主流的面向对象式编程语言不同, JavaScript并没有类(Class)的概念,而是使用基于原型(prototype-base)的继承方式(ES6中的Class其实是原型继承的语法糖)。相应的,JavaScript中的构造函数也很特殊,如果不适用new调用,则和普通函数一样。作为又一项约定俗成的准则,构造函数以大写字母开头,提醒调用者使用正确的方式调用。如果调用正确,this绑定到新创建的对象上。

function People(name, age) {
  this.name = name;
  this.age = age;
}

使用 apply 或 call 调用

让我们再一次重申,在 JavaScript 中函数也是对象,对象则有方法,apply 和 call 就是函数对象的方法。这两个方法异常强大,他们允许切换函数执行的上下文环境(context),即 this 绑定的对象。很多 JavaScript 中的技巧以及类库都用到了该方法。让我们看一个具体的例子:

function People(name, age) {
    this.name = name;
    this.age = age;
    this.sayName = function(name, age) {
        this.name = name;
        this.age = age;
    }
}
var elaine = new People('elaine', 26);
var johan = {name: 'johan', age: 26};
elaine.sayName('elaine1', 261);
elaine.sayName.apply(johan, ['johan1', 261])
console.log(elaine.name) // elaine1;
console.log(elaine.age) // 261
console.log(johan) { name: "johan1", age: 261 }

在上面的例子中,我们使用构造函数生成了一个对象elaine,该对象同时具有 sayName 方法;使用对象字面量创建了另一个对象 johan,我们看到使用apply 可以将 elaine 上的方法应用到 johan 上,这时候 this 也被绑定到对象 johan 上,另一个 call 也具备相同的功能,不同的是最后的参数不是作为一个数组统一传入,而是分开传入的

elaine.sayName.call(johan, "johan1", 261);

回过头来看,apply 和 call 的语义就是 elaine 的方法 sayName 作用于 johan ,sayName 需要传入的参数,我从第二个参数开始传值;或者说 johan 调用 elaine 的 sayName 方法,从第二个参数开始传值

call和apply具有掰弯this指向的能力

箭头函数

与箭头函数相关的语法和特征我们会在ES6篇中着重描述,这里,我们只讲箭头函数与 this 的关系。在“作为函数调用”小节中我们使用箭头函数,试图让它绑定,但是却感觉错了

网上对箭头函数与this 关系的解释是:箭头函数会默认帮我们绑定外层 this 的值,所以在箭头函数中的this的值和外层的this是一样的。这个解释很困扰我,就好比你看高中政治课本,一谈到马克思主义思想浪潮时虽然文字都看的懂,但是连在一起却那么的神奇,让人疑惑不止。

其实箭头函数很简单,和我们之前说作用域时谈到的动态作用域和静态作用域(词法作用域)有关系。this本身的机制和动态作用域很像,而箭头函数的出现,某种程度上规避了JavaScript 的设计缺陷(正确的设计方式应该是内部函数的 this 应该绑定到其外层函数对应的对象上)

"use strict"; // 严格模式下
var people = {
  name: "eliane",
  age: 26,
  sayName: () => console.log(this.name, this),
  sayName2: function () {
    console.log(this.name, this);
  },
};
people.sayName(); // undefined Window {...}
people.sayName2(); // elaine, peole {...}

使用箭头函数后,就不用管调用者是谁,它只关心在哪里调用

var foo = {
  bar: {
    a: () => console.log(this),
  },
};
foo.bar.a(); // window

函数的执行环境

我们之前一直在讲一件事,this是如何被调用的,也说了this是什么,那么我们来看看,一个函数被执行时会发生什么?

一个函数被执行时,会创建一个执行环境(或叫活动记录,或叫执行上下文,英文名 ExecutionContext),函数所有的行为都发生在此执行环境中,构建该执行环境时,JavaScript 首先会创建 arguments 变量,其中包含调用函数时传入的参数。接下来创建作用域链。然后初始化变量,首先初始化函数的形参表,值为 arguments 变量中对应的值,如果 arguments 变量中没有对应值,则该形参初始化为 undefined。如果该函数中含有内部函数,则初始化这些内部函数。如果没有,继续初始化该函数内定义的局部变量,需要注意的是此时这些变量初始化为 undefined,其赋值操作在执行环境( ExecutionContext )创建成功后,函数执行时才会执行,这点对于我们理解JavaScript中的变量作用域非常重要。

最后是 this 变量赋值,如前所述,会根据函数调用方式的不同,赋给 this 全局对象,当前对象等。至此函数的执行环境( ExecutionContext )创建成功,函数开始逐行执行,所需变量均从之前构建好的执行环境( ExecutionContext )中读取

this有什么作用

全局执行上下文中:this 指向了 window 对象,方便我们来调用全局 window 对象

函数执行上下文中:this 指向了调用该函数的对象,减少的参数的传递,原来如何需要在函数内部操作被调用对象,当然还需要将对象作为参数传递进去,而又了 this,就不需要了,直接拿 this 就可以操作该调用对象的属性

结语

结语就留给后面的自己吧

构造函数就是个模式,this未来会指向new出来的对象。创建 Person 的实例时,this.name 将引用新创建的对象,并将一个名为 name 的属性放入新对象中。

this 其实很好理解,它就是一个代词,表示”这个“。

生活中遇到一些事物规律,我们归纳总结,得出结论,用一个名词代替这个规律,例如马太效应,墨菲定律,我们约定俗成,这个词就是表示这些意。这样一抽象,彼此信息消耗就减少了。this 其实很好理解,this 就是”这个“。

var foo = {
  value: 1,
};
function bar() {
  console.log(this.value);
}
bar();

调用函数bar,函数中的 this 就默认代指 window。window上没有value,那结果就是 undefined。

var foo = {
  value: 1,
};
function bar() {
  console.log(this.value);
}
bar.call(foo);

call 能硬核掰弯this指向,将this指向第一个参数,所以这段代码中,this 代指 foo , foo 上有value,所以打印结果是 1

针对 js 中的 this 指向问题,知乎上有人曾经回答过:

https://www.zhihu.com/question/412637481/answer/1539325572

  • this 的灵活指向,属于 JS 自己发明的语言
  • this 指向存在的问题是公认的
  • this 的这种设计既不利于代码可读性,也不利于性能优化,完全可对其世家强制性
  • this 设计问题的更远,是产品营销需求与设计师个人偏好之间的冲突

this 是万恶之源,大家都是(词法)静态作用域,就他是动态的.

参考资料

芝麻开门,显示全文!