OBKoro1

OBKoro1 查看完整档案

上海编辑  |  填写毕业院校  |  填写所在公司/组织 obkoro1.com 编辑
编辑

前端进阶积累、前端算法、开箱即用代码块:http://obkoro1.com/web_accumu...

个人动态

OBKoro1 发布了文章 · 1月14日

手摸手教你写一个命令行终端[electron实战]

前言

Electron很出名,很多人可能了解过,知道它是用来开发桌面端的应用,但是一直没有在项目中实践过,缺乏练手的实践项目。

很多开源的命令行终端都是使用Electron来开发的,本文将从零开始手把手的教大家用Electron写一个命令行终端。

作为一个完整的实战项目示例,该终端demo也将集成到Electron开源学习项目electron-playground中,目前这个项目拥有800+ Star⭐️,它最大的特点是所见即所得的演示Electron的各种特性,帮助大家快速学习、上手Electron

大家跟着本文一起来试试Electron吧~

终端效果

开源地址: electron-terminal-demo

giit提交代码演示

目录

  1. 初始化项目。
  2. 项目目录结构
  3. Electron启动入口index-创建窗口
  4. 进程通信类-processMessage。
  5. 窗口html页面-命令行面板
  6. 命令行面板做了哪些事情

    • 核心方法:child_process.spawn-执行命令行监听命令行的输出
    • stderr不能直接识别为命令行执行错误
    • 命令行终端执行命令保存输出信息的核心代码
    • html完整代码
    • 命令行终端的更多细节
  7. 下载试玩

    • 项目演示
    • 项目地址
    • 启动与调试
  8. 小结

初始化项目

npm init
npm install electron -D

如果Electron安装不上去,需要添加一个.npmrc文件,来修改Electron的安装地址,文件内容如下:

registry=https://registry.npm.taobao.org/
electron_mirror=https://npm.taobao.org/mirrors/electron/
chromedriver_cdnurl=https://npm.taobao.org/mirrors/chromedriver

修改一下package.json的入口mainscripts选项, 现在package.json长这样,很简洁:

{
  "name": "electron-terminal",
  "version": "1.0.0",
  "main": "./src/index.js",
  "scripts": {
    "start": "electron ."
  },
  "devDependencies": {
    "electron": "^11.1.1"
  }
}

项目目录结构

我们最终实现的项目将是下面这样子的,页面css文件不算的话,我们只需要实现src下面的三个文件即可。

.
├── .vscode // 使用vscode的调试功能启动项目
├── node_dodules
├── src
│   ├── index.js // Electron启动入口-创建窗口
│   └── processMessage.js // 主进程和渲染进程通信类-进程通信、监听时间
│   └── index.html // 窗口html页面-命令行面板、执行命令并监听输出
│   └── index.css // 窗口html的css样式 这部分不写
├── package.json
└── .npmrc // 修改npm安装包的地址
└── .gitignore

Electron启动入口index-创建窗口

  1. 创建窗口, 赋予窗口直接使用node的能力。
  2. 窗口加载本地html页面
  3. 加载主线程和渲染进程通信逻辑
// ./src/index.js
const { app, BrowserWindow } = require('electron')
const processMessage = require('./processMessage')

// 创建窗口
function createWindow() {
  // 创建窗口
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: true, // 页面直接使用node的能力 用于引入node模块 执行命令
    },
  })
  // 加载本地页面
  win.loadFile('./src/index.html')
  win.webContents.openDevTools() // 打开控制台
  // 主线程和渲染进程通信
  const ProcessMessage = new processMessage(win)
  ProcessMessage.init()
}

// app ready 创建窗口
app.whenReady().then(createWindow)

进程通信类-processMessage

electron分为主进程和渲染进程,因为进程不同,在各种事件发生的对应时机需要相互通知来执行一些功能。

这个类就是用于它们之间的通信的,electron通信这部分封装的很简洁了,照着用就可以了。

// ./src/processMessage.js
const { ipcMain } = require('electron')
class ProcessMessage {
  /**
   * 进程通信
   * @param {*} win 创建的窗口
   */
  constructor(win) {
    this.win = win
  }
  init() {
    this.watch()
    this.on()
  }
  // 监听渲染进程事件通信
  watch() {
    // 页面准备好了
    ipcMain.on('page-ready', () => {
      this.sendFocus()
    })
  }
  // 监听窗口、app、等模块的事件
  on() {
    // 监听窗口是否聚焦
    this.win.on('focus', () => {
      this.sendFocus(true)
    })
    this.win.on('blur', () => {
      this.sendFocus(false)
    })
  }
  /**
   * 窗口聚焦事件发送
   * @param {*} isActive 是否聚焦
   */
  sendFocus(isActive) {
    // 主线程发送事件给窗口
    this.win.webContents.send('win-focus', isActive)
  }
}
module.exports = ProcessMessage

窗口html页面-命令行面板

在创建窗口的时候,我们赋予了窗口使用node的能力, 可以在html中直接使用node模块。

所以我们不需要通过进程通信的方式来执行命令和渲染输出,可以直接在一个文件里面完成。

终端的核心在于执行命令,渲染命令行输出,保存命令行的输出

这些都在这个文件里面实现了,代码行数不到250行。

命令行面板做了哪些事情

  • 页面: 引入vue、element,css文件来处理页面
  • template模板-渲染当前命令行执行的输出以及历史命令行的执行输出
  • 核心:执行命令监听命令行输出

    • 执行命令并监听执行命令的输出,同步渲染输出。
    • 执行完毕,保存命令行输出的信息。
    • 渲染历史命令行输出。
    • 对一些命令进行特殊处理,比如下面的细节处理。
  • 围绕执行命令行的细节处理

    • 识别cd,根据系统保存cd路径
    • 识别clear清空所有输出。
    • 执行成功与失败的箭头图标展示。
    • 聚焦窗口,聚焦输入。
    • 命令执行完毕滚动底部。
    • 等等细节。

核心方法:child_process.spawn-执行命令行监听命令行的输出

child_process.spawn介绍

spawn是node子进程模块child_process提供的一个异步方法。

它的作用是执行命令并且可以实时监听命令行执行的输出

当我第一次知道这个API的时候,我就感觉这个方法简直是为命令行终端量身定做的。

终端的核心也是执行命令行,并且实时输出命令行执行期间的信息。

下面就来看看它的使用方式。

使用方式

const { spawn } = require('child_process');
const ls = spawn('ls', {
  encoding: 'utf8',
  cwd: process.cwd(), // 执行命令路径
  shell: true, // 使用shell命令
})

// 监听标准输出
ls.stdout.on('data', (data) => {
  console.log(`stdout: ${data}`);
});

// 监听标准错误
ls.stderr.on('data', (data) => {
  console.error(`stderr: ${data}`);
});

// 子进程关闭事件
ls.on('close', (code) => {
  console.log(`子进程退出,退出码 ${code}`);
});

api的使用很简单,但是终端信息的输出,需要很多细节的处理,比如下面这个。

stderr不能直接识别为命令行执行错误

stderr虽然是标准错误输出,但里面的信息不全是错误的信息,不同的工具会有不同的处理。

对于git来说,有很多命令行操作的输出信息都输出在stederr上。

比如git clonegit push等,信息输出在stederr中,我们不能将其视为错误。

git总是将详细的状态信息和进度报告,以及只读信息,发送给stederr

具体细节可以查看git stderr(错误流)探秘等资料。

暂时还不清楚其他工具/命令行也有没有类似的操作,但是很明显我们不能将stederr的信息视为错误的信息。

PS: 对于git如果想提供更好的支持,需要根据不同的git命令进行特殊处理,比如对下面clear命令和cd命令的特殊处理。

根据子进程close事件判断命令行是否执行成功

我们应该检测close事件的退出码code, 如果code为0则表示命令行执行成功,否则即为失败。

命令行终端执行命令保存输出信息的核心代码

下面这段是命令行面板的核心代码,我贴一下大家重点看一下,

其他部分都是一些细节、优化体验、状态处理这样的代码,下面会将完整的html贴上来。

const { spawn } = require('child_process') // 使用node child_process模块
// 执行命令行
actionCommand() {
  // 处理command命令 
  const command = this.command.trim()
  this.isClear(command)
  if (this.command === '') return
  // 执行命令行
  this.action = true
  this.handleCommand = this.cdCommand(command)
  const ls = spawn(this.handleCommand, {
    encoding: 'utf8',
    cwd: this.path, // 执行命令路径
    shell: true, // 使用shell命令
  })
  // 监听命令行执行过程的输出
  ls.stdout.on('data', (data) => {
    const value = data.toString().trim()
    this.commandMsg.push(value)
    console.log(`stdout: ${value}`)
  })

  ls.stderr.on('data', this.stderrMsgHandle)
  ls.on('close', this.closeCommandAction)
},
// 错误或详细状态进度报告 比如 git push
stderrMsgHandle(data) {
  console.log(`stderr: ${data}`)
  this.commandMsg.push(`stderr: ${data}`)
},
// 执行完毕 保存信息 更新状态
closeCommandAction(code) {
  // 保存执行信息
  this.commandArr.push({
    code, // 是否执行成功
    path: this.path, // 执行路径
    command: this.command, // 执行命令
    commandMsg: this.commandMsg.join('\r'), // 执行信息
  })
  // 清空
  this.updatePath(this.handleCommand, code)
  this.commandFinish()
  console.log(
    `子进程退出,退出码 ${code}, 运行${code === 0 ? '成功' : '失败'}`
  )
}

html完整代码

这里是html的完整代码,代码中有详细注释,建议根据上面的命令行面板做了哪些事情,来阅读源码。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>极简electron终端</title>
    <link
      rel="stylesheet"
      href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"
    />
    <script data-original="https://unpkg.com/vue"></script>
    <!-- 引入element -->
    <script data-original="https://unpkg.com/element-ui/lib/index.js"></script>
    <!-- css -->
    <link rel="stylesheet" href="./index.css" />
  </head>
  <body>
    <div id="app">
      <div class="main-class">
        <!-- 渲染过往的命令行 -->
        <div v-for="item in commandArr">
          <div class="command-action">
            <!-- 执行成功或者失败图标切换 -->
            <i
              :class="['el-icon-right', 'command-action-icon', { 'error-icon': item.code !== 0  }]"
            ></i>
            <!-- 过往执行地址和命令行、信息 -->
            <span class="command-action-path">{{ item.path }} $</span>
            <span class="command-action-contenteditable"
              >{{ item.command }}</span
            >
          </div>
          <div class="output-command">{{ item.commandMsg }}</div>
        </div>
        <!-- 当前输入的命令行 -->
        <div
          class="command-action command-action-editor"
          @mouseup="timeoutFocusInput"
        >
          <i class="el-icon-right command-action-icon"></i>
          <!-- 执行地址 -->
          <span class="command-action-path">{{ path }} $</span>
          <!-- 命令行输入 -->
          <span
            :contenteditable="action ? false : 'plaintext-only'"
            class="command-action-contenteditable"
            @input="onDivInput($event)"
            @keydown="keyFn"
          ></span>
        </div>
        <!-- 当前命令行输出 -->
        <div class="output-command">
          <div v-for="item in commandMsg">{{item}}</div>
        </div>
      </div>
    </div>

    <script>
      const { ipcRenderer } = require('electron')
      const { spawn } = require('child_process')
      const path = require('path')

      var app = new Vue({
        el: '#app',
        data: {
          path: '', // 命令行目录
          command: '', // 用户输入命令
          handleCommand: '', // 经过处理的用户命令 比如清除首尾空格、添加获取路径的命令
          commandMsg: [], // 当前命令信息
          commandArr: [], // 过往命令行输出保存
          isActive: true, // 终端是否聚焦
          action: false, // 是否正在执行命令
          inputDom: null, // 输入框dom
          addPath: '', // 不同系统 获取路径的命令 mac是pwd window是chdir
        },
        mounted() {
          this.addGetPath()
          this.inputDom = document.querySelector(
            '.command-action-contenteditable'
          )
          this.path = process.cwd() // 初始化路径
          this.watchFocus()
          ipcRenderer.send('page-ready') // 告诉主进程页面准备好了
        },
        methods: {
          // 回车执行命令
          keyFn(e) {
            if (e.keyCode == 13) {
              this.actionCommand()
              e.preventDefault()
            }
          },
          // 执行命令
          actionCommand() {
            const command = this.command.trim()
            this.isClear(command)
            if (this.command === '') return
            this.action = true
            this.handleCommand = this.cdCommand(command)
            const ls = spawn(this.handleCommand, {
              encoding: 'utf8',
              cwd: this.path, // 执行命令路径
              shell: true, // 使用shell命令
            })
            // 监听命令行执行过程的输出
            ls.stdout.on('data', (data) => {
              const value = data.toString().trim()
              this.commandMsg.push(value)
              console.log(`stdout: ${value}`)
            })
            // 错误或详细状态进度报告 比如 git push、 git clone 
            ls.stderr.on('data', (data) => {
              const value = data.toString().trim()
              this.commandMsg.push(`stderr: ${data}`)
              console.log(`stderr: ${data}`)
            })
            // 子进程关闭事件 保存信息 更新状态
            ls.on('close', this.closeCommandAction) 
          },
          // 执行完毕 保存信息 更新状态
          closeCommandAction(code) {
            // 保存执行信息
            this.commandArr.push({
              code, // 是否执行成功
              path: this.path, // 执行路径
              command: this.command, // 执行命令
              commandMsg: this.commandMsg.join('\r'), // 执行信息
            })
            // 清空
            this.updatePath(this.handleCommand, code)
            this.commandFinish()
            console.log(
              `子进程退出,退出码 ${code}, 运行${code === 0 ? '成功' : '失败'}`
            )
          },
          // cd命令处理
          cdCommand(command) {
            let pathCommand = ''
            if (this.command.startsWith('cd ')) {
              pathCommand = this.addPath
            } else if (this.command.indexOf(' cd ') !== -1) {
              pathCommand = this.addPath
            }
            return command + pathCommand
            // 目录自动联想...等很多细节功能 可以做但没必要2
          },
          // 清空历史
          isClear(command) {
            if (command === 'clear') {
              this.commandArr = []
              this.commandFinish()
            }
          },
          // 获取不同系统下的路径
          addGetPath() {
            const systemName = getOsInfo()
            if (systemName === 'Mac') {
              this.addPath = ' && pwd'
            } else if (systemName === 'Windows') {
              this.addPath = ' && chdir'
            }
          },
          // 命令执行完毕 重置参数
          commandFinish() {
            this.commandMsg = []
            this.command = ''
            this.inputDom.textContent = ''
            this.action = false
            // 激活编辑器
            this.$nextTick(() => {
              this.focusInput()
              this.scrollBottom()
            })
          },
          // 判断命令是否添加过addPath
          updatePath(command, code) {
            if (code !== 0) return
            const isPathChange = command.indexOf(this.addPath) !== -1
            if (isPathChange) {
              this.path = this.commandMsg[this.commandMsg.length - 1]
            }
          },
          // 保存输入的命令行
          onDivInput(e) {
            this.command = e.target.textContent
          },
          // 点击div
          timeoutFocusInput() {
            setTimeout(() => {
              this.focusInput()
            }, 200)
          },
          // 聚焦输入
          focusInput() {
            this.inputDom.focus() //解决ff不获取焦点无法定位问题
            var range = window.getSelection() //创建range
            range.selectAllChildren(this.inputDom) //range 选择obj下所有子内容
            range.collapseToEnd() //光标移至最后
            this.inputDom.focus()
          },
          // 滚动到底部
          scrollBottom() {
            let dom = document.querySelector('#app')
            dom.scrollTop = dom.scrollHeight // 滚动高度
            dom = null
          },
          // 监听窗口聚焦、失焦
          watchFocus() {
            ipcRenderer.on('win-focus', (event, message) => {
              this.isActive = message
              if (message) {
                this.focusInput()
              }
            })
          },
        },
      })

      // 获取操作系统信息
      function getOsInfo() {
        var userAgent = navigator.userAgent.toLowerCase()
        var name = 'Unknown'
        if (userAgent.indexOf('win') > -1) {
          name = 'Windows'
        } else if (userAgent.indexOf('iphone') > -1) {
          name = 'iPhone'
        } else if (userAgent.indexOf('mac') > -1) {
          name = 'Mac'
        } else if (
          userAgent.indexOf('x11') > -1 ||
          userAgent.indexOf('unix') > -1 ||
          userAgent.indexOf('sunname') > -1 ||
          userAgent.indexOf('bsd') > -1
        ) {
          name = 'Unix'
        } else if (userAgent.indexOf('linux') > -1) {
          if (userAgent.indexOf('android') > -1) {
            name = 'Android'
          } else {
            name = 'Linux'
          }
        }
        return name
      }
    </script>
  </body>
</html>

以上就是整个项目的代码实现,总共只有三个文件。

更多细节

本项目终究是一个简单的demo,如果想要做成一个完整的开源项目,还需要补充很多细节。

还会有各种各样奇奇怪怪的需求和需要定制的地方,比如下面这些:

  • command+c终止命令
  • cd目录自动补全
  • 命令保存上下键滑动
  • git等常用功能单独特殊处理。
  • 输出信息颜色变化
  • 等等

下载试玩

即使这个终端demo的代码量很少,注释足够详细,但还是需要上手体验一下一个Electron项目运行的细节。

项目演示

clear命令演示

实际上就是将历史命令行输出的数组重置为空数组。

执行失败箭头切换

根据子进程close事件,判断执行是否成功,切换一下图标。

cd命令

识别cd命令,根据系统添加获取路径(pwd/chdir)的命令,再将获取到的路径,更改为最终路径。

giit提交代码演示

项目地址

开源地址: electron-terminal-demo

启动与调试

安装

npm install

启动

  1. 通过vscode的调试运行项目,这种形式可以直接在VSCode中进行debugger调试。

  2. 如果不是使用vscode编辑器, 也可以通过使用命令行启动。
npm run start

小结

命令行终端的实现原理就是这样啦,强烈推荐各位下载体验一下这个项目,最好单步调试一下,这样会更熟悉Electron

项目idea诞生于我们团队开源的另一个开源项目:electron-playground, 目的是为了让小伙伴学习electron实战项目。

electron-playground是用来帮助前端小伙伴们更好、更快的学习和理解前端桌面端技术Electron, 尽量少走弯路。

它通过如下方式让我们快速学习electron。

  1. 带有gif示例和可操作的demo的教程文章。
  2. 系统性的整理了Electron相关的api和功能。
  3. 搭配演练场,自己动手尝试electron的各种特性。

前端进阶积累公众号GitHub、wx:OBkoro1、邮箱:obkoro1@foxmail.com

以上2021/01/12

查看原文

赞 6 收藏 3 评论 0

OBKoro1 发布了文章 · 2020-12-10

OBKoro1的2020年年终总结-人生是一场马拉松

前言

一晃眼2020年马上就要过去了,今年感觉过的特别快。

工作已经三年了,之前都没有写过年终总结,结果造成了下面这个现象:

回首过去的几年,记忆已经很模糊了,需要很用力才能想起过去一部分往事

人生百年,好像也没有多少年终总结可以写呢~

这么激励一下,一下子就有动力写年终总结了 😝

工作

在家办公

年初的疫情,是2020年过不去的记忆~

待到山花烂漫时,我们再相见

当时疫情比较严重,全国封闭,公司很快就宣布在家办公,并且特地说明了不用担心隔离14天赶不上上班~

对于当时还惶惶不安的朋友来说,着实感受到了公司所给予的温暖❤️

在家办公

在家办公的感受

从大年初九开始在家远程办公,疫情对线上教育行业也是一个机会,周末也没有休息,总共在家办公两个月。

后面直到疫情已经很稳定了,在四月一号那天才回的上海,然后隔离一周多,就恢复正常的上班了。

在家办公的优势

效率高:

在家办公的时候,家里人都不会来打扰我,可以很专心,效率非常高。

三餐都不用 操心

一日三餐我老妈都弄好了,都不用操心,我妈也都不会唠叨我,非常幸福^_^~

空间大

家里是自己盖的房子, 拿一间来做书房,空间比较大, 不会感到压抑,累了就在家里走走跳跳,活动一下身体。

可以陪伴家人

偶尔在晚上的时候会跟家人一起打打牌,输赢几十块钱的那种,也很开心~

过年的时候买了一个羽毛球网,下午或者晚上(开灯),也可以跟家里人在院子里打羽毛球。

又可以锻炼身体又可以陪伴家人,非常nice!

弊端: 工作和生活没有界限

因为全体人员都在家里办公,每个人的作息时间、生活习惯都不一样。

有些人很早就起床开始办公,有些人很晚才起床,但是晚上很晚才休息。

这时候就会出现,别人在他的办公时间来找你语音,影响了你的休息时间。

你可能会感到任何时间都有可能有人来找你,没什么事不敢离开工作岗位~

总的来说,在家里办公还是非常愉快的,只要完成你的工作内容就可以了,其他时间可以自己调节。

疫情封闭期间科比意外逝世

退役的时候你曾经说过,如果再过18年你人生的成就还只是篮球的话,那么你该有多失败?但是我们永远猜不到明天和意外谁先来💔

科比逝世对我个人来说是一件非常重大的事情

科比一直是我的精神偶像,他身上的专注与不服输的精神深深的影响我,激励我前行

一觉醒来同学突然在群里艾特我,说科比去世了,还以为是在开玩笑,好端端的,没灾没病的怎么会呢?

当我打开微博求证之后,情绪一下子崩溃了,泪奔, 花了很长时间才接受这个事实😭

写到这里,联想到多灾多难的2020,属实有点难过~

实不相瞒,我从来不信鬼神之说,但是如果有上帝的话

希望他2021年能对这个世界能够温柔一点,谢谢❤️

electron教程开源项目

今年做了几个关于electron的项目,公司恰好有开源的计划,后面就拨出几个人员来做开源项目。

经过几个版本的推到重来与迭代,最后沉淀出了一个关于electron的教程项目-electron-playground

这是一个类似于 store book的项目,通过尝试electron各种特性,使electron的各项特性所见即所得, 来达到我们快速上手和学习electron的目的。

将来如果需要学习electron,或者通过electron做项目,一定要看一下这个项目,可以通过这个项目来入门electron、搭建electron、做一些工程化方面的内容。

前两天写了一篇推广文章,感兴趣的朋友可以看一下这篇博客了解一下这个项目。

公司被好未来收购

今年公司有点动荡,公司因为对赌失败被好未来收购了,创始人出局。

一整年都在融合好未来的制度,我意外的成为了“大厂”人 😝

PPT能力

被收购之后,过了一段时间,要求所有人员进行一次定级,需要所有人讲20分钟左右的PPT。

大厂在晋升和述职的时候都会要求演讲者准备PPT,PPT能力特别重要,如何将自己的东西讲的高大上一点!

没有写过PPT的同学,建议可以自己准备一个晋升PPT模拟一下~

PPT经验

  • PPT字不要太多,准备一些关键词提示一下思路即可。

    字写得密密麻麻非常减分,PPT应该是一个类似大纲的东西。

  • 写完PPT之后,自己对着镜子演练几遍,
    这样会提高熟练度,讲起来不会磕磕巴巴的,并且能找出一些问题。
  • 平日做业务多思考,多承担职责这样在写PPT的时候就不会没东西写了。

    可以对标大公司,大公司有哪些东西我们没有哪些东西,哪些东西能给公司带来什么东西。

蠢事

第一次讲PPT没有经验,我干了一件特别蠢的事情,在这里跟大家分享一下,希望大家不要踩到这个坑:

因为平时做的业务比较杂,项目比较多,不知道要讲哪一块

后面把自己在开源社区上的成就,当成PPT的主要内容。

当时因为是自己熟悉和喜欢的内容,讲的特别流畅和自信,还以为自己肯定没问题。

结果后面评委因为我主要讲开源上的东西,工作上的内容不足,认为我本末倒置了 o(╥﹏╥)o,结果不太理想

融合与动荡

裁员

经历了所有人员定级之后,公司也根据定级表现优化了一部分同事。

因为赔偿N+1给够了,大家也没有什么怨言,好聚好散。

后面听说有的同事是自己申请的名额,工作的累了,自己想休息下,这招学到了~

实线与虚线管理

团队由实线管理,前端leader直属管理,要转变为虚线管理。

简单的说现在就是跟项目,一个人从头到尾做一个项目,以前一个人做很多个项目,这边做一期那边做一期。

跟项目的好处在于:做项目的人对项目熟悉,项目也不会太乱,职责也比较清晰。

技术

博客

有一些粉丝在微信上,以及一些朋友都会说我今年博客写的少了。

今年前中期的时候,在工作、学习上有点迷茫、懈怠。

后面因为投入大量时间在跑步、健身上。

在学习、维护开源项目、写博客、生活、lol等事项上的时间管理做的不是很好。

突然培养了一个耗时间的习惯是这样的。

现在我在时间管理上已经调整好了,学习目标也已经找到了,每天很忙但很充实。

还有一点是我写博客比较磨蹭慢,一篇博客需要三五天的时间才能完成。

因为以上诸多借口原因,今年的输出少了一些。

此情此景,我只能说下次一定😝

要写的东西是很多,我都记在笔记中,明年争取多输出一些篇高质量的博客 💪

深入学习webpack

近期在深入学习webpack,包括各种环境的配置,plugin、loader、项目优化。

也了解了webpack的编译流程,tapable如何通过发布订阅动态生成代码,

从webpack入口配置开始是如何运行的,模块的编译过程、chunk的生成过程,

下面准备自己手写一个webpack,以及再深入一下webpack的tapable机制以及其他细节。

学习方法:

这里分享一下我朋友(易全文)跟我说的学习方法,我觉得特别有效:

学习的时候不要分散精力,集中所有精力攻破一个方向的所有内容,彻底学会、学精

webpack我就是采取这种方式学习,正在进行中, 感觉真的掌握了这个知识。

autoCommit

autoCommit是我年初的时候开源的一款插件,它是用来刷首页Github commit记录的。

它可以刷过去几年以及未来的commit, 一键帮你把github首页的绿色格子填满

有兴趣的同学可以点击autoCommit来了解它,觉得项目还不错的话,就给我点个Star⭐️)吧~

技术无罪

当时以为会大火,结果反响也就一般,还有些争议,有人质疑我为了commit而commit~

我的观点:技术无罪,每个人用工具的方式和目的都不一样

  1. 坚持了很久的commit,不小心断更的commit记录, 可以用autoCommit补一下记录。
  2. 规划一下github首页commit记录的图案,通过autoCommit在绿色格子里面画出有创意的图形~

如果github什么有价值的东西都没有,就算把绿色格子都刷满了,那也不能代表什么

不喜欢这个工具的人可以不用,没人强迫你必须要用。

emmmm, 千金难买我乐意,写到这里越想越气,我把Github的commit记录刷了一波

koroFileHeader

这是我开源的另外一款插件,目前插件已经维护两年半了, 更新了50+版本,关闭200多个issue~

今年比较开心的一件事是koroFileHeader头部注释插件Github仓库Star数量突破了2000,啦啦啦~

今年更新了两个比较重要的功能:

  1. 一键添加注释图案的功能

  1. 函数参数自动提取功能

关于生活

健身

这块是我今年来最大的收获,记录了一下我减肥和健身的心理路程,写的比较长~

被肥胖速度刺激到了

过年回到家里,不出意料,所有人都说我胖了。

去上海后,呆了一个月,就又回去了一下。家里所有人都说我胖了,那时候163斤应该是有的。

我胖起来的原因其实自己也知道就是天天吃夜宵+很少运动,

这个事情一下子就给我刺激到了, 以前一直不以为然,我的天,我才出去一个月就又胖了😱

而且胖的那么明显,再这样下去可怎么办?当时在老家我就开始跑步减肥之旅了..

跑步

不适应-痛苦

今年五月份的时候我只能跑两三公里,并且一公里才配速8分半。

因为太久没有运动了,身体不适应,跑完一次浑身酸痛的要死。

走路都要慢慢走,走一步痛一次,上下楼梯更是痛的要死。

每次需要休息大概四天才缓过来,这个阶段持续了一个月跑了8次。

跟大佬一起锻炼

六月下旬公司健身五六年的大佬(全文)带我一起跑步锻炼,有人一起锻炼,积极性也会提高很多。

每天到下午六点钟的时候就会互相提醒去锻炼,如果不去就是一顿嘲讽,哈哈哈

我用了一个月时间跑了把配速练到七分钟,距离也可以跑到五公里了,跑了20次。

又花了一个月把配速练到6分10秒左右(20次),花了三个月终于达到正常的配速。

办健身卡

在我跑步两个月后(8月份), 大佬带我去体验了一周的健身房。

体验过后经过一番讨价还价,以三年6K的价格办了威尔士的全国通用卡。

哼 说实话过了四个月,我还是觉得他肯定吃了我办卡的回扣,奈何没有抓到证据😕

如果有想锻炼的同学可以先办一个季卡,不要办太久 万一不能坚持就浪费了~

现在我一个月大概去健身房15次到22次之间,每次锻炼一个半小时左右。

今年室外和室内总共跑了六个月105次,800公里✌️

有氧运动超级解压

我最喜欢每次跑完步大汗淋漓的感觉,特别爽,在健身房洗过澡。

走在回家的路上,即使你刚刚加过班,也会感觉整个人非常轻松。

赶走身上所有疲惫,超级超级超级解压!

跑步经验

我现在跑步机10公里能跑46分钟,最后三四公里都是4分半左右的配速 ✌️

设备: 运动手表

一开始跑步我是把手机拿在手上,后面买了腰包,但是跑步的时候都不舒服。

大佬教我买个设备检测一下自己的心率,可以根据心率判断一下自己的状态。

后来我买的是华为手表,跑步的时候不用带手机,跑完再同步跑步数据到keep。

根据心率判断自己的状态是否极限,以及查看目前的配速,距离。

如何提高跑步配速

我个人建议就是不断的打破自己的舒适区,不断挑战更高配速!

可以采取大佬教我的变速跑,比如12公里配速跑400米再10公里配速跑200米,这样交替进行跑8组。

这种形式锻炼心肺能力非常好,心率一直会保持比较高的速度。

前面几次会非常辛苦,到后面就适应了。

我提高配速都是以这种形式,现在我可以用13公里的配速跑三四公里不休息

健身餐

三分练,七分吃,我感觉减肥就是消耗卡路里大于摄入卡路里就自然会瘦下来。

不要以为锻炼了就自然会瘦下来,我一开始锻炼吃的没有控制,减肥也是没有效果的。

我看到其他大佬有在吃健身餐,我也跟着吃了三个月(7.15-10.16)

最疯狂的时期经常:早上不吃,一天只吃中午一顿健身餐,晚上回家吃脱脂面包

现在再也不想吃健身餐了😭, 我减肥很野蛮不够科学,经常被大佬骂~

效果

锻炼两个月之后,在八月份中旬我从163已经减重到150以下了。

目前稳定在143左右,但是减不到140以下。

增加肌肉含量,增加消耗

大佬说我肌肉含量不够,每天身体自然消耗不够大。

下面我可能要吃一下蛋白粉,练练肌肉,增加消耗,不然减不下去。

增加肌肉之后,以后也不会轻易反弹,因为每天不运动消耗也比较大。

健身习惯一辈子的事情

健身感受

在我看来我今年最大的收获就是培养了健身的习惯

无论我以后走到哪里我都会找一个健身房定期的去锻炼身体

真心特别感谢大佬全文带我锻炼身体, 我也培养了一辈子的好习惯 ❤️

一开始身体不适应运动,很痛苦,很勉强,为了减肥后面还是撑了过来

但现在我在健身和跑步中我收获了快乐,一点都不勉强

只要有空都会积极的去健身,因为我知道对我的身体好,而且很也很快乐

通过健身我感觉我的身体也充满了活力,精神状态也好了很多,整个人积极向上多了。

健身例子对比

我们这个行业每天久坐,普遍缺乏锻炼,很多人还有熬夜或者其他不良的习惯。

95年的同事

有一个跟我玩的非常好的朋友,跟我一样95年的

平时基本不运动,人也比较胖,25岁都有脂肪肝和高血压了

肩颈和背部都不太好(职业病 我也是) 等其他问题

32岁的大佬

反观带我健身的大佬,今年32岁,以前一开始见的时候还以为他只是比我大两三岁的样子

健身四五年后,他现在身体状态和精神状态都非常好,活力满满。

大佬说他的同学,一个个的都是中年大叔,跟他们站在一起好像两个时代的人一样。

有一个说法是:什么时候开始锻炼,你的年龄就停留在什么时候。

坚持运动健身,稳赚不赔

上面通过两个例子的对比,大家可以感受一下健身带来的好处,希望大家也可以尽快的行动起来~

旅行散心

今年跟我基友yeyan1996一样,出去玩了几次~

可能工作久了,总想散散心,一有机会就想在周边城市玩一下

没有机会,也要请一两天假凑成一个小长假出去转转,下周飞重庆😝

今年自驾游去了这几个地方:安徽黄山、福建平潭、南京、杭州

墙裂推荐自驾游,同学开车,到地方下车玩就可以了什么都不用操心😝

福建平潭

基金理财

今年我也入了理财的坑,钱放着是行不通的

起码也要做点投资吧,不然怎么跑得赢通货膨胀,要让钱生钱

后面请教了一些同事,开始玩支付宝的基金,今年的收益率24.8% ✌️

为了缓冲一下风险,买了一个银行定期,定期是稳赚的,不过3.5%-4%实在是太低了

后面嫌赚的太慢了,但是银行定期不让我取出来 😭

看书

今年看了几本书:图解http、系统之美、老人与海、三体、瓦尔登湖

这里推荐一下三体,这本小说是国内第一科幻,还获得了雨果奖,经常在知乎、各种场合看到别人在讨论这本书。

里面的设定、脑洞、对人性的深刻描写,超级精彩,总之,看了就是赚!

后记

年终总结写的比较琐碎、比较细,全文5000多字,感谢耐心看完。

写完总结,顿感今年要结束了

来年的计划已经做好了,明年应该会是收获的一年。

2021冲冲冲,最后送一段鸡汤给大家。

人生是一场马拉松

这一段给大家分享一下我的人生观,希望对诸位有所启发。

种一棵树最好的时间是十年前,其次就是在现在了

这是我的人生格言。

人生是一场马拉松,没有终点,不要在意自己能跑多远。

只要一直种树,一直在路上,就不会太差。

在路上的过程中,也要记得欣赏

生活还是要快乐最重要,我一向不主张给自己上太多的限制。

有什么事情是必须做的? 房子?车子?

佛系一点,享受生活不一定需要这些。

培养好学习习惯,锻炼习惯,自然而然的向前跑就好了。

最后衷心祝愿大家过得开心,自在!❤️

查看原文

赞 6 收藏 0 评论 1

OBKoro1 发布了文章 · 2020-12-04

Electron桌面端所见即所得-electron练习操场,快速上手electron

image

突然让你开发Electron应用,你能hold住吗?

如果领导突然说需要开发一款前端桌面端应用,那么对于我们前端er来说选择Electron是一件顺理成章的事情。但事实上很多同学对于Electron都不太了解和熟悉。

如果突然让我们去开发Electron应用,很多人都会陷入迷茫和懵逼的状态。然后在依靠网上相对较少的资料,慢慢摸索、一路踩坑的完成Electronn的需求。

为了解决上述问题,我们完成了一个项目,并把它开源了出来, 希望能够对大家学习Electron有点帮助。

快速学习和上手Electron: electron-playground

electron-playground是我司(好未来集团晓黑板)前端团队近期开源的项目。

electron-playground项目的目的

帮助前端仔更好、更快的学习和理解前端桌面端技术Electron, 少走弯路

electron-playrgound能为我学习Electron做什么

  1. 带有gif示例和可操作的demo的教程文章。
  2. 系统性的整理了Electron相关的api和功能。
  3. 搭配演练场,自己动手尝试electron的各种特性。

下面我来具体介绍一下项目的内容。

项目演示

1. 带有gif示例和可操作的demo文章讲解

项目搭配一系列教程文章,这些文章都是经过踩坑验证、成体系化的内容,并且带有gif示例,和可操作的demo示例、流程图等内容。

项目自带的gif演示

menu: 添加菜单

项目demo操作的gif演示

dialog: 消息提示与确认

dialog: 选择文件

流程图

窗口管理-创建和管理窗口

系统性的整理了Electron相关的api和功能

electronn-playground系统性的整理了Electron的相关API和功能,以及关于工程化相关的内容。

electron-playground列表分类

  • 工程化

    • 崩溃分析和收集
    • 开发调试
    • 打包问题
    • 应用更新
  • 应用

    • 自定义协议
    • 系统提示和文件选择
    • 菜单
    • 系统托盘
    • 文件下载
  • 窗口管理

    • 创建和管理窗口
    • 隐藏和恢复
    • 聚焦、失焦
    • 全屏、最大化、最小化
    • 窗口通信
    • 窗口类型
    • 窗口事件
  • 其他

    • 安全性

electron-playground列表分类截图

演练场

想要实现更复杂的操作,我们参考fiddle创建了演练场。

演练场集成了vscode的web端编辑库:monaco-editor,编码体验接近vscode。

如何启动

electron-playground启动流程如下:

git clone https://github.com/tal-tech/electron-playground.git // 下载项目
npm install // 安装依赖
npm run start // 启动项目

欢迎下载学习/体验

electron-playground是一个通过尝试electron各种特性,使electron的各项特性所见即所得, 来达到我们快速上手和学习electron的目的。

感兴趣的同学可以下载一下项目,体验一下,希望通过这个项目可以帮助大家更好、更快的学习和理解前端桌面端技术Electron, 少走弯路

如果觉得还不错的话,就给个Star⭐️ 鼓励一下我们吧^_^~

查看原文

赞 21 收藏 12 评论 2

OBKoro1 发布了文章 · 2020-05-20

一个可以一键添加佛祖保佑永无BUG、神兽护体等注释的工具

很早之前就见过各种佛祖保佑永无BUG神兽护体等形式的注释,感觉很有趣,蛮骚的😉。

然后最近有人在我开源的VSCode插件:koroFileHeader里面给我提issue,希望能够支持这种类型的注释。

现在开发完成了,大家可以根据下面的使用方式试用一下。

koroFileHeader插件简介

  1. 这个插件目前维护两年多了,有1300+Star,支持所有主流语言,支持自定义语言(不支持的语言可以自行设置)。
  2. 作用: 在文件开头添加注释,记录文件信息/文件的传参/出参等,让人对文件的功能一目了然。
  3. 如果觉得还不错的话,就给我点个Star⭐️吧~

插件示例:

example.gif

一键添加佛祖保佑永无BUG、神兽护体等注释图案

使用方式

  1. 在VSCode插件市场下载安装koroFileHeader
  2. 通过快捷键shift+command+p 输入注释图案/codeDesign,就可以选择注释图案了, 如下图所示。

注释图案GIF示例

支持各种语言的注释

注释图案不仅支持目前世面主流的注释形式,还支持自定义语言的注释形式。url

注释图案和头部注释结合

"fileheader.configObj": {
    "designAddHead": false // 默认关闭
}

设为true效果如下:

/*
 *                        .::::.
 *                      .::::::::.
 *                     :::::::::::
 *                  ..:::::::::::'
 *               '::::::::::::'
 *                 .::::::::::
 *            '::::::::::::::..
 *                 ..::::::::::::.
 *               ``::::::::::::::::
 *                ::::``:::::::::'        .:::.
 *               ::::'   ':::::'       .::::::::.
 *             .::::'      ::::     .:::::::'::::.
 *            .:::'       :::::  .:::::::::' ':::::.
 *           .::'        :::::.:::::::::'      ':::::.
 *          .::'         ::::::::::::::'         ``::::.
 *      ...:::           ::::::::::::'              ``::.
 *     ````':.          ':::::::::'                  ::::..
 *                        '.:::::'                    ':'````..
 * 
 * Author       : OBKoro1
 * Date         : 2020-04-30 15:51:08
 * LastEditors  : OBKoro1
 * LastEditTime : 2020-05-13 17:24:47
 * FilePath     : \fileHead\test.js
 * Description  : 
 * https://github.com/OBKoro1
 */

注释图案

佛祖

/*
 *                        _oo0oo_
 *                       o8888888o
 *                       88" . "88
 *                       (| -_- |)
 *                       0\  =  /0
 *                     ___/`---'\___
 *                   .' \\|     |// '.
 *                  / \\|||  :  |||// \
 *                 / _||||| -:- |||||- \
 *                |   | \\\  - /// |   |
 *                | \_|  ''\---/''  |_/ |
 *                \  .-\__  '-'  ___/-. /
 *              ___'. .'  /--.--\  `. .'___
 *           ."" '<  `.___\_<|>_/___.' >' "".
 *          | | :  `- \`.;`\ _ /`;.`/ - ` : | |
 *          \  \ `_.   \_ __\ /__ _/   .-` /  /
 *      =====`-.____`.___ \_____/___.-`___.-'=====
 *                        `=---='
 * 
 * 
 *      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 * 
 *            佛祖保佑       永不宕机     永无BUG
 */

佛曰

/*
 *           佛曰:  
 *                   写字楼里写字间,写字间里程序员;  
 *                   程序人员写程序,又拿程序换酒钱。  
 *                   酒醒只在网上坐,酒醉还来网下眠;  
 *                   酒醉酒醒日复日,网上网下年复年。  
 *                   但愿老死电脑间,不愿鞠躬老板前;  
 *                   奔驰宝马贵者趣,公交自行程序员。  
 *                   别人笑我忒疯癫,我笑自己命太贱;  
 *                   不见满街漂亮妹,哪个归得程序员?
 */

佛祖+佛曰

/*
 *                        _oo0oo_
 *                       o8888888o
 *                       88" . "88
 *                       (| -_- |)
 *                       0\  =  /0
 *                     ___/`---'\___
 *                   .' \\|     |// '.
 *                  / \\|||  :  |||// \
 *                 / _||||| -:- |||||- \
 *                |   | \\\  - /// |   |
 *                | \_|  ''\---/''  |_/ |
 *                \  .-\__  '-'  ___/-. /
 *              ___'. .'  /--.--\  `. .'___
 *           ."" '<  `.___\_<|>_/___.' >' "".
 *          | | :  `- \`.;`\ _ /`;.`/ - ` : | |
 *          \  \ `_.   \_ __\ /__ _/   .-` /  /
 *      =====`-.____`.___ \_____/___.-`___.-'=====
 *                        `=---='
 * 
 * 
 *      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 * 
 *            佛祖保佑       永不宕机     永无BUG
 * 
 *        佛曰:  
 *                写字楼里写字间,写字间里程序员;  
 *                程序人员写程序,又拿程序换酒钱。  
 *                酒醒只在网上坐,酒醉还来网下眠;  
 *                酒醉酒醒日复日,网上网下年复年。  
 *                但愿老死电脑间,不愿鞠躬老板前;  
 *                奔驰宝马贵者趣,公交自行程序员。  
 *                别人笑我忒疯癫,我笑自己命太贱;  
 *                不见满街漂亮妹,哪个归得程序员?
 */

美女

/*
 *                        .::::.
 *                      .::::::::.
 *                     :::::::::::
 *                  ..:::::::::::'
 *               '::::::::::::'
 *                 .::::::::::
 *            '::::::::::::::..
 *                 ..::::::::::::.
 *               ``::::::::::::::::
 *                ::::``:::::::::'        .:::.
 *               ::::'   ':::::'       .::::::::.
 *             .::::'      ::::     .:::::::'::::.
 *            .:::'       :::::  .:::::::::' ':::::.
 *           .::'        :::::.:::::::::'      ':::::.
 *          .::'         ::::::::::::::'         ``::::.
 *      ...:::           ::::::::::::'              ``::.
 *     ````':.          ':::::::::'                  ::::..
 *                        '.:::::'                    ':'````..
 */

龙图腾

/*
 * ......................................&&.........................
 * ....................................&&&..........................
 * .................................&&&&............................
 * ...............................&&&&..............................
 * .............................&&&&&&..............................
 * ...........................&&&&&&....&&&..&&&&&&&&&&&&&&&........
 * ..................&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&..............
 * ................&...&&&&&&&&&&&&&&&&&&&&&&&&&&&&.................
 * .......................&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&.........
 * ...................&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&...............
 * ..................&&&   &&&&&&&&&&&&&&&&&&&&&&&&&&&&&............
 * ...............&&&&&@  &&&&&&&&&&..&&&&&&&&&&&&&&&&&&&...........
 * ..............&&&&&&&&&&&&&&&.&&....&&&&&&&&&&&&&..&&&&&.........
 * ..........&&&&&&&&&&&&&&&&&&...&.....&&&&&&&&&&&&&...&&&&........
 * ........&&&&&&&&&&&&&&&&&&&.........&&&&&&&&&&&&&&&....&&&.......
 * .......&&&&&&&&.....................&&&&&&&&&&&&&&&&.....&&......
 * ........&&&&&.....................&&&&&&&&&&&&&&&&&&.............
 * ..........&...................&&&&&&&&&&&&&&&&&&&&&&&............
 * ................&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&............
 * ..................&&&&&&&&&&&&&&&&&&&&&&&&&&&&..&&&&&............
 * ..............&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&....&&&&&............
 * ...........&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&......&&&&............
 * .........&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&.........&&&&............
 * .......&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&...........&&&&............
 * ......&&&&&&&&&&&&&&&&&&&...&&&&&&...............&&&.............
 * .....&&&&&&&&&&&&&&&&............................&&..............
 * ....&&&&&&&&&&&&&&&.................&&...........................
 * ...&&&&&&&&&&&&&&&.....................&&&&......................
 * ...&&&&&&&&&&.&&&........................&&&&&...................
 * ..&&&&&&&&&&&..&&..........................&&&&&&&...............
 * ..&&&&&&&&&&&&...&............&&&.....&&&&...&&&&&&&.............
 * ..&&&&&&&&&&&&&.................&&&.....&&&&&&&&&&&&&&...........
 * ..&&&&&&&&&&&&&&&&..............&&&&&&&&&&&&&&&&&&&&&&&&.........
 * ..&&.&&&&&&&&&&&&&&&&&.........&&&&&&&&&&&&&&&&&&&&&&&&&&&.......
 * ...&&..&&&&&&&&&&&&.........&&&&&&&&&&&&&&&&...&&&&&&&&&&&&......
 * ....&..&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&...........&&&&&&&&.....
 * .......&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&..............&&&&&&&....
 * .......&&&&&.&&&&&&&&&&&&&&&&&&..&&&&&&&&...&..........&&&&&&....
 * ........&&&.....&&&&&&&&&&&&&.....&&&&&&&&&&...........&..&&&&...
 * .......&&&........&&&.&&&&&&&&&.....&&&&&.................&&&&...
 * .......&&&...............&&&&&&&.......&&&&&&&&............&&&...
 * ........&&...................&&&&&&.........................&&&..
 * .........&.....................&&&&........................&&....
 * ...............................&&&.......................&&......
 * ................................&&......................&&.......
 * .................................&&..............................
 * ..................................&..............................
 */

程序员之歌

/*
 *                   江城子 . 程序员之歌
 * 
 *               十年生死两茫茫,写程序,到天亮。
 *                   千行代码,Bug何处藏。
 *               纵使上线又怎样,朝令改,夕断肠。
 * 
 *               领导每天新想法,天天改,日日忙。
 *                   相顾无言,惟有泪千行。
 *               每晚灯火阑珊处,夜难寐,加班狂。
 * 
 */

耶稣

/*
 *                                |~~~~~~~|
 *                                |       |
 *                                |       |
 *                                |       |
 *                                |       |
 *                                |       |
 *     |~.\\\_\~~~~~~~~~~~~~~xx~~~         ~~~~~~~~~~~~~~~~~~~~~/_//;~|
 *     |  \  o \_         ,XXXXX),                         _..-~ o /  |
 *     |    ~~\  ~-.     XXXXX`)))),                 _.--~~   .-~~~   |
 *      ~~~~~~~`\   ~\~~~XXX' _/ ';))     |~~~~~~..-~     _.-~ ~~~~~~~
 *               `\   ~~--`_\~\, ;;;\)__.---.~~~      _.-~
 *                 ~-.       `:;;/;; \          _..-~~
 *                    ~-._      `''        /-~-~
 *                        `\              /  /
 *                          |         ,   | |
 *                           |  '        /  |
 *                            \/;          |
 *                             ;;          |
 *                             `;   .       |
 *                             |~~~-----.....|
 *                            | \             \
 *                           | /\~~--...__    |
 *                           (|  `\       __-\|
 *                           ||    \_   /~    |
 *                           |)     \~-'      |
 *                            |      | \      '
 *                            |      |  \    :
 *                             \     |  |    |
 *                              |    )  (    )
 *                               \  /;  /\  |
 *                               |    |/   |
 *                               |    |   |
 *                                \  .'  ||
 *                                |  |  | |
 *                                (  | |  |
 *                                |   \ \ |
 *                                || o `.)|
 *                                |`\\) |
 *                                |       |
 *                                |       |
 */

/*
 *                        ::
 *                       :;J7, :,                        ::;7:
 *                       ,ivYi, ,                       ;LLLFS:
 *                       :iv7Yi                       :7ri;j5PL
 *                      ,:ivYLvr                    ,ivrrirrY2X,
 *                      :;r@Wwz.7r:                :ivu@kexianli.
 *                     :iL7::,:::iiirii:ii;::::,,irvF7rvvLujL7ur
 *                    ri::,:,::i:iiiiiii:i:irrv177JX7rYXqZEkvv17
 *                 ;i:, , ::::iirrririi:i:::iiir2XXvii;L8OGJr71i
 *               :,, ,,:   ,::ir@mingyi.irii:i:::j1jri7ZBOS7ivv,
 *                  ,::,    ::rv77iiiriii:iii:i::,rvLq@huhao.Li
 *              ,,      ,, ,:ir7ir::,:::i;ir:::i:i::rSGGYri712:
 *            :::  ,v7r:: ::rrv77:, ,, ,:i7rrii:::::, ir7ri7Lri
 *           ,     2OBBOi,iiir;r::        ,irriiii::,, ,iv7Luur:
 *         ,,     i78MBBi,:,:::,:,  :7FSL: ,iriii:::i::,,:rLqXv::
 *         :      iuMMP: :,:::,:ii;2GY7OBB0viiii:i:iii:i:::iJqL;::
 *        ,     ::::i   ,,,,, ::LuBBu BBBBBErii:i:i:i:i:i:i:r77ii
 *       ,       :       , ,,:::rruBZ1MBBqi, :,,,:::,::::::iiriri:
 *      ,               ,,,,::::i:  @arqiao.       ,:,, ,:::ii;i7:
 *     :,       rjujLYLi   ,,:::::,:::::::::,,   ,:i,:,,,,,::i:iii
 *     ::      BBBBBBBBB0,    ,,::: , ,:::::: ,      ,,,, ,,:::::::
 *     i,  ,  ,8BMMBBBBBBi     ,,:,,     ,,, , ,   , , , :,::ii::i::
 *     :      iZMOMOMBBM2::::::::::,,,,     ,,,,,,:,,,::::i:irr:i:::,
 *     i   ,,:;u0MBMOG1L:::i::::::  ,,,::,   ,,, ::::::i:i:iirii:i:i:
 *     :    ,iuUuuXUkFu7i:iii:i:::, :,:,: ::::::::i:i:::::iirr7iiri::
 *     :     :rk@Yizero.i:::::, ,:ii:::::::i:::::i::,::::iirrriiiri::,
 *      :      5BMBBBBBBSr:,::rv2kuii:::iii::,:i:,, , ,,:,:i@petermu.,
 *           , :r50EZ8MBBBBGOBBBZP7::::i::,:::::,: :,:,::i;rrririiii::
 *               :jujYY7LS0ujJL7r::,::i::,::::::::::::::iirirrrrrrr:ii:
 *            ,:  :@kevensun.:,:,,,::::i:i:::::,,::::::iir;ii;7v77;ii;i,
 *            ,,,     ,,:,::::::i:iiiii:i::::,, ::::iiiir@xingjief.r;7:i,
 *         , , ,,,:,,::::::::iiiiiiiiii:,:,:::::::::iiir;ri7vL77rrirri::
 *          :,, , ::::::::i:::i:::i:i::,,,,,:,::i:i:::iir;@Secbone.ii:::
 */

喷火龙

/*
 *                                                     __----~~~~~~~~~~~------___
 *                                    .  .   ~~//====......          __--~ ~~
 *                    -.            \_|//     |||\\  ~~~~~~::::... /~
 *                 ___-==_       _-~o~  \/    |||  \\            _/~~-
 *         __---~~~.==~||\=_    -_--~/_-~|-   |\\   \\        _/~
 *     _-~~     .=~    |  \\-_    '-~7  /-   /  ||    \      /
 *   .~       .~       |   \\ -_    /  /-   /   ||      \   /
 *  /  ____  /         |     \\ ~-_/  /|- _/   .||       \ /
 *  |~~    ~~|--~~~~--_ \     ~==-/   | \~--===~~        .\
 *           '         ~-|      /|    |-~\~~       __--~~
 *                       |-~~-_/ |    |   ~\_   _-~            /\
 *                            /  \     \__   \/~                \__
 *                        _--~ _/ | .-~~____--~-/                  ~~==.
 *                       ((->/~   '.|||' -_|    ~~-/ ,              . _||
 *                                  -_     ~\      ~~---l__i__i__i--~~_/
 *                                  _-~-__   ~)  \--______________--~~
 *                                //.-~~~-~_--~- |-------~~~~~~~~
 *                                       //.-~~~--\
 *                       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 * 
 *                               神兽保佑            永无BUG
 */

蝙蝠

/*
 *                   ___====-_  _-====___
 *             _--^^^#####//      \\#####^^^--_
 *          _-^##########// (    ) \\##########^-_
 *         -############//  |\^^/|  \\############-
 *       _/############//   (@::@)   \############\_
 *      /#############((     \\//     ))#############\
 *     -###############\\    (oo)    //###############-
 *    -#################\\  / VV \  //#################-
 *   -###################\\/      \//###################-
 *  _#/|##########/\######(   /\   )######/\##########|\#_
 *  |/ |#/\#/\#/\/  \#/\##\  |  |  /##/\#/  \/\#/\#/\#| \|
 *  `  |/  V  V  `   V  \#\| |  | |/#/  V   '  V  V  \|  '
 *     `   `  `      `   / | |  | | \   '      '  '   '
 *                      (  | |  | |  )
 *                     __\ | |  | | /__
 *                    (vvv(VVV)(VVV)vvv)
 * 
 *      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 * 
 *                神兽保佑            永无BUG
 */

Auto Commit 一键补充commit记录

本人还开源了另外一个VSCode插件:Auto Commit

这是一个用于Git自动commit的VSCode插件,它可以用来补充之前忘记提交commit,帮助你把首页的绿色格子填满,感兴趣的可以试用一下~

autoCommit演示

最后

如果觉得还不错的话,就给个 Star ⭐️ 鼓励一下我吧~

前端进阶积累公众号GitHub、wx:OBkoro1、邮箱:obkoro1@foxmail.com

查看原文

赞 8 收藏 4 评论 3

OBKoro1 发布了文章 · 2019-12-09

手摸手教你定制ESLint rule以及了解ESLint的运行原理

这篇文章目的是介绍如何创建一个ESLint插件和创建一个ESLintrule,用以帮助我们更深入的理解ESLint的运行原理,并且在有必要时可以根据需求创建出一个完美满足自己需求的Lint规则。

插件目标

禁止项目中setTimeout的第二个参数是数字。

PS: 如果是数字的话,很容易就成为魔鬼数字,没有人知道为什么是这个数字, 这个数字有什么含义。

使用模板初始化项目:

1. 安装NPM包

ESLint官方为了方便开发者开发插件,提供了使用Yeoman模板(generator-eslint)。

对于Yeoman我们只需知道它是一个脚手架工具,用于生成包含指定框架结构的工程化目录结构。

npm install -g yo generator-eslint

2. 创建一个文件夹:

mkdir eslint-plugin-demo
cd eslint-plugin-demo

3. 命令行初始化ESLint插件的项目结构:

yo eslint:plugin

下面进入命令行交互流程,流程结束后生成ESLint插件项目框架和文件。

? What is your name? OBKoro1
? What is the plugin ID? korolint   // 这个插件的ID是什么
? Type a short description of this plugin: XX公司的定制ESLint rule // 输入这个插件的描述
? Does this plugin contain custom ESLint rules? Yes // 这个插件包含自定义ESLint规则吗?
? Does this plugin contain one or more processors? No // 这个插件包含一个或多个处理器吗
// 处理器用于处理js以外的文件 比如.vue文件
   create package.json
   create lib/index.js
   create README.md

现在可以看到在文件夹内生成了一些文件夹和文件,但我们还需要创建规则具体细节的文件。

4. 创建规则

上一个命令行生成的是ESLint插件的项目模板,这个命令行是生成ESLint插件具体规则的文件。
yo eslint:rule // 生成 eslint rule的模板文件

创建规则命令行交互:

? What is your name? OBKoro1
? Where will this rule be published? (Use arrow keys) // 这个规则将在哪里发布?
❯ ESLint Core  // 官方核心规则 (目前有200多个规则)
  ESLint Plugin  // 选择ESLint插件
? What is the rule ID? settimeout-no-number  // 规则的ID
? Type a short description of this rule: setTimeout 第二个参数禁止是数字  // 输入该规则的描述
? Type a short example of the code that will fail:  占位  // 输入一个失败例子的代码
   create docs/rules/settimeout-no-number.md
   create lib/rules/settimeout-no-number.js
   create tests/lib/rules/settimeout-no-number.js

加了具体规则文件的项目结构

.
├── README.md
├── docs // 使用文档
│   └── rules // 所有规则的文档
│       └── settimeout-no-number.md // 具体规则文档
├── lib // eslint 规则开发
│   ├── index.js 引入+导出rules文件夹的规则
│   └── rules // 此目录下可以构建多个规则
│       └── settimeout-no-number.js // 规则细节
├── package.json
└── tests // 单元测试
    └── lib
        └── rules
            └── settimeout-no-number.js // 测试该规则的文件

4. 安装项目依赖

npm install

以上是开发ESLint插件具体规则的准备工作,下面先来看看AST和ESLint原理的相关知识,为我们开发ESLint rule 打一下基础。

AST——抽象语法树

AST是: Abstract Syntax Tree的简称,中文叫做:抽象语法树。

AST的作用

将代码抽象成树状数据结构,方便后续分析检测代码。

代码被解析成AST的样子

astexplorer.net是一个工具网站:它能查看代码被解析成AST的样子。

如下图:在右侧选中一个值时,左侧对应区域也变成高亮区域,这样可以在AST中很方便的选中对应的代码

AST 选择器:

下图中被圈起来的部分,称为AST selectors(选择器)。

AST 选择器的作用:使用代码通过选择器来选中特定的代码片段,然后再对代码进行静态分析。

AST 选择器很多,ESLint官方专门有一个仓库列出了所有类型的选择器: estree

下文中开发ESLint rule就需要用到选择器,等下用到了就懂了,现在知道一下就好了。

将代码解析成AST


ESLint的运行原理

在开发规则之前,我们需要ESLint是怎么运行的,了解插件为什么需要这么写。

1. 将代码解析成AST

ESLint使用JavaScript解析器Espree把JS代码解析成AST。

PS:解析器:是将代码解析成AST的工具,ES6、react、vue都开发了对应的解析器所以ESLint能检测它们的,ESLint也是因此一统前端Lint工具的。

2. 深度遍历AST,监听匹配过程。

在拿到AST之后,ESLint会以"从上至下"再"从下至上"的顺序遍历每个选择器两次。

3. 触发监听选择器的rule回调

在深度遍历的过程中,生效的每条规则都会对其中的某一个或多个选择器进行监听,每当匹配到选择器,监听该选择器的rule,都会触发对应的回调。

4. 具体的检测规则等细节内容。


开发规则

规则默认模板

打开rule生成的模板文件lib/rules/settimeout-no-number.js, 清理一下文件,删掉不必要的选项:

module.exports = {
    meta: {
        docs: {
            description: "setTimeout 第二个参数禁止是数字",
        },
        fixable: null,  // 修复函数
    },
   // rule 核心
    create: function(context) {
       // 公共变量和函数应该在此定义
        return {
            // 返回事件钩子
        };
    }
};

删掉的配置项,有些是ESLint官方核心规则才是用到的配置项,有些是暂时不必了解的配置,需要用到的时候,可以自行查阅ESLint 文档

create方法-监听选择器

上文ESLint原理第三部中提到的:在深度遍历的过程中,生效的每条规则都会对其中的某一个或多个选择器进行监听,每当匹配到选择器,监听该选择器的rule,都会触发对应的回调。

create返回一个对象,对象的属性设为选择器,ESLint会收集这些选择器,在AST遍历过程中会执行所有监听该选择器的回调。

// rule 核心
create: function(context) {
    // 公共变量和函数应该在此定义
    return {
        // 返回事件钩子
        Identifier: (node) => {
            // node是选中的内容,是我们监听的部分, 它的值参考AST
        }
    };
}

观察AST:

创建一个ESLint rule需要观察代码解析成AST,选中你要检测的代码,然后进行一些判断。

以下代码都是通过astexplorer.net在线解析的。

setTimeout(()=>{
    console.log('settimeout')
}, 1000)

setTimeout第二个参数为数字时的AST

rule完整文件

lib/rules/settimeout-no-number.js:

module.exports = {
    meta: {
        docs: {
            description: "setTimeout 第二个参数禁止是数字",
        },
        fixable: null,  // 修复函数
    },
    // rule 核心
    create: function (context) {
        // 公共变量和函数应该在此定义
        return {
            // 返回事件钩子
            'CallExpression': (node) => {
                if (node.callee.name !== 'setTimeout') return // 不是定时器即过滤
                const timeNode = node.arguments && node.arguments[1] // 获取第二个参数
                if (!timeNode) return // 没有第二个参数
                // 检测报错第二个参数是数字 报错
                if (timeNode.type === 'Literal' && typeof timeNode.value === 'number') {
                    context.report({
                        node,
                        message: 'setTimeout第二个参数禁止是数字'
                    })
                }
            }
        };
    }
};

context.report():这个方法是用来通知ESLint这段代码是警告或错误的,用法如上。在这里查看contextcontext.report()的文档。

规则写完了,原理就是依据AST解析的结果,做针对性的检测,过滤出我们要选中的代码,然后对代码的值进行逻辑判断

可能现在会有点懵逼,但是不要紧,我们来写一下测试用例,然后用debugger来看一下代码是怎么运行的。

测试用例:

测试文件tests/lib/rules/settimeout-no-number.js:

/**
 * @fileoverview setTimeout 第二个参数禁止是数字
 * @author OBKoro1
 */
"use strict";
var rule = require("../../../lib/rules/settimeout-no-number"), // 引入rule
    RuleTester = require("eslint").RuleTester;

var ruleTester = new RuleTester({
    parserOptions: {
        ecmaVersion: 7, // 默认支持语法为es5 
    },
});
// 运行测试用例
ruleTester.run("settimeout-no-number", rule, {
    // 正确的测试用例
    valid: [
        {
            code: 'let someNumber = 1000; setTimeout(()=>{ console.log(11) },someNumber)'
        },
        {
            code: 'setTimeout(()=>{ console.log(11) },someNumber)'
        }
    ],
    // 错误的测试用例
    invalid: [
        {
            code: 'setTimeout(()=>{ console.log(11) },1000)',
            errors: [{
                message: "setTimeout第二个参数禁止是数字", // 与rule抛出的错误保持一致
                type: "CallExpression" // rule监听的对应钩子
            }]
        }
    ]
});

下面来学习一下怎么在VSCode中调试node文件,用于观察rule是怎么运行的。

实际上打console的形式,也是可以的,但是在调试的时候打console实在是有点慢,对于node这种节点来说,信息也不全,所以我还是比较推荐通过debugger的方式来调试rule

在VSCode中调试node文件

  1. 点击下图中的设置按钮, 将会打开一个文件launch.json
  2. 在文件中填入如下内容,用于调试node文件。
  3. rule文件中打debugger或者在代码行数那里点一下小红点。
  4. 点击图中的开始按钮,进入debugger

vscode 设置

{
    // 使用 IntelliSense 了解相关属性。 
    // 悬停以查看现有属性的描述。
    // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "启动程序", // 调试界面的名称
            // 运行项目下的这个文件:
            "program": "${workspaceFolder}/tests/lib/rules/settimeout-no-number.js",
            "args": [] // node 文件的参数
        },
        // 下面是用于调试package.json的命令 之前可以用,貌似vscode出了点bug导致现在用不了了
        {
            "name": "Launch via NPM",
            "type": "node",
            "request": "launch",
            "runtimeExecutable": "npm",
            "runtimeArgs": [
                "run-script", "dev"    //这里的dev就对应package.json中的scripts中的dev
            ],
            "port": 9229    //这个端口是调试的端口,不是项目启动的端口
        },
    ]
}

运行测试用例进入断点

  1. lib/rules/settimeout-no-number.js中打一些debugger
  2. 点击开始按钮,以调试的形式运行测试文件tests/lib/rules/settimeout-no-number.js
  3. 开始调试rule

发布插件

eslint插件都是以npm包的形式来引用的,所以需要把插件发布一下:

  1. 注册:如果你还未注册npm账号的话,需要去注册一下。
  2. 登录npm: npm login
  3. 发布npm包: npm publish即可,ESLint已经把package.json弄好了。

集成到项目:

安装npm包:npm i eslint-plugin-korolint -D

  1. 常规的方法: 引入插件一条条写入规则
// .eslintrc.js
module.exports = {
  plugins: [ 'korolint' ],
  rules: { 
    "korolint/settimeout-no-number": "error"
 }
}
  1. extends继承插件配置:

当规则比较多的时候,用户一条条去写,未免也太麻烦了,所以ESLint可以继承插件的配置

修改一下lib/rules/index.js文件:

'use strict';
var requireIndex = require('requireindex');
const output = {
  rules: requireIndex(__dirname + '/rules'), // 导出所有规则
  configs: {
    // 导出自定义规则 在项目中直接引用
    koroRule: {
      plugins: ['korolint'], // 引入插件
      rules: {
        // 开启规则
        'korolint/settimeout-no-number': 'error'
      }
    }
  }
};
module.exports = output;

使用方法:

使用extends来继承插件的配置,extends不止这种继承方式,即使你传入一个npm包,一个文件的相对路径地址,eslint也能继承其中的配置。

// .eslintrc.js
module.exports = {
  extends: [ 'plugin:korolint/koroRule' ] // 继承插件导出的配置
}

PS : 这种使用方式, npm的包名不能为eslint-plugin-xx-xx,只能为eslint-plugin-xx否则会有报错,被这个问题搞得头疼o(╥﹏╥)o

扩展:

以上内容足够开发一个插件,这里是一些扩展知识点。

遍历方向:

上文中说过: 在拿到AST之后,ESLint会以"从上至下"再"从下至上"的顺序遍历每个选择器两次。

我们所监听的选择器默认会在"从上至下"的过程中触发,如果需要在"从下至上"的过程中执行则需要添加:exit,在上文中CallExpression就变为CallExpression:exit

注意:一段代码解析后可能包含多次同一个选择器,选择器的钩子也会多次触发。

fix函数:自动修复rule错误

修复效果

// 修复前
setTimeout(() => {

}, 1000)
// 修复后 变量名故意写错 为了让用户去修改它
const countNumber1 = 1000
setTimeout(() => {

}, countNumber2)
  1. 在rule的meta对象上打开修复功能:
// rule文件
module.exports = {
  meta: {
    docs: {
      description: 'setTimeout 第二个参数禁止是数字'
    },
    fixable: 'code' // 打开修复功能
  }
}
  1. context.report()上提供一个fix函数:

把上文的context.report修改一下,增加一个fix方法即可,更详细的介绍可以看一下文档

context.report({
    node,
    message: 'setTimeout第二个参数禁止是数字',
    fix(fixer) {
        const numberValue = timeNode.value;
        const statementString = `const countNumber = ${numberValue}\n`
        return [
        // 修改数字为变量
        fixer.replaceTextRange(node.arguments[1].range, 'countNumber'),
        // 在setTimeout之前增加一行声明变量的代码 用户自行修改变量名
        fixer.insertTextBeforeRange(node.range, statementString),
        ];
    }
});

项目地址:

eslint-plugin-korolint


呼~ 这篇博客断断续续,写了好几周,终于完成了!

大家有看到这篇博客的话,建议跟着博客的一起动手写一下,动手实操一下比你mark一百篇文章都来的有用,花不了很长时间的,希望各位看完本文,都能够更深入的了解到ESLint的运行原理。

觉得我的博客对你有帮助的话,就关注一下/点个赞吧!

前端进阶积累公众号GitHub、wx:OBkoro1、邮箱:obkoro1@foxmail.com

基友带我飞

ESLint插件是向基友yeyan1996学习的,在遇到问题的时候,也是他指点我的,特此感谢。

参考资料:

创建规则
ESLint 工作原理探讨

查看原文

赞 2 收藏 0 评论 0

OBKoro1 发布了文章 · 2019-09-23

JS基础-完美掌握继承知识点

前言

上篇文章详细解析了原型、原型链的相关知识点,这篇文章讲的是和原型链有密切关联的继承,它是前端基础中很重要的一个知识点,它对于代码复用来说非常有用,本篇将详细解析JS中的各种继承方式和优缺点进行,希望看完本篇文章能够对继承以及相关概念理解的更为透彻。

本篇文章需要先理解原型、原型链以及call 的相关知识:

JS基础-函数、对象和原型、原型链的关系

js基础-面试官想知道你有多理解call,apply,bind?

何为继承?

维基百科):继承可以使得子类具有父类别的各种属性和方法,而不需要再次编写相同的代码。

继承是一个类从另一个类获取方法和属性的过程

PS:或者是多个类

JS实现继承的原理

记住这个概念,你会发现JS中的继承都是在实现这个目的,差异是它们的实现方式不同。

复制父类的属性和方法来重写子类原型对象

原型链继承(new):

function fatherFn() {
  this.some = '父类的this属性';
}
fatherFn.prototype.fatherFnSome =  '父类原型对象的属性或者方法';
// 子类
function sonFn() {
  this.obkoro1 = '子类的this属性';
}
// 核心步骤:重写子类的原型对象
sonFn.prototype = new fatherFn(); // 将fatherFn的实例赋值给sonFn的prototype
sonFn.prototype.sonFnSome = '子类原型对象的属性或者方法' // 子类的属性/方法声明在后面,避免被覆盖
// 实例化子类
const sonFnInstance = new sonFn();
console.log('子类的实例:', sonFnInstance);

原型链子类实例

原型链子类实例

原型链继承获取父类的属性和方法

  1. fatherFn通过this声明的属性/方法都会绑定在new期间创建的新对象上。
  2. 新对象的原型是father.prototype,通过原型链的属性查找到father.prototype的属性和方法。

理解new做了什么:

new在本文出现多次,new也是JS基础中很重要的一块内容,很多知识点会涉及到new,不太理解的要多看几遍。
  1. 创建一个全新的对象。
  2. 这个新对象的原型(__proto__)指向函数的prototype对象。
  3. 执行函数,函数的this会绑定在新创建的对象上。
  4. 如果函数没有返回其他对象(包括数组、函数、日期对象等),那么会自动返回这个新对象。
  5. 返回的那个对象为构造函数的实例。

构造调用函数返回其他对象

返回其他对象会导致获取不到构造函数的实例,很容易因此引起意外的问题

我们知道了fatherFnthisprototype的属性/方法都跟new期间创建的新对象有关系

如果在父类中返回了其他对象(new的第四点),其他对象没有父类的thisprototype,因此导致原型链继承失败

我们来测试一下,修改原型链继承中的父类fatherFn

function fatherFn() {
  this.some = '父类的this属性';
  console.log('new fatherFn 期间生成的对象', this)
  return [ '数组对象', '函数对象', '日期对象', '正则对象', '等等等', '都不会返回new期间创建的新对象' ]
}

原型链继承返回其他对象,将导致原型链继承失败

PS: 本文中构造调用函数都不能返回其他函数,下文不再提及该点。

不要使用对象字面量的形式创建原型方法:

这种方式很容易在不经意间,清除/覆盖了原型对象原有的属性/方法,不该为了稍微简便一点,而使用这种写法。

有些人在需要在原型对象上创建多个属性和方法,会使用对象字面量的形式来创建:

sonFn.prototype = new fatherFn();
// 子类的prototype被清空后 重新赋值, 导致上一行代码失效
sonFn.prototype = {
    sonFnSome: '子类原型对象的属性',
    one: function() {},
    two: function() {},
    three: function() {}
}

还有一种常见的做法,该方式会导致函数原型对象的属性constructor丢失:

function test() {}
test.prototype = {
    ...
}

原型链继承的缺点

  1. 父类使用this声明的属性被所有实例共享

    原因是:实例化的父类(sonFn.prototype = new fatherFn())是一次性赋值到子类实例的原型(sonFn.prototype)上,它会将父类通过this声明的属性也在赋值到sonFn.prototype上。

值得一提的是:很多博客中说,引用类型的属性被所有实例共享,通常会用数组来举例,实际上数组以及其他父类通过this声明的属性也只是通过原型链查找去获取子类实例的原型(sonFn.prototype)上的值。
  1. 创建子类实例时,无法向父类构造函数传参,不够灵活。

这种模式父类的属性、方法一开始就是定义好的,无法向父类传参,不够灵活。

sonFn.prototype = new fatherFn()

借用构造函数继承(call)

 function fatherFn(...arr) {
  this.some = '父类的this属性';
  this.params = arr // 父类的参数
}
fatherFn.prototype.fatherFnSome = '父类原型对象的属性或者方法';
function sonFn(fatherParams, ...sonParams) {
  fatherFn.call(this, ...fatherParams); // 核心步骤: 将fatherFn的this指向sonFn的this对象上
  this.obkoro1 = '子类的this属性';
  this.sonParams = sonParams; // 子类的参数
}
sonFn.prototype.sonFnSome = '子类原型对象的属性或者方法'
let fatherParamsArr = ['父类的参数1', '父类的参数2']
let sonParamsArr = ['子类的参数1', '子类的参数2']
const sonFnInstance = new sonFn(fatherParamsArr, ...sonParamsArr); // 实例化子类
console.log('借用构造函数子类实例', sonFnInstance)

借用构造函数继承的子类实例

借用构造函数继承的子类实例

借用构造函数继承做了什么?

声明类,组织参数等,只是辅助的上下文代码,核心是借用构造函数使用call做了什么:

一经调用call/apply它们就会立即执行函数,并在函数执行时改变函数的this指向

fatherFn.call(this, ...fatherParams); 
  1. 在子类中使用call调用父类,fatherFn将会被立即执行,并且将fatherFn函数的this指向sonFnthis
  2. 因为函数执行了,所以fatherFn使用this声明的函数都会被声明到sonFnthis对象下。
  3. 实例化子类,this将指向new期间创建的新对象,返回该新对象。
  4. fatherFn.prototype没有任何操作,无法继承。

该对象的属性为:子类和父类声明的this属性/方法,它的原型是

PS: 关于call/apply/bind的更多细节,推荐查看我的博客:[js基础-面试官想知道你有多理解call,apply,bind?[不看后悔系列]](https://juejin.im/post/5d469e...

借用构造函数继承的优缺点

优点:

  1. 可以向父类传递参数
  2. 解决了原型链继承中:父类属性使用this声明的属性会在所有实例共享的问题。

缺点:

  1. 只能继承父类通过this声明的属性/方法,不能继承父类prototype上的属性/方法。
  2. 父类方法无法复用:因为无法继承父类的prototype,所以每次子类实例化都要执行父类函数,重新声明父类this里所定义的方法,因此方法无法复用。

组合继承(call+new)

原理:使用原型链继承(new)将thisprototype声明的属性/方法继承至子类的prototype上,使用借用构造函数来继承父类通过this声明属性和方法至子类实例的属性上。
function fatherFn(...arr) {
  this.some = '父类的this属性';
  this.params = arr // 父类的参数
}
fatherFn.prototype.fatherFnSome = '父类原型对象的属性或者方法';
function sonFn() {
  fatherFn.call(this, '借用构造继承', '第二次调用'); // 借用构造继承: 继承父类通过this声明属性和方法至子类实例的属性上
  this.obkoro1 = '子类的this属性';
}
sonFn.prototype = new fatherFn('原型链继承', '第一次调用'); // 原型链继承: 将`this`和`prototype`声明的属性/方法继承至子类的`prototype`上
sonFn.prototype.sonFnSome = '子类原型对象的属性或者方法'
const sonFnInstance = new sonFn();
console.log('组合继承子类实例', sonFnInstance)

组合继承的子类实例

组合继承的子类实例

从图中可以看到fatherFn通过this声明的属性/方法,在子类实例的属性上,和其原型上都复制了一份,原因在代码中也有注释:

  1. 原型链继承: 父类通过thisprototype声明的属性/方法继承至子类的prototype上。
  2. 借用构造继承: 父类通过this声明属性和方法继承至子类实例的属性上。

组合继承的优缺点

优点:

完整继承(又不是不能用),解决了:

  1. 父类通过this声明属性/方法被子类实例共享的问题(原型链继承的问题)
    每次实例化子类将重新初始化父类通过this声明的属性,实例根据原型链查找规则,每次都会
  2. 父类通过prototype声明的属性/方法无法继承的问题(借用构造函数的问题)。

缺点:

  1. 两次调用父类函数(new fatherFn()fatherFn.call(this)),造成一定的性能损耗。
  2. 因调用两次父类,导致父类通过this声明的属性/方法,生成两份的问题。
  3. 原型链上下文丢失:子类和父类通过prototype声明的属性/方法都存在于子类的prototype上

原型式继承(Object.create())

继承对象原型-Object.create()实现

以下是Object.create()的模拟实现,使用Object.create()可以达成同样的效果,基本上现在都是使用Object.create()来做对象的原型继承。

function cloneObject(obj){
  function F(){}
  F.prototype = obj; // 将被继承的对象作为空函数的prototype
  return new F(); // 返回new期间创建的新对象,此对象的原型为被继承的对象, 通过原型链查找可以拿到被继承对象的属性
}

PS:上面Object.create()实现原理可以记一下,有些公司可能会让你讲一下它的实现原理。

例子:

let oldObj = { p: 1 };
let newObj = cloneObject(oldObj)
oldObj.p = 2
console.log('oldObj newObj', oldObj, newObj)

原型式继承

原型式继承优缺点:

优点: 兼容性好,最简单的对象继承。

缺点:

  1. 因为旧对象(oldObj)是实例对象(newObj)的原型,多个实例共享被继承对象的属性,存在篡改的可能。
  2. 无法传参

寄生式继承(封装继承过程)

创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后返回对象。
function createAnother(original){
  var clone = cloneObject(original); // 继承一个对象 返回新函数
  // do something 以某种方式来增强对象
  clone.some = function(){}; // 方法
  clone.obkoro1 = '封装继承过程'; // 属性
  return clone; // 返回这个对象
}

使用场景:专门为对象来做某种固定方式的增强。

寄生组合式继承(call+寄生式封装)

寄生组合式继承原理:

  1. 使用借用构造函数(call)来继承父类this声明的属性/方法
  2. 通过寄生式封装函数设置父类prototype为子类prototype的原型来继承父类的prototype声明的属性/方法
function fatherFn(...arr) {
  this.some = '父类的this属性';
  this.params = arr // 父类的参数
}
fatherFn.prototype.fatherFnSome = '父类原型对象的属性或者方法';
function sonFn() {
  fatherFn.call(this, '借用构造继承'); // 核心1 借用构造继承: 继承父类通过this声明属性和方法至子类实例的属性上
  this.obkoro1 = '子类的this属性';
}
// 核心2 寄生式继承:封装了son.prototype对象原型式继承father.prototype的过程,并且增强了传入的对象。
function inheritPrototype(son, father) {
  const fatherFnPrototype = Object.create(father.prototype); // 原型式继承:浅拷贝father.prototype对象 father.prototype为新对象的原型
  son.prototype = fatherFnPrototype; // 设置father.prototype为son.prototype的原型
  son.prototype.constructor = son; // 修正constructor 指向
}
inheritPrototype(sonFn, fatherFn)
sonFn.prototype.sonFnSome = '子类原型对象的属性或者方法'
const sonFnInstance = new sonFn();
console.log('寄生组合式继承子类实例', sonFnInstance)

寄生组合式继承子类实例

寄生组合式继承子类实例

寄生组合式继承是最成熟的继承方法:

寄生组合式继承是最成熟的继承方法, 也是现在最常用的继承方法,众多JS库采用的继承方案也是它。

寄生组合式继承相对于组合继承有如下优点:

  1. 只调用一次父类fatherFn构造函数。
  2. 避免在子类prototype上创建不必要多余的属性。
  3. 使用原型式继承父类的prototype,保持了原型链上下文不变。

    子类的prototype只有子类通过prototype声明的属性/方法和父类prototype上的属性/方法泾渭分明。

ES6 extends继承:

ES6继承的原理跟寄生组合式继承是一样的。

ES6 extends核心代码:

这段代码是通过babel在线编译
成es5, 用于子类prototype原型式继承父类prototype的属性/方法。

// 寄生式继承 封装继承过程
function _inherits(son, father) {
  // 原型式继承: 设置father.prototype为son.prototype的原型 用于继承father.prototype的属性/方法
  son.prototype = Object.create(father && father.prototype);
  son.prototype.constructor = son; // 修正constructor 指向
  // 将父类设置为子类的原型 用于继承父类的静态属性/方法(father.some)
  if (father) {
    Object.setPrototypeOf
      ? Object.setPrototypeOf(son, father)
      : son.__proto__ = father;
  }
}

另外子类是通过借用构造函数继承(call)来继承父类通过this声明的属性/方法,也跟寄生组合式继承一样。

ES5继承与ES6继承的区别:

本段摘自阮一峰-es6入门文档
  • ES5的继承实质上是先创建子类的实例对象,再将父类的方法添加到this上
  • ES6的继承是先创建父类的实例对象this,再用子类的构造函数修改this

    因为子类没有自己的this对象,所以必须先调用父类的super()方法。

扩展:

为什么要修正construct指向?

在寄生组合式继承中有一段如下一段修正constructor 指向的代码,很多人对于它的作用以及为什么要修正它不太清楚。

son.prototype.constructor = son; // 修正constructor 指向

construct的作用

MDN的定义:返回创建实例对象的Object构造函数的引用

即返回实例对象的构造函数的引用,例如:

let instance = new sonFn()
instance.constructor // sonFn函数

construct的应用场景:

当我们只有实例对象没有构造函数的引用时

某些场景下,我们对实例对象经过多轮导入导出,我们不知道实例是从哪个函数中构造出来或者追踪实例的构造函数,较为艰难。

这个时候就可以通过实例对象的constructor属性来得到构造函数的引用:

let instance = new sonFn() // 实例化子类
export instance;
// 多轮导入+导出,导致sonFn追踪非常麻烦,或者不想在文件中再引入sonFn
let  fn = instance.construct
// do something: new fn() / fn.prototype / fn.length / fn.arguments等等

保持construct指向的一致性:

因此每次重写函数的prototype都应该修正一下construct的指向,以保持读取construct行为的一致性。

小结

继承也是前端的高频面试题,了解本文中继承方法的优缺点,有助于更深刻的理解JS继承机制。除了组合继承和寄生式继承都是由其他方法组合而成的,分块理解会对它们理解的更深刻。

建议多看几遍本文,建个html文件试试文中的例子,两相结合更佳!

对prototype还不是很理解的同学,可以再看看:JS基础-函数、对象和原型、原型链的关系

觉得我的博客对你有帮助的话,就给我点个Star吧!

前端进阶积累公众号GitHub、wx:OBkoro1、邮箱:obkoro1@foxmail.com

以上2019/9/22

作者:OBKoro1

参考资料:

JS高级程序设计(红宝书)6.3继承

JavaScript常用八种继承方案

查看原文

赞 16 收藏 14 评论 0

OBKoro1 发布了文章 · 2019-08-26

JS基础-原型、原型链真的不能一知半解

JS的原型、原型链一直是比较难理解的内容,不少初学者甚至有一定经验的老鸟都不一定能完全说清楚,更多的"很可能"是一知半解,而这部分内容又是JS的核心内容,想要技术进阶的话肯定不能对这个概念一知半解,碰到问题靠“猜”,却不理解它的规则!

prototype

只有函数有prototype属性

let a = {}
let b = function () { }
console.log(a.prototype) // undefined
console.log(b.prototype) // { constructor: function(){...} }

Object.prototype怎么解释?

其实Object是一个全局对象,也是一个构造函数,以及其他基本类型的全局对象也都是构造函数:

function outTypeName(data, type) {
    let typeName =  Object.prototype.toString.call(data)
    console.log(typeName)
}
outTypeName(Object) //[object Function]
outTypeName(String) // [object Function]
outTypeName(Number) // [object Function]

为什么只有函数有prototype属性

JS通过new来生成对象,但是仅靠构造函数,每次生成的对象都不一样。

有时候需要在两个对象之间共享属性,由于JS在设计之初没有类的概念,所以JS使用函数的prototype来处理这部分需要被共享的属性,通过函数的prototype来模拟类:

当创建一个函数时,JS会自动为函数添加prototype属性,值是一个有constructor的对象。

以下是共享属性prototype的栗子:

function People(name) {
    this.name = name
}
People.prototype.age = 23 // 岁数
// 创建两个实例
let People1 = new People('OBKoro1')
let People2 = new People('扣肉')
People.prototype.age = 24 // 长大了一岁
console.log(People1.age, People2.age) // 24 24

为什么People1People2可以访问到People.prototype.age

原因是:People1People2的原型是People.prototype,答案在下方的:构造函数是什么以及它做了什么。

原型链

__proto__Object.getPrototypeOf(target): 对象的原型

__proto__ 是对象实例和它的构造函数之间建立的链接,它的值是:构造函数的`prototype。

也就是说:__proto__ 的值是它所对应的原型对象,是某个函数的prototype

Object.getPrototypeOf(target)全等于__proto__

它是ES6的标准,兼容IE9,主流浏览器也都支持,MDN,本文将以Object.getPrototypeOf(target)指代__proto__

不要再使用__proto__:

本段摘自阮一峰-ES6入门,具体解析请点击链接查看
  1. __proto__属性没有写入 ES6 的正文,而是写入了附录。
  2. 原因是它本质上是一个内部属性,而不是一个正式的对外的 API,只是由于浏览器广泛支持,才被加入了 ES6
  3. 标准明确规定,只有浏览器必须部署这个属性,其他运行环境不一定需要部署,而且新的代码最好认为这个属性是不存在的
  4. 所以无论从语义的角度,还是从兼容性的角度,都不要使用这个属性,应该使用:Object.getPrototypeOf(target)(读操作)、Object.setPrototypeOf(target)(写操作)、Object.create(target)(生成操作)代替

构造函数是什么、它做了什么

出自《你不知道的js》:在js中, 实际上并不存在所谓的'构造函数',只有对于函数的'构造调用'。

上文一直提到构造函数,所谓的构造函数,实际上就是通过关键字new来调用的函数:

let newObj = new someFn() // 构造调用函数

构造/new调用函数的时候做了什么

  1. 创建一个全新的对象。
  2. 这个新对象的原型(Object.getPrototypeOf(target))指向构造函数的prototype对象。
  3. 该函数的this会绑定在新创建的对象上。
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
  5. 我们称这个新对象为构造函数的实例。

原型继承就是利用构造调用函数的特性

SubType.prototype = new SuperType();  // 原型继承:SubType继承SuperType
SubType.prototype.constructor = SubType // 重新指定constructor指向 方便找到构造函数
// 挂载SuperType的this和prototype的属性和方法到SubType.prototype上
  1. 构造调用的第二点:将新对象的Object.getPrototypeOf(target)指向函数的prototype
  2. 构造调用的第三点:该函数的this会绑定在新创建的对象上。(所以父类this声明的属性被所有子类实例共享)
  3. 新对象赋值给SubType.prototype

原型链是什么

来看个例子:

function foo() { }
const newObj = new foo() // 构造调用foo 返回一个新对象
const newObj__proto__ = Object.getPrototypeOf(newObj) // 获取newObj的原型对象
newObj__proto__ === foo.prototype // true 验证newObj的原型指向foo
const foo__proto__ = Object.getPrototypeOf(foo.prototype) // 获取foo.prototype的原型
foo__proto__ === Object.prototype // true foo.prototype的原型是Object.prototype

如果用以前的语法,从newObj查找foo的原型,是这样的:

newObj.__proto__.__proto__ // 这种关系就是原型链

可以用以下三句话来理解原型链

  1. 每个对象都拥有一个原型对象: newObj的原型是foo.prototype
  2. 对象的原型可能也是继承其他原型对象的: foo.prototype也有它的原型Object.prototype
  3. 一层一层的,以此类推,这种关系就是原型链

一个对象是否在另一个对象的原型链上

如果一个对象存在另一个对象的原型链上,我们可以说:它们是继承关系。

判断方式有两种,但都是根据构造函数的prototype是否在原型链上来判断的:

  1. instanceof : 用于测试构造函数的prototype属性是否出现在对象的原型链中的任何位置

语法:object instanceof constructor

let test = function () { }
let testObject = new test();
testObject instanceof test // true test.prototype在testObject的原型链上
 testObject instanceof Function // false Function.prototype 不在testObject的原型链上
testObject instanceof Object // true Object.prototype在testObject的原型链上
  1. isPrototypeOf :测试一个对象是否存在于另一个对象的原型链上

语法:prototypeObj.isPrototypeOf(object)

let test = function () { }
let testObject = new test();
test.prototype.isPrototypeOf(testObject) // true test.prototype在testObject的原型链上
Object.prototype.isPrototypeOf(testObject) // true Object.prototype在testObject的原型链上

原型链的终点: Object.prototype

Object.prototype是原型链的终点,所有对象都是从它继承了方法和属性。

Object.prototype没有原型对象

const proto = Object.getPrototypeOf(Object.prototype) // null

下面是两个验证例子,有疑虑的同学多写几个测试用例印证一下。

字符串原型链的终点Object.prototype

let test = '由String函数构造出来的'
let stringPrototype = Object.getPrototypeOf(test) // 字符串的原型
stringPrototype === String.prototype // true 字符串的原型是String对象
Object.getPrototypeOf(stringPrototype) === Object.prototype // true String对象的原型是Object对象

函数原型链的终点:Object.prototype

let test = function () { }
let fnPrototype = Object.getPrototypeOf(test)
fnPrototype === Function.prototype // true test的原型是Function.prototype
Object.getPrototypeOf(Function.prototype) === Object.prototype // true

原型链用来做什么?

属性查找:

如果试图访问对象(实例instance)的某个属性,会首先在对象内部寻找该属性,直至找不到,然后才在该对象的原型(instance.prototype)里去找这个属性,以此类推

我们用一个例子来形象说明一下:

let test = '由String函数构造出来的'
let stringPrototype = Object.getPrototypeOf(test) // 字符串的原型
stringPrototype === String.prototype // true 字符串的原型是String对象
Object.getPrototypeOf(stringPrototype) === Object.prototype // true String对象的原型是Object对象

当你访问test的某个属性时,浏览器会进行以下查找:

  1. 浏览器首先查找test 本身
  2. 接着查找它的原型对象:String.prototype
  3. 最后查找String.prototype的原型对象:Object.prototype
  4. 一旦在原型链上找到该属性,就会立即返回该属性,停止查找。
  5. 原型链上的原型都没有找到的话,返回undefiend

这种查找机制还解释了字符串为何会有自带的方法: slice/split/indexOf等。

准确的说:

  • 这些属性和方法是定义在String这个全局对象/函数上的。
  • 字符串的原型指向了String函数的prototype
  • 之后通过查找原型链,在String函数的prototype中找到这些属性和方法。

拒绝查找原型链:

hasOwnProperty: 指示对象自身属性中是否具有指定的属性

语法:obj.hasOwnProperty(prop)

参数: prop 要查找的属性

返回值: 用来判断某个对象是否含有指定的属性的Boolean

let test ={ 'OBKoro1': '扣肉' }
test.hasOwnProperty('OBKoro1');  // true
test.hasOwnProperty('toString'); // false test本身没查找到toString 

这个API是挂载在object.prototype上,所有对象都可以使用,API会忽略掉那些从原型链上继承到的属性。

扩展:

实例的属性

你知道构造函数的实例对象上有哪些属性吗?这些属性分别挂载在哪个地方?原因是什么?

function foo() {
    this.some = '222'
    let ccc = 'ccc'
    foo.obkoro1 = 'obkoro1'
    foo.prototype.a = 'aaa'
}
foo.koro = '扣肉'
foo.prototype.test = 'test'
let foo1 = new foo() // `foo1`上有哪些属性,这些属性分别挂载在哪个地方
foo.prototype.test = 'test2' // 重新赋值

上面这道是考察JS基础的题,很多人都没说对,原因是没有彻底掌握this原型链函数

想一下再看解析:

想一下再看解析:

想一下再看解析:

想一下再看解析:

想一下再看解析:

  1. this.somefoo1对象的属性

通过构造调用foothis指向foo1,所以this.some挂载在foo1对象下。

属性查找: foo1.some

foo1.some直接读取foo1的属性。

  1. foo1.testfoo1.afoo1对象的原型

根据上文提到的:构造/new调用函数的时候会创建一个新对象(foo1),自动将foo1的原型(Object.getPrototypeOf(foo1))指向构造函数的prototype对象。

构造调用会执行函数,所以 foo.prototype.a = 'aaaaa'也会执行,单就赋值这个层面来说写在foo外面和写在foo里面是一样的。

属性查找:foo1.testfoo1.a

  • foo1本身没有找到,继续查找
  • foo1的原型Object.getPrototypeOf(foo1)上找到了atest,返回它们,停止查找。
  1. foo1.obkoro1foo1.koro:返回undefined

静态属性: foo.obkoro1foo.koro

函数在JS中是一等公民,它也是一个对象, 用来模拟类。

这两个属性跟foo1没有关系,它是对象foo上的两个属性(类似函数的:arguments/prototype/length等属性),称为静态属性

它们只能通过foo.obkoro1foo.koro来访问。

原型对象改变,原型链下游获取的值也会改变

上面那个例子中的foo1.test的值是什么?

foo.prototype.test = 'test'
let foo1 = new foo() // `foo1`上有哪些属性,这些属性分别挂载在哪个地方
foo.prototype.test = 'test2' // 重新赋值

foo1.test的值是test2,原因是:foo1的原型对象是Object.getPrototypeOf(foo1)存的指针,指向foo.prototype的内存地址,不是拷贝,每次读取的值都是当前foo.prototype的最新值。

打印foo1

小结

写了好几天,之前网上很多图文博客,那些线指来指去,就我个人看来还是比较难以理解的,所以本文纯文字的形式来描述这些概念,相信认真看完的同学肯定都有所收获,如果没看懂的话,建议多看几遍,这部分概念真的很重要!

PS:实际上还有很多引申出来的东西没写全,准备放到其他文章中去写。

觉得我的博客对你有帮助的话,就给我点个Star吧!

前端进阶积累公众号GitHub、wx:OBkoro1、邮箱:obkoro1@foxmail.com

以上2019/8/25

作者:OBKoro1

参考资料:

MDN:对象原型

JS原型链与继承别再被问倒了

从__proto__和prototype来深入理解JS对象和原型链

查看原文

赞 62 收藏 45 评论 2

OBKoro1 发布了文章 · 2019-08-09

前端中等算法-无重复字符的最长子串

无重复字符的最长子串

难度:中等

描述:

给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。

样例:

  • 输入: "abcabcbb"

输出: 3

解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

  • 输入: "bbbbb"

输出: 1

解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

  • 输入: "pwwkew"

输出: 3

解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。

  • 输入: "dvdf"

输出: 3

解释: 因为无重复字符的最长子串是 "vdf",所以其长度为 3。

  • 输入: "asjrgapa"

输出: 6

解释: 因为无重复字符的最长子串是 "sjrgap",所以其长度为 6。

  • 输入: "aabaab!bb"

输出: 3

解释: 因为无重复字符的最长子串是 "ab!",所以其长度为 3。

  • 输入: "abcb"

输出: 3

解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

  • 输入: "asljlj"

输出: 4

解释: 因为无重复字符的最长子串是 "aslj",所以其长度为 4。

  • 输入: "qwnfenpglqdq"

输出: 8

解释: 因为无重复字符的最长子串是 "fenpglqd",所以其长度为 8。

思路分析:

关键在于在出现重复字符时,如何更新不重复字符的index

代码模板:

/**
 * @param {string} s
 * @return {number}
 */
var lengthOfLongestSubstring = function (s) {
}

代码:

  1. 用对象储存字符的位置, 出现重复字符时更新不重复字符的index。
var lengthOfLongestSubstring = function (s) {
    let obj = {}; // 用于储存字符出现的位置
    let res = 0; // 最大值
    let j = 0; // 不重复字符的index
    for (let i = 0; i < s.length; i++) {
        // 当前值是否在对象中存储过
        const value = obj[s[i]]
        if (value !== undefined) {
            // 更新上一次重复值的index
            // value + 1 跳过之前重复的字符
            // j: 之前不重复的index 重复字符 需要全部跳过
            j = Math.max(value + 1, j)

        }
        // 每个字符都计算一下最长不重复值 保存最大值
        // 不重复最长长度 = 当前index - 上一次重复值的index + index从0开始 长度从1开始
        res = Math.max(res, i - j + 1);
        // 存/更新 字符串index
        obj[s[i]] = i
    }
    return res;
};
  1. 从左到右,一个字符一个字符搜索,看是否重复。
var lengthOfLongestSubstring = function (s) {
    var i = 0, // 不重复字符的index
        res = 0; // 更新无重复字符的长度
    for (j = 0; j < s.length; j++) {
        // 查找:不重复字符-当前index之间 有没有出现当前字符
        let index = s.slice(i, j).indexOf(s[j])
        if (index === -1) {
            // 更新无重复字符的长度:当前index-不重复字符的index + 长度从1开始算
            res = Math.max(res, j - i + 1);
        } else {
            // 更新i = 不重复字符的index
            // 不重复字符的index = 原不重复的字符index + i-j中出现重复字符的index + 跳过该重复字符
            i = i + index + 1;
        }
    }
    return res;
};

<!-- 特殊字符串:用于修改/删除markdown的结尾提示语-OBKoro1 -->

点个Star支持我一下~

查看原文

赞 12 收藏 9 评论 2

OBKoro1 关注了用户 · 2019-08-05

公子 @lizheming

  • 额米那个陀佛,无量那个天尊!
  • SF啥时候出注销功能啊

关注 4001

OBKoro1 发布了文章 · 2019-08-05

js基础-面试官想了解你有多理解call,apply,bind?

函数原型链中的 apply,call 和 bind 方法是 JavaScript 中相当重要的概念,与 this 关键字密切相关,相当一部分人对它们的理解还是比较浅显,所谓js基础扎实,绕不开这些基础常用的API,这次让我们来彻底掌握它们吧!

目录

  1. call,apply,bind的基本介绍
  2. call/apply/bind的核心理念:借用方法
  3. call和apply的应用场景
  4. bind的应用场景
  5. 中高级面试题:手写call/apply、bind

call,apply,bind的基本介绍

语法:

fun.call(thisArg, param1, param2, ...)
fun.apply(thisArg, [param1,param2,...])
fun.bind(thisArg, param1, param2, ...)

返回值:

call/apply:fun执行的结果
bind:返回fun的拷贝,并拥有指定的this值和初始参数

参数

thisArg(可选):

  1. funthis指向thisArg对象
  2. 非严格模式下:thisArg指定为null,undefined,fun中的this指向window对象.
  3. 严格模式下:funthisundefined
  4. 值为原始值(数字,字符串,布尔值)的this会指向该原始值的自动包装对象,如 String、Number、Boolean

param1,param2(可选): 传给fun的参数。

  1. 如果param不传或为 null/undefined,则表示不需要传入任何参数.
  2. apply第二个参数为数组,数组内的值为传给fun的参数。

调用call/apply/bind的必须是个函数

call、apply和bind是挂在Function对象上的三个方法,只有函数才有这些方法。

只要是函数就可以,比如: Object.prototype.toString就是个函数,我们经常看到这样的用法:Object.prototype.toString.call(data)

作用:

改变函数执行时的this指向,目前所有关于它们的运用,都是基于这一点来进行的。

如何不弄混call和apply

弄混这两个API的不在少数,不要小看这个问题,记住下面的这个方法就好了。

apply是以a开头,它传给fun的参数是Array,也是以a开头的。

区别:

call与apply的唯一区别

传给fun的参数写法不同:

  • apply是第2个参数,这个参数是一个数组:传给fun参数都写在数组中。
  • call从第2~n的参数都是传给fun的。

call/apply与bind的区别

执行

  • call/apply改变了函数的this上下文后马上执行该函数
  • bind则是返回改变了上下文后的函数,不执行该函数

返回值:

  • call/apply 返回fun的执行结果
  • bind返回fun的拷贝,并指定了fun的this指向,保存了fun的参数。

返回值这段在下方bind应用中有详细的示例解析。

call/apply/bind的核心理念:借用方法

看到一个非常棒的例子

生活中:

平时没时间做饭的我,周末想给孩子炖个腌笃鲜尝尝。但是没有适合的锅,而我又不想出去买。所以就问邻居借了一个锅来用,这样既达到了目的,又节省了开支,一举两得。

程序中:

A对象有个方法,B对象因为某种原因也需要用到同样的方法,那么这时候我们是单独为 B 对象扩展一个方法呢,还是借用一下 A 对象的方法呢?

当然是借用 A 对象的方法啦,既达到了目的,又节省了内存。

这就是call/apply/bind的核心理念:借用方法

借助已实现的方法,改变方法中数据的this指向,减少重复代码,节省内存。

call和apply的应用场景:

这些应用场景,多加体会就可以发现它们的理念都是:借用方法
  1. 判断数据类型:

Object.prototype.toString用来判断类型再合适不过,借用它我们几乎可以判断所有类型的数据:

function isType(data, type) {
    const typeObj = {
        '[object String]': 'string',
        '[object Number]': 'number',
        '[object Boolean]': 'boolean',
        '[object Null]': 'null',
        '[object Undefined]': 'undefined',
        '[object Object]': 'object',
        '[object Array]': 'array',
        '[object Function]': 'function',
        '[object Date]': 'date', // Object.prototype.toString.call(new Date())
        '[object RegExp]': 'regExp',
        '[object Map]': 'map',
        '[object Set]': 'set',
        '[object HTMLDivElement]': 'dom', // document.querySelector('#app')
        '[object WeakMap]': 'weakMap',
        '[object Window]': 'window',  // Object.prototype.toString.call(window)
        '[object Error]': 'error', // new Error('1')
        '[object Arguments]': 'arguments',
    }
    let name = Object.prototype.toString.call(data) // 借用Object.prototype.toString()获取数据类型
    let typeName = typeObj[name] || '未知类型' // 匹配数据类型
    return typeName === type // 判断该数据类型是否为传入的类型
}
console.log(
    isType({}, 'object'), // true
    isType([], 'array'), // true
    isType(new Date(), 'object'), // false
    isType(new Date(), 'date'), // true
)
  1. 类数组借用数组的方法:

类数组因为不是真正的数组所有没有数组类型上自带的种种方法,所以我们需要去借用数组的方法。

比如借用数组的push方法:

var arrayLike = {
  0: 'OB',
  1: 'Koro1',
  length: 2
}
Array.prototype.push.call(arrayLike, '添加元素1', '添加元素2');
console.log(arrayLike) // {"0":"OB","1":"Koro1","2":"添加元素1","3":"添加元素2","length":4}
  1. apply获取数组最大值最小值:

apply直接传递数组做要调用方法的参数,也省一步展开数组,比如使用Math.maxMath.min来获取数组的最大值/最小值:

const arr = [15, 6, 12, 13, 16];
const max = Math.max.apply(Math, arr); // 16
const min = Math.min.apply(Math, arr); // 6
  1. 继承

ES5的继承也都是通过借用父类的构造方法来实现父类方法/属性的继承:

// 父类
function supFather(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green']; // 复杂类型
}
supFather.prototype.sayName = function (age) {
    console.log(this.name, 'age');
};
// 子类
function sub(name, age) {
    // 借用父类的方法:修改它的this指向,赋值父类的构造函数里面方法、属性到子类上
    supFather.call(this, name);
    this.age = age;
}
// 重写子类的prototype,修正constructor指向
function inheritPrototype(sonFn, fatherFn) {
    sonFn.prototype = Object.create(fatherFn.prototype); // 继承父类的属性以及方法
    sonFn.prototype.constructor = sonFn; // 修正constructor指向到继承的那个函数上
}
inheritPrototype(sub, supFather);
sub.prototype.sayAge = function () {
    console.log(this.age, 'foo');
};
// 实例化子类,可以在实例上找到属性、方法
const instance1 = new sub("OBKoro1", 24);
const instance2 = new sub("小明", 18);
instance1.colors.push('black')
console.log(instance1) // {"name":"OBKoro1","colors":["red","blue","green","black"],"age":24}
console.log(instance2) // {"name":"小明","colors":["red","blue","green"],"age":18} 

类似的应用场景还有很多,就不赘述了,关键在于它们借用方法的理念,不理解的话多看几遍。

call、apply,该用哪个?、

call,apply的效果完全一样,它们的区别也在于

  • 参数数量/顺序确定就用call,参数数量/顺序不确定的话就用apply
  • 考虑可读性:参数数量不多就用call,参数数量比较多的话,把参数整合成数组,使用apply。
  • 参数集合已经是一个数组的情况,用apply,比如上文的获取数组最大值/最小值。

参数数量/顺序不确定的话就用apply,比如以下示例:

const obj = {
    age: 24,
    name: 'OBKoro1',
}
const obj2 = {
    age: 777
}
callObj(obj, handle)
callObj(obj2, handle)
// 根据某些条件来决定要传递参数的数量、以及顺序
function callObj(thisAge, fn) {
    let params = []
    if (thisAge.name) {
        params.push(thisAge.name)
    }
    if (thisAge.age) {
        params.push(thisAge.age)
    }
    fn.apply(thisAge, params) // 数量和顺序不确定 不能使用call
}
function handle(...params) {
    console.log('params', params) // do some thing
}

bind的应用场景:

1. 保存函数参数:

首先来看下一道经典的面试题:

for (var i = 1; i <= 5; i++) {
   setTimeout(function test() {
        console.log(i) // 依次输出:6 6 6 6 6
    }, i * 1000);
}

造成这个现象的原因是等到setTimeout异步执行时,i已经变成6了。

关于js事件循环机制不理解的同学,可以看我这篇博客:Js 的事件循环(Event Loop)机制以及实例讲解

那么如何使他输出: 1,2,3,4,5呢?

方法有很多:

  • 闭包, 保存变量
for (var i = 1; i <= 5; i++) {
    (function (i) {
        setTimeout(function () {
            console.log('闭包:', i); // 依次输出:1 2 3 4 5
        }, i * 1000);
    }(i));
}

在这里创建了一个闭包,每次循环都会把i的最新值传进去,然后被闭包保存起来。

  • bind
for (var i = 1; i <= 5; i++) {
    // 缓存参数
    setTimeout(function (i) {
        console.log('bind', i) // 依次输出:1 2 3 4 5
    }.bind(null, i), i * 1000);
}

实际上这里也用了闭包,我们知道bind会返回一个函数,这个函数也是闭包

它保存了函数的this指向、初始参数,每次i的变更都会被bind的闭包存起来,所以输出1-5。

具体细节,下面有个手写bind方法,研究一下,就能搞懂了。

  • let

let声明i也可以输出1-5: 因为let是块级作用域,所以每次都会创建一个新的变量,所以setTimeout每次读的值都是不同的,详解

2. 回调函数this丢失问题:

这是一个常见的问题,下面是我在开发VSCode插件处理webview通信时,遇到的真实问题,一开始以为VSCode的API哪里出问题,调试了一番才发现是this指向丢失的问题。

class Page {
    constructor(callBack) {
        this.className = 'Page'
        this.MessageCallBack = callBack // 
        this.MessageCallBack('发给注册页面的信息') // 执行PageA的回调函数
    }
}
class PageA {
    constructor() {
        this.className = 'PageA'
        this.pageClass = new Page(this.handleMessage) // 注册页面 传递回调函数 问题在这里
    }
    // 与页面通信回调
    handleMessage(msg) {
        console.log('处理通信', this.className, msg) //  'Page' this指向错误
    }
}
new PageA()

回调函数this为何会丢失?

显然声明的时候不会出现问题,执行回调函数的时候也不可能出现问题。

问题出在传递回调函数的时候:

this.pageClass = new Page(this.handleMessage)

因为传递过去的this.handleMessage是一个函数内存地址,没有上下文对象,也就是说该函数没有绑定它的this指向。

那它的this指向于它所应用的绑定规则

class Page {
    constructor(callBack) {
        this.className = 'Page'
        // callBack() // 直接执行的话 由于class 内部是严格模式,所以this 实际指向的是 undefined
        this.MessageCallBack = callBack // 回调函数的this 隐式绑定到class page
        this.MessageCallBack('发给注册页面的信息')
    }
}

既然知道问题了,那我们只要绑定回调函数的this指向为PageA就解决问题了。

回调函数this丢失的解决方案

  1. bind绑定回调函数的this指向:

这是典型bind的应用场景, 绑定this指向,用做回调函数。

this.pageClass = new Page(this.handleMessage.bind(this)) // 绑定回调函数的this指向

PS: 这也是为什么reactrender函数在绑定回调函数的时候,也要使用bind绑定一下this的指向,也是因为同样的问题以及原理。

  1. 箭头函数绑定this指向

箭头函数的this指向定义的时候外层第一个普通函数的this,在这里指的是class类:PageA

这块内容,可以看下我之前写的博客:详解箭头函数和普通函数的区别以及箭头函数的注意事项、不适用场景

this.pageClass = new Page(() => this.handleMessage()) // 箭头函数绑定this指向

中高级面试题-手写call/apply、bind:

在大厂的面试中,手写实现call,apply,bind(特别是bind)一直是比较高频的面试题,在这里我们也一起来实现一下这几个函数。

你能手写实现一个call吗?

思路

  1. 根据call的规则设置上下文对象,也就是this的指向。
  2. 通过设置context的属性,将函数的this指向隐式绑定到context上
  3. 通过隐式绑定执行函数并传递参数。
  4. 删除临时属性,返回函数执行结果
Function.prototype.myCall = function (context, ...arr) {
    if (context === null || context === undefined) {
       // 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window)
        context = window 
    } else {
        context = Object(context) // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象
    }
    const specialPrototype = Symbol('特殊属性Symbol') // 用于临时储存函数
    context[specialPrototype] = this; // 函数的this指向隐式绑定到context上
    let result = context[specialPrototype](...arr); // 通过隐式绑定执行函数并传递参数
    delete context[specialPrototype]; // 删除上下文对象的属性
    return result; // 返回函数执行结果
};

判断函数的上下文对象:

很多人判断函数上下文对象,只是简单的以context是否为false来判断,比如:

// 判断函数上下文绑定到`window`不够严谨
context = context ? Object(context) : window; 
context = context || window; 

经过测试,以下三种为false的情况,函数的上下文对象都会绑定到window上:

// 网上的其他绑定函数上下文对象的方案: context = context || window; 
function handle(...params) {
    this.test = 'handle'
    console.log('params', this, ...params) // do some thing
}
handle.elseCall('') // window
handle.elseCall(0) // window
handle.elseCall(false) // window

call则将函数的上下文对象会绑定到这些原始值的实例对象上:

原始值的实例对象

所以正确的解决方案,应该是像我上面那么做:

// 正确判断函数上下文对象
    if (context === null || context === undefined) {
       // 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window)
        context = window 
    } else {
        context = Object(context) // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象
    }

使用Symbol临时储存函数

尽管之前用的属性是testFn但不得不承认,还是有跟上下文对象的原属性冲突的风险,经网友提醒使用Symbol就不会出现冲突了。

考虑兼容的话,还是用尽量特殊的属性,比如带上自己的ID:OBKoro1TestFn

你能手写实现一个apply吗?

思路:

  1. 传递给函数的参数处理,不太一样,其他部分跟call一样。
  2. apply接受第二个参数为类数组对象, 这里用了JavaScript权威指南中判断是否为类数组对象的方法。
Function.prototype.myApply = function (context) {
    if (context === null || context === undefined) {
        context = window // 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window)
    } else {
        context = Object(context) // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象
    }
    // JavaScript权威指南判断是否为类数组对象
    function isArrayLike(o) {
        if (o &&                                    // o不是null、undefined等
            typeof o === 'object' &&                // o是对象
            isFinite(o.length) &&                   // o.length是有限数值
            o.length >= 0 &&                        // o.length为非负值
            o.length === Math.floor(o.length) &&    // o.length是整数
            o.length < 4294967296)                  // o.length < 2^32
            return true
        else
            return false
    }
    const specialPrototype = Symbol('特殊属性Symbol') // 用于临时储存函数
    context[specialPrototype] = this; // 隐式绑定this指向到context上
    let args = arguments[1]; // 获取参数数组
    let result
    // 处理传进来的第二个参数
    if (args) {
        // 是否传递第二个参数
        if (!Array.isArray(args) && !isArrayLike(args)) {
            throw new TypeError('myApply 第二个参数不为数组并且不为类数组对象抛出错误');
        } else {
            args = Array.from(args) // 转为数组
            result = context[specialPrototype](...args); // 执行函数并展开数组,传递函数参数
        }
    } else {
        result = context[specialPrototype](); // 执行函数 
    }
    delete context[specialPrototype]; // 删除上下文对象的属性
    return result; // 返回函数执行结果
};

你能手写实现一个bind吗?

划重点

手写bind是大厂中的一个高频的面试题,如果面试的中高级前端,只是能说出它们的区别,用法并不能脱颖而出,理解要有足够的深度才能抱得offer归!

思路

  1. 拷贝源函数:

    • 通过变量储存源函数
    • 使用Object.create复制源函数的prototype给fToBind
  2. 返回拷贝的函数
  3. 调用拷贝的函数:

    • new调用判断:通过instanceof判断函数是否通过new调用,来决定绑定的context
    • 绑定this+传递参数
    • 返回源函数的执行结果

2019/8/26更新:修复函数没有prototype的情况

Function.prototype.myBind = function (objThis, ...params) {
    const thisFn = this; // 存储源函数以及上方的params(函数参数)
    // 对返回的函数 secondParams 二次传参
    let fToBind = function (...secondParams) {
        const isNew = this instanceof fToBind // this是否是fToBind的实例 也就是返回的fToBind是否通过new调用
        const context = isNew ? this : Object(objThis) // new调用就绑定到this上,否则就绑定到传入的objThis上
        return thisFn.call(context, ...params, ...secondParams); // 用call调用源函数绑定this的指向并传递参数,返回执行结果
    };
    if (thisFn.prototype) {
        // 复制源函数的prototype给fToBind 一些情况下函数没有prototype,比如箭头函数
        fToBind.prototype = Object.create(thisFn.prototype);
    }
    return fToBind; // 返回拷贝的函数
};

对象缩写方法没有prototype

箭头函数没有prototype,这个我知道的,可是getInfo2就是一个缩写,为什么没有prototype

谷歌/stack overflow都没有找到原因,有大佬指点迷津一下吗??

var student = {
    getInfo: function (name, isRegistered) {
        console.log('this1', this)
    },
    getInfo2(name, isRegistered) {
        console.log('this2', this) // 没有prototype
    },
    getInfo3: (name, isRegistered) => {
        console.log('this3', this) // 没有prototype
    }
}

小结

本来以为这篇会写的很快,结果断断续续的写了好几天,终于把这三个API相关知识介绍清楚了,希望大家看完之后,面试的时候再遇到这个问题,就可以海陆空全方位的装逼了^_^

觉得我的博客对你有帮助的话,就给我点个Star吧!

前端进阶积累公众号GitHub、wx:OBkoro1、邮箱:obkoro1@foxmail.com

查看原文

赞 87 收藏 70 评论 14

OBKoro1 赞了文章 · 2019-07-05

精读《Serverless 给前端带来了什么》

1. 引言

Serverless 是一种 “无服务器架构”,让用户无需关心程序运行环境、资源及数量,只要将精力 Focus 到业务逻辑上的技术。

现在公司已经实现 DevOps 化,正在向 Serverless 迈进,而为什么前端要关注 Serverless?

对业务前端同学:

  1. 会改变前后端接口定义规范。
  2. 一定会改变前后端联调方式,让前端参与服务器逻辑开发,甚至 Node Java 混部。
  3. 大大降低 Nodejs 服务器维护门槛,只要会写 JS 代码就可以维护 Node 服务,而无需学习 DevOps 相关知识。

对一个自由开发者:

  1. 未来服务器部署更弹性,更省钱。
  2. 部署速度更快,更不易出错。

前端框架总是带入后端思维,而 Serverless 则是把前端思维带入了后端运维。

前端开发者其实是最早享受到 “Serverless” 好处的群体。他们不需要拥有自己的服务,甚至不需要自己的浏览器,就可以让自己的 JS 代码均匀、负载均衡的运行在每一个用户的电脑中。

而每个用户的浏览器,就像现在最时髦,最成熟的 Serverless 集群,从远程加载 JS 代码开始冷启动,甚至在冷启动上也是卓越领先的:利用 JIT 加速让代码实现毫秒级别的冷启动。不仅如此,浏览器还是实现了 BAAS 服务的完美环境,我们可以调用任何函数获取用户的 Cookie、环境信息、本地数据库服务,而无需关心用户用的是什么电脑,连接了怎样的网络,甚至硬盘的大小。

这就是 Serverless 理念。通过 FAAS(函数及服务)与 BAAS(后台及服务)企图在服务端制造前端开发者习以为常的开发环境,所以前端开发者应该更能理解 Serverless 带来的好处。

2. 精读

FAAS(函数即服务) + BAAS(后台即服务) 可以称为一个完整的 Serverless 的实现,除此之外,还有 PASS(平台即服务)的概念。而通常平台环境都通过容器技术实现,最终都为了达到 NoOps(无人运维),或者至少 DevOps(开发&运维)。

简单介绍一下这几个名词,防止大家被绕晕:

FAAS - Function as a service

函数即服务,每一个函数都是一个服务,函数可以由任何语言编写,除此之外不需要关心任何运维细节,比如:计算资源、弹性扩容,而且可以按量计费,且支持事件驱动。业界大云厂商都支持 FAAS,各自都有一套工作台、或者可视化工作流来管理这些函数。

BAAS - Backend as a service

后端及服务,就是集成了许多中间件技术,可以无视环境调用服务,比如数据即服务(数据库服务),缓存服务等。虽然下面还有很多 XASS,但组成 Serverless 概念的只有 FAAS + BAAS。

PAAS - Platform as a service

平台即服务,用户只要上传源代码就可以自动持续集成并享受高可用服务,如果速度足够快,可以认为是类似 Serverless。但随着以 Docker 为代表的容器技术兴起,以容器为粒度的 PASS 部署逐渐成为主流,是最常用的应用部署方式。比如中间件、数据库、操作系统等。

DAAS - Data as a service

数据即服务,将数据采集、治理、聚合、服务打包起来提供出去。DASS 服务可以应用 Serverless 的架构。

IAAS - Infrastructure as a Service

基础设施即服务,比如计算机存储、网络、服务器等基建设施以服务的方式提供。

SAAS - Software as a Service

软件即服务,比如 ERP、CRM、邮箱服务等,以软件为粒度提供服务。

容器

容器就是隔离了物理环境的虚拟程序执行环境,而且环境可被描述、迁移。比较热门的容器技术是 Docker。

随着容器数量增多,就出现了管理容器集群的技术,比较有名的容器编排平台是 Kubernetes。容器技术是 Serverless 架构实现的一种选择,也是实现的基础。

NoOps

就是无人运维,比较理想主义,也许要借助 AI 的能力才能实现完全无人运维。

无人运维不代表 Serverless,Serverless 可能也需要人运维(至少现在),只是开发者不再需要关心环境。

DevOps

笔者觉得可以理解为 “开发即运维”,毕竟出了事情,开发要被问责,而一个成熟的 DevOps 体系可以让更多的开发者承担 OP 的职责,或者与 OP 更密切的合作。


回到 Serverless,未来后端开发的体验可能与前端相似:不需要关心代码运行在哪台服务器(浏览器),无需关心服务器环境(浏览器版本)、不用担心负载均衡(前端从未担心过)、中间件服务随时调用(LocalStorage、Service Worker)

前端同学对 Serverless 应该尤为激动。就拿笔者亲身经历举例吧。

从做一款游戏说起

笔者非常迷恋养成类游戏,养成游戏最常见的就是资源建造、收集,或者挂机时计算资源的 读秒规则。笔者在开发游戏的时候,最初是将客户端代码与服务端代码完全分成两套实现的:

// ... UI 部分,画出一个倒计时伐木场建造进度条
const currentTime = await requestBuildingProcess();
const leftTime = new Date().getTime() - currentTime;
// ... 继续倒计时读条
// 读条完毕后,每小时木头产量 + 100,更新到客户端计时器
store.woodIncrement += 100;

为了游戏体验,用户可以在不刷新浏览器的情况下,看到伐木场建造进度的读条,以及 嘭 一下建造完毕,并且发现每秒钟多获得了 100 点木材!但是当伐木场 建造完成前、完成时、完成后的任意时间点刷新浏览器,都要保持逻辑的统一,而且数据需要在后端离线计算。 此时就要写后端代码了:

// 每次登陆时,校验当前登陆
const currentTime = new Date().getTime()
// 获取伐木场当前状态
if ( /* 建造中 */) {
  // 返回给客户端当前时间
  const leftTime = building.startTime - currentTime
  res.body = leftTime
} else {
  // 建造完毕
  store.woodIncrement += 100
}

很快,建筑的种类多了起来,不同的状态、等级产量都不同,前后端分开维护成本会越来越大,我们需要做配置同步。

配置同步

为了做前后端配置同步,可以将配置单独托管起来前后端共用,比如新建一个配置文件,专门存储游戏信息:

export const buildings = {
  wood: {
    name: "..",
    maxLevel: 100,
    increamentPerLevel: 50,
    initIncreament: 100
  }
  /* .. and so on .. */
};

这虽然复用了配置,但前后端都有一些共同的逻辑可以复用,比如 根据建筑建造时间判断建筑状态,判断 N 秒后建筑的产量等等。 而 Serverless 带来了进一步优化的空间。

在 Serverless 环境下做游戏

试想一下,可以在服务器以函数粒度执行代码,我们可以这样抽象游戏逻辑:

// 根据建筑建造时间判断建筑状态
export const getBuildingStatusByTime = (instanceId: number, time: number) => {
  /**/
};

// 判断建筑生产量
export const getBuildingProduction = (instanceId: number, lastTime: number) => {
  const status = getBuildingStatusByTime(instanceId, new Date().getTime());
  switch (status) {
    case "building":
      return 0;
    case "finished":
      // 根据 (当前时间 - 上次打开时间)* 每秒产量得到总产量
      return; /**/
  }
};

// 前端 UI 层,每隔一秒调用一次 getBuildingProduction 函数,及时更新生产数据
// 前端入口函数
export const frontendMain = () => {
  /**/
};

// 后端 根据每次打开时间,调用一次 getBuildingProduction 函数并入库
// 后端入口函数
export const backendMain = () => {
  /**/
};

利用 PASS 服务,将前后端逻辑写在一起,将 getBuildingProduction 函数片段上传至 FAAS 服务,这样就可以同时共享前后端逻辑了!

在文件夹视图下,可以做如下结构规划:

.
├── client    # 前端入口
├── server    # 后端入口
├── common    # 共享工具函数,可以包含 80% 的通用游戏逻辑

也许有人会问:前后端共享代码不止有 Serverless 才能做到。

的确如此,如果代码抽象足够好,有成熟的工程方案支持,是可以将一份代码分别导出到浏览器与服务器的。但 Serverless 基于函数粒度功能更契合前后端复用代码的理念,它的出现可能会推动更广泛的前后端代码复用,这虽然不是新发明,但足够称为一个伟大的改变。

前后端的视角

对于前端开发者,会发现后台服务变简单了。对于后端开发者,发现服务做厚了,面临的挑战更多了。

更简单的后台服务

传统 ECS 服务器在租赁时,CentOS 与 AliyunOS 的环境选择就足够让人烦恼。对个人开发者而言,我们要搭建一个完整的持续集成服务是很困难的,而且面临的选择很多,让人眼花缭乱:

  • 可以在服务器安装数据库等服务,本地直联服务器的数据库进行开发。
  • 可以本地安装 Docker 连接本地数据库服务,将环境打包成镜像整体部署到服务器。
  • 将前后端代码分离,前端代码在本地开发,服务端代码在服务器开发。

甚至服务器的稳定性,需要 PM2 等工具进行管理。当服务器面临攻击、重启、磁盘故障时,打开复杂的工作台或登陆 Shell 后一通操作才能恢复。这怎么让人专心把精力放在要做的事情上呢?

Serverless 解决了这个问题,因为我们要上传的只是一个代码片段,不再需要面对服务器、系统环境、资源等环境问题,外部服务也有封装好的 BAAS 体系支持。

实际上在 Serverless 出来之前,就有许多后端团队利用 FAAS 理念简化开发流程。

为了减少写后端业务逻辑时,环境、部署问题的干扰,许多团队会将业务逻辑抽象成一个个区块(Block),对应到代码片段或 Blockly,这些区块可以独立维护、发布,最后将这些代码片段注入到主程序中,或动态加载。如果习惯了这种开发方式,那也更容易接受 Serverless。

更厚的后台服务

站在后台角度,事情就变得比较复杂了。相对于提供简单的服务器和容器,现在要对用户屏蔽执行环境,将服务做得更厚。

笔者通过一些文章了解到,Serverless 的推行还面临着如下一些挑战:

  • Serverless 各厂商实现种类很多,想让业务做到多云部署,需要抹平差异。
  • 成熟的 PASS 服务其实是伪 Serverless,后续该如何标准化。
  • FAAS 冷启动需要重新加载代码、动态分配资源,导致冷启动速度很慢,除了预热,还需要经济的优化方式。
  • 对于高并发(比如双 11 秒杀)场景的应用,无需容量评估是很危险的事情,但如果真能做到完全弹性化,就省去了烦恼的容量评估。
  • 存量应用如何迁移。业界的 Serverless 服务厂商大部分都没有解决存量应用迁移的问题。
  • Serverless 的特性导致了无状态,而复杂的互联网应用都是有状态的,因此挑战在不改变开发习惯的情况下支持状态。

所幸的是,这些问题都已经在积极处理中,而且不少有了已经落地的解决方案。

Serverless 给后台带来的好处远比面临的挑战多:

  • 推进前后端一体化。进一步降低 Node 写服务端代码的门槛,免除应用运营的学习成本。笔者曾经遇到过自己申请的数据库服务被迁移到其他机房而导致的应用服务中断,以后再也不需要担心了,因为数据库作为 BAAS 服务,是不需要关心在哪部署,是否跨机房,以及如何做迁移的。
  • 提高资源利用效率。杜绝应用独占资源,换成按需加载必然能减少不必要的资源消耗,而且将服务均摊到集群的每一台机器,拉平集群的 CPU 水位。
  • 降低云平台使用门槛。无需运维,灵活拓展,按价值服务,高可用,这些能力在吸引更多客户的同时,完全按需计费的特性也减少了用户开销,达到双赢。

利用 Serverless 尝试服务开放

笔者在公司负责一个大型 BI 分析平台建设,BI 分析平台的底层能力之一就是可视化搭建。

那么可视化搭建能力该如何开放呢?现在比较容易做到的是组件开放,毕竟前端可以与后端设计相对解耦,利用 AMD 加载体系也比较成熟。

现在遇到的一个挑战就是后端能力开放,因为当对取数能力有定制要求时,可能需要定制后端数据处理的逻辑。目前能做到的是利用 maven3、jdk7 搭建本地开发环境测试,如果想上线,还需要后端同学的协助。

如果后端搭建一个特有的 Serverless BAAS 服务,那么就可以像前端组件一样进行线上 Coding,调试,甚至灰度发布进行预发测试。现在前端云端开发已经有了不少成熟的探索,Serverless 可以统一前后端代码在云端开发的体验,而不需要关心环境。

Serverless 应用架构设计

看了一些 Serverless 应用架构图,发现大部分业务都可以套用这样一张架构图:

将业务函数抽象成一个个 FAAS 函数,将数据库、缓存、加速等服务抽象成 BAAS 服务。

上层提供 Restful 或事件触发机制调用,对应到不同的端(PC、移动端)。

想要拓展平台能力,只要在端上做开放(组件接入)与 FAAS 服务做开放(后端接入)即可。

收益与挑战

Serverless 带来了的收益与挑战并存,本文站在前端角度聊一聊。

收益一:前端更 Focus 在前端体验技术,而不需要具备太多应用管理知识。

最近看了很多前端前辈写的总结文,最大的体会就是回忆 “前端在这几年到底起到了什么作用”。我们往往会夸大自己的存在感,其实前端存在的意义就是解决人机交互问题,大部分场景下,都是一种景上添花的作用,而不是必须。

回忆你最自豪的工作经历,可能是掌握了 Node 应用的运维知识、前端工程体系建设、研发效能优化、标准规范制定等,但真正对业务起效的部分,恰恰是你觉得写得最不值得一提的业务代码。前端花了太多的时间在周边技术上,而减少了很多对业务、交互的思考。

即便是大公司,也难以招到既熟练使用 Nodejs,又具备丰富运维知识的人,同时还要求他前端技术精湛,对业务理解深刻,鱼和熊掌几乎不可兼得。

Serverless 可以有效解决这个问题,前端同学只需要会写 JS 代码而无需掌握任何运维知识,就可以快速实现自己的整套想法。

诚然,了解服务端知识是有必要的,但站在合理分工的角度,前端就应该 focus 在前端技术上。前端的核心竞争力或者带来的业务价值,并不会随着了解多一些运维知识而得到补充,相反,这会吞噬掉我们本可以带来更多业务价值的时间。

语言的进化、浏览器的进化、服务器的进化,都是从复杂到简单,底层到封装的过程,而 serverless 是后端 + 运维作为一个整体的进一步封装的过程。

收益二:逻辑编排带来的代码高度复用、可维护,拓展 云+端 的能力。

云+端 是前端开发的下个形态,提供强大的云编码能力,或者通过插件将端打造为类似云的开发环境。其最大好处就是屏蔽前端开发环境细节,理念与 Serverless 类似。

之前有不少团队尝试过利用 Graphsql 让接口 “更有弹性”,而 Serverless 则是更彻底的方案。

我自己的团队就尝试过 Graphsql 方案,但由于业务非常复杂,难以用标准的模型描述所有场景的需求,因此不适合使用 Graphsql。恰恰是一套基于 Blockly 的可视化后端开发平台坚持了下来,而且取得了惊人的开发提效。这套 Blockly 通用化抽象后几乎可以由 Serverless 来代替。所以 Serverless 可以解决复杂场景下后端研发提效的问题。

Serverless 在融合了云端开发后,就可以通过逻辑编排进一步可视化调整函数执行顺序、依赖关系。

笔者之前在百度广告数据处理团队使用过这种平台计算离线日志,每个 MapReduce 计算节点经过可视化后,就可以轻松看出故障时哪个节点在阻塞,还可以看到最长执行链路,并为每个节点重新分配执行权重。即便逻辑编排不能解决开发的所有痛点,但在某个具体业务场景下一定可以大有作为。

挑战一:Serverless 可以完全取消前端转后端的门槛?

前端同学写 Node 代码最容易犯的毛病就是内存溢出。

浏览器 + Tab 天然是一个用完即关的场景,UI 组件与逻辑创建与销毁也非常频繁,因此前端同学很少,也不太需要关心 GC 问题。而 GC 在后端开发场景中是一个早已养成的习惯,因此 Nodejs 程序缓存溢出是大家最关注的问题。

Serverless 应用是动态加载,长时间不用就会释放的,因此一般来说不需要太担心 GC 的问题,就算内存溢出,在内存被占满前可能已经进程被释放,或者被监测到异常强制 Kill 掉。

但毕竟 FAAS 函数的加载与释放完全是由云端控制的,一个常用的函数长时间不卸载也是有可能的,因此 FAAS 函数还是要注意控制副作用。

所以 Serverless 虽然抹平了运维环境,但服务端基本知识还需要了解,必须意识到代码跑在前端还是后端。

挑战二:性能问题

Serverless 的冷启动会导致性能问题,而让业务方主动关心程序的执行频率或者性能要求,再开启预热服务又重新将研发拖入了运维的深渊中。

即便是业界最成熟的亚马逊 Serverless 云服务,也无法做到业务完全不关心调用频率,就可以轻松应付秒杀场景。

因此目前 Serverless 可能更适合结合合适的场景使用,而不是任何应用都强行套用 Serverless。

虽然可以通过定期运行 FAAS 服务来保证程序一直 Online,但笔者认为这还是违背了 Serverless 的理念。

挑战三:如何保证代码可迁移性

有一张很经典的 Serverless 定位描述图:

TB1i7PULkzoK1RjSZFlXXai4VXa-804-1184.png =00x300

网络、存储、服务、虚拟家、操作系统、中间件、运行时、数据都不需要关心了,甚至连应用层都只需要关心其中函数部分,而不需要关心其他比如启动、销毁部分。

前面总拿这点当优势,但也可以反过来认为是个劣势。 当你的代码完全依赖某个公有云环境后,你就失去了整体环境的掌控力,甚至代码都只能在特定的云平台才能运行。

不同云平台提供的 BAAS 服务规范可能不同,FAAS 的入口、执行方式也可能不同,想要采用多云部署就必须克服这个问题。

现在许多 Serverless 平台都在考虑做标准化,但同时也有一些自下而上的工具库抹平一些差异,比如 Serverless Framework 等。

而我们写 FAAS 函数时,也尽量将与平台绑定的入口函数写得轻一些,将真正的入口放在通用的比如 main 函数中。

3. 总结

Serverless 的价值远比挑战大,其理念可以切实解决许多研发效能问题。

但目前 Serverless 发展阶段仍处于早期,国内的 Serverless 也处于尝试阶段,而且执行环境存在诸多限制,也就是并没有完全实现 Serverless 的美好理念,因此如果什么都往上套一定会踩坑。

可能在 3-5 年后,这些坑会被填平,那么你是选择加入填坑大军,还是选一个合适的场景使用 Serverless 呢?

讨论地址是:精读《Serverless 给前端带来了什么》 · Issue #135 · dt-fe/weekly

如果你想参与讨论,请点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。

查看原文

赞 52 收藏 38 评论 5

OBKoro1 发布了文章 · 2019-06-05

博客图片失效?使用npm包20行代码一次下载/替换所有失效的外链图片

前言

大约一个月前,微博的图片外链失效了,以及掘金因为盗链问题也于2019/06/06决定开启防盗链,造成的影响是:个人博客网站的引用了这些图片外链都不能显示

目前微博和掘金的屏蔽,在CSDN和segmentfault都是可以正常显示的,只影响个人博客

比如前段时间我的博客:http://obkoro1.com上引用的微博图片都不能显示了。

因为我写博客比较频繁,被屏蔽的图片不在少数,肯定不能一个个手动的替换,查了一番没有找到现成的解决方案,做了个脚本工具,并且写了文档把它开源出来了。

markdown-img-down-site-change(下载/替换markdown中的图片)

搜索目标文件夹中的markdown文件,找到目标图片,提供下载图片,替换图片链接的功能-通常用于markdown 图片失效。

简介

这是一个极为轻量的脚本,引用包,设置好参数,通过API即可轻松上手。

解决什么问题?

  1. 集中下载markdown文件中某个域名下的图片到一个文件夹下。
  2. 用新的图片链接替换markdown文件中某个域名的图片链接。
// 1. 下载这两个图片
// ![](https://user-gold-cdn.xitu.io/2019/5/20/图片名字?w=2024&h=1240&f=png&s=339262)
// ![](https://user-gold-cdn.xitu.io/2018/6/16/图片名字)
// 2. 替换成:github的链接
![](https://raw.githubusercontent.com/OBKoro1/articleImg_src/master/juejin/图片名字?w=2024&h=1240&f=png&s=339262)
![](https://raw.githubusercontent.com/OBKoro1/articleImg_src/master/juejin/图片名字)

安装:

npm i markdown-img-down-site-change -S

文档:

Github

API

更新日志

数据安全:

刚上手可能不了解脚本的功能,需要调试一番,这时候万一把markdown文件给改坏了,岂不是要哭死?

脚本有两种形式来防止这种情况发生:

  1. 脚本会默认备份你的文件。
  2. 默认开启测试模式,等到调试的差不多了,可以关闭测试模式。
  3. 建议:再不放心的话,可以先用一两个文件来测试一下脚本

使用:20行代码不到

在项目中有一个使用栗子,里面加了蛮多注释和空行的,实际代码20行都不到,可以说很简单了,如下:

// npm i markdown-img-down-site-change -S 
const markdownImageDown = require('markdown-img-down-site-change'); // 文件模块

// 传参: 这也是脚本的默认参数,根据情况可以自行修改
let option = {
    replace_image_url: 'https://user-gold-cdn.xitu.io/',
    read_markdown_src: './source', // 要查找markdown文件的文件夹地址
    down_img_src: './juejin', // 下载图片到这个文件夹
    var_number: 3 // url前半部分的变量数量 比如上面的日期: /2019/5/20/、/2018/6/16/
}

// 初始化
const markdownImage = new markdownImageDown(option)

// 下载外链
markdownImage.checkDownImg();

// 上传下载下来的图片文件夹到云端 用户自己操作

// 上传图片之后 
// 脚本会把以前的外链替换成云端地址+拼接一个图片名
markdownImage.updateOption({
    new_image_url: 'https://xxx.com/目录地址/', // 图片上传的地址
    add_end: '?raw=true' // github图片地址有后缀 直接进去是仓库
})

// 替换外链 
// 把replace_image_url的字符串换成new_image_url字符串
markdownImage.replaceMarkdown();

运行:

仔细阅读文本,配置好参数之后

在项目根节点新建一个handleImg.js文件,安装一下脚本,然后用node运行该文件:

npm i markdown-img-down-site-change -S
node handleImg.js

功能/参数简介:

  • checkDownImg(): 下载查找到的图片
  • replaceMarkdown(): 替换图片链接为新的图片链接
  • replace_image_url:要替换的图片地址
  • new_image_url:图片的新地址
  • test: 测试模式。
  • var_number: 匹配图片链接的图片名之前的url,值为变量数量
  • is_link: 匹配链接。
  • write_file_time: 间隔多久修改markdown图片链接
  • read_markdown_src:要查找markdown文件的文件夹地址
  • down_img_src:下载图片到这个地址下
  • copy_item_data: 备份项目
  • filter_item: 过滤某些文件夹,不查找markdown。
  • add_end:在图片链接后面添加后缀添加后缀

欢迎试用

有需要的小伙伴,赶紧来试试吧!文档写的很全,上手非常轻松,项目将会持续维护,有什么问题,欢迎给我提issue~

如果觉得这个脚本还不错的话,就给项目点个Star吧!

博客前端积累文档公众号、wx:OBkoro1、邮箱:obkoro1@foxmail.com

以上2019.06.04

查看原文

赞 6 收藏 4 评论 1

OBKoro1 评论了文章 · 2019-05-21

js 调用栈机制与ES6尾调用优化介绍

调用栈的英文名叫做Call Stack,大家或多或少是有听过的,但是对于js调用栈的工作方式以及如何在工作中利用这一特性,大部分人可能没有进行过更深入的研究,这块内容可以说对我们前端来说就是所谓的基础知识,咋一看好像用处并没有很大,但掌握好这个知识点,就可以让我们在以后可以走的更远,走的更快!

博客前端积累文档公众号GitHub

目录

  1. 数据结构:栈
  2. 调用栈是什么?用来做什么?
  3. 调用栈的运行机制
  4. 调用栈优化内存
  5. 调用栈debug大法

数据结构:栈

栈是一种遵从后进先出(LIFO)原则的有序集合,新元素都靠近栈顶,旧元素都接近栈底。

生活中的栗子,帮助一下理解:

餐厅里面堆放的盘子(栈),一开始放的都在下面(先进),后面放的都在上面(后进),洗盘子的时候先从上面开始洗(先出)。

调用栈是什么?用来做什么?

  1. 调用栈是一种栈结构的数据,它是由调用侦组成的
  2. 调用栈记录了函数的执行顺序和函数内部变量等信息

调用栈的运行机制

机制

程序运行到一个函数,它就会将其添加到调用栈中,当从这个函数返回的时候,就会将这个函数从调用栈中删掉。

看一下例子帮助理解:

// 调用栈中的执行步骤用数字表示
printSquare(5); // 1 添加
function printSquare(x) {
    var s = multiply(x, x); // 2 添加 => 3 运行完成,内部没有再调用其他函数,删掉
    console.log(s); // 4 添加 => 5 删掉
    // 运行完成 删掉printSquare
}
function multiply(x, y) {
    return x * y;
}

调用栈中的执行步骤如下(删除multiply的步骤被省略了):

调用侦

每个进入到调用栈中的函数,都会分配到一个单独的栈空间,称为“调用侦”。

在调用栈中每个“调用侦”都对应一个函数,最上方的调用帧称为“当前帧”,调用栈是由所有的调用侦形成的。

找到一张图片,调用侦:

调用栈优化内存

调用栈的内存消耗

如上图,函数的变量等信息会被调用侦保存起来,所以调用侦中的变量不会被垃圾收集器回收

当函数嵌套的层级比较深了,调用栈中的调用侦比较多的时候,这些信息对内存消耗是非常大的。

针对这种情况除了我们要尽量避免函数层级嵌套的比较深之外,ES6提供了“尾调用优化”来解决调用侦过多,引起的内存消耗过大的问题。

何谓尾调用

尾调用指的是:函数的最后一步是调用另一个函数

function f(x){
  return g(x); // 最后一步调用另一个函数并且使用return
}
function f(x){
  g(x); // 没有return 不算尾调用 因为不知道后面还有没有操作
  // return undefined; // 隐式的return
}

尾调用优化优化了什么?

尾调用用来删除外层无用的调用侦,只保留内层函数的调用侦,来节省浏览器的内存。

下面这个例子调用栈中的调用侦一直只有一项,如果不使用尾调用的话会出现三个调用侦:

a() // 1 添加a到调用栈
function a(){
    return b(); // 在调用栈中删除a 添加b
}
function b(){
    return c() // 删除b 添加c
}

防止爆栈

浏览器对调用栈都有大小限制,在ES6之前递归比较深的话,很容易出现“爆栈”问题(stack overflow)。

现在可以使用“尾调用优化”来写一个“尾递归”,只保存一个调用侦,来防止爆栈问题。

注意

  1. 只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧。
如果要使用外层函数的变量,可以通过参数的形式传到内层函数中
function a(){
    var aa = 1;
    let b = val => aa + val // 使用了外层函数的参数aa
    return b(2) // 无法进行尾调用优化
}
  1. 尾调用优化只在严格模式下开启,非严格模式是无效的。
  2. 如果环境不支持“尾调用优化”,代码还可以正常运行,是无害的!

更多

关于尾递归以及更多尾调用优化的内容,推荐查阅ES6入门-阮一峰

调用栈debug大法

查看调用栈有什么用

  1. 查看函数的调用顺序是否跟预期一致,比如不同判断调用不同函数。
  2. 快速定位问题/修改三方库的代码。

    当接手一个历史项目,或者引用第三方库出现问题的时候,可以先查看对应API的调用栈,找到其中涉及的关键函数,针对性的修复它。

    通过查看调用栈的形式,帮助我快速定位问题,修改三方库的源码。

如何查看调用栈

  1. 只查看调用栈:console.trace
a()
function a() {
    b();
}
function b() {
    c()
}
function c() {
    let aa = 1;
    console.trace()
}

如图所示,点击右侧还能查看代码位置:

  1. debugger打断点形式,这也是我最喜欢的调试方式:

结语

本文主要讲了这几个方面的内容:

  1. 理解调用栈的运行机制,对代码背后的一些执行机制也可以更加了解,帮助我们在百尺竿头更进一步。
  2. 我们应该在日常的code中,有意识的使用ES6的“尾调用优化”,来减少调用栈的长度,节省客户端内存。
  3. 利用调用栈,对第三方库或者不熟悉的项目,可以更快速的定位问题,提高我们debug速度。

最后:之前写过一篇关于垃圾回收机制与内存泄露的文章,感兴趣的同学可以扩展一下。

如果这篇文章帮助到了你,欢迎点赞和关注,你的支持是对我最大的鼓励!

博客前端积累文档公众号GitHub

以上2019/5/19

参考资料:

JS垃圾回收机制与常见内存泄露的解决方法

ES6入门-阮一峰

JavaScript 如何工作:对引擎、运行时、调用堆栈的概述

浅析javascript调用栈

查看原文

OBKoro1 发布了文章 · 2019-05-21

js 调用栈机制与ES6尾调用优化介绍

调用栈的英文名叫做Call Stack,大家或多或少是有听过的,但是对于js调用栈的工作方式以及如何在工作中利用这一特性,大部分人可能没有进行过更深入的研究,这块内容可以说对我们前端来说就是所谓的基础知识,咋一看好像用处并没有很大,但掌握好这个知识点,就可以让我们在以后可以走的更远,走的更快!

博客前端积累文档公众号GitHub

目录

  1. 数据结构:栈
  2. 调用栈是什么?用来做什么?
  3. 调用栈的运行机制
  4. 调用栈优化内存
  5. 调用栈debug大法

数据结构:栈

栈是一种遵从后进先出(LIFO)原则的有序集合,新元素都靠近栈顶,旧元素都接近栈底。

生活中的栗子,帮助一下理解:

餐厅里面堆放的盘子(栈),一开始放的都在下面(先进),后面放的都在上面(后进),洗盘子的时候先从上面开始洗(先出)。

调用栈是什么?用来做什么?

  1. 调用栈是一种栈结构的数据,它是由调用侦组成的
  2. 调用栈记录了函数的执行顺序和函数内部变量等信息

调用栈的运行机制

机制

程序运行到一个函数,它就会将其添加到调用栈中,当从这个函数返回的时候,就会将这个函数从调用栈中删掉。

看一下例子帮助理解:

// 调用栈中的执行步骤用数字表示
printSquare(5); // 1 添加
function printSquare(x) {
    var s = multiply(x, x); // 2 添加 => 3 运行完成,内部没有再调用其他函数,删掉
    console.log(s); // 4 添加 => 5 删掉
    // 运行完成 删掉printSquare
}
function multiply(x, y) {
    return x * y;
}

调用栈中的执行步骤如下(删除multiply的步骤被省略了):

调用侦

每个进入到调用栈中的函数,都会分配到一个单独的栈空间,称为“调用侦”。

在调用栈中每个“调用侦”都对应一个函数,最上方的调用帧称为“当前帧”,调用栈是由所有的调用侦形成的。

找到一张图片,调用侦:

调用栈优化内存

调用栈的内存消耗

如上图,函数的变量等信息会被调用侦保存起来,所以调用侦中的变量不会被垃圾收集器回收

当函数嵌套的层级比较深了,调用栈中的调用侦比较多的时候,这些信息对内存消耗是非常大的。

针对这种情况除了我们要尽量避免函数层级嵌套的比较深之外,ES6提供了“尾调用优化”来解决调用侦过多,引起的内存消耗过大的问题。

何谓尾调用

尾调用指的是:函数的最后一步是调用另一个函数

function f(x){
  return g(x); // 最后一步调用另一个函数并且使用return
}
function f(x){
  g(x); // 没有return 不算尾调用 因为不知道后面还有没有操作
  // return undefined; // 隐式的return
}

尾调用优化优化了什么?

尾调用用来删除外层无用的调用侦,只保留内层函数的调用侦,来节省浏览器的内存。

下面这个例子调用栈中的调用侦一直只有一项,如果不使用尾调用的话会出现三个调用侦:

a() // 1 添加a到调用栈
function a(){
    return b(); // 在调用栈中删除a 添加b
}
function b(){
    return c() // 删除b 添加c
}

防止爆栈

浏览器对调用栈都有大小限制,在ES6之前递归比较深的话,很容易出现“爆栈”问题(stack overflow)。

现在可以使用“尾调用优化”来写一个“尾递归”,只保存一个调用侦,来防止爆栈问题。

注意

  1. 只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧。
如果要使用外层函数的变量,可以通过参数的形式传到内层函数中
function a(){
    var aa = 1;
    let b = val => aa + val // 使用了外层函数的参数aa
    return b(2) // 无法进行尾调用优化
}
  1. 尾调用优化只在严格模式下开启,非严格模式是无效的。
  2. 如果环境不支持“尾调用优化”,代码还可以正常运行,是无害的!

更多

关于尾递归以及更多尾调用优化的内容,推荐查阅ES6入门-阮一峰

调用栈debug大法

查看调用栈有什么用

  1. 查看函数的调用顺序是否跟预期一致,比如不同判断调用不同函数。
  2. 快速定位问题/修改三方库的代码。

    当接手一个历史项目,或者引用第三方库出现问题的时候,可以先查看对应API的调用栈,找到其中涉及的关键函数,针对性的修复它。

    通过查看调用栈的形式,帮助我快速定位问题,修改三方库的源码。

如何查看调用栈

  1. 只查看调用栈:console.trace
a()
function a() {
    b();
}
function b() {
    c()
}
function c() {
    let aa = 1;
    console.trace()
}

如图所示,点击右侧还能查看代码位置:

  1. debugger打断点形式,这也是我最喜欢的调试方式:

结语

本文主要讲了这几个方面的内容:

  1. 理解调用栈的运行机制,对代码背后的一些执行机制也可以更加了解,帮助我们在百尺竿头更进一步。
  2. 我们应该在日常的code中,有意识的使用ES6的“尾调用优化”,来减少调用栈的长度,节省客户端内存。
  3. 利用调用栈,对第三方库或者不熟悉的项目,可以更快速的定位问题,提高我们debug速度。

最后:之前写过一篇关于垃圾回收机制与内存泄露的文章,感兴趣的同学可以扩展一下。

如果这篇文章帮助到了你,欢迎点赞和关注,你的支持是对我最大的鼓励!

博客前端积累文档公众号GitHub

以上2019/5/19

参考资料:

JS垃圾回收机制与常见内存泄露的解决方法

ES6入门-阮一峰

JavaScript 如何工作:对引擎、运行时、调用堆栈的概述

浅析javascript调用栈

查看原文

赞 14 收藏 13 评论 4

OBKoro1 回答了问题 · 2019-04-17

解决sessionStorage取值为空的问题

可以通过获取sessionStorage里面的全部值,来判断,console时,是否有值,进而可判断是否是你key值写错了。

sessionStorage.valueOf() // 获取sessionStorage里面的全部值

现在感觉是你key值写错了,我再segmentfault里面的storage中就没有看到引号。

clipboard.png

关注 4 回答 2

OBKoro1 评论了文章 · 2019-03-24

论普通函数和箭头函数的区别以及箭头函数的注意事项、不适用场景

箭头函数是ES6的API,相信很多人都知道,因为其语法上相对于普通函数更简洁,深受大家的喜爱。就是这种我们日常开发中一直在使用的API,大部分同学却对它的了解程度还是不够深...

普通函数和箭头函数的区别:

箭头函数的this指向规则:

1. 箭头函数没有prototype(原型),所以箭头函数本身没有this

let a = () =>{};
console.log(a.prototype); // undefined

2. 箭头函数的this指向在定义的时候继承自外层第一个普通函数的this。

下面栗子中在一个函数中定义箭头函数,然后在另一个函数中执行箭头函数。

let a,
  barObj = { msg: 'bar的this指向' };
fooObj = { msg: 'foo的this指向' };
bar.call(barObj); // 将bar的this指向barObj
foo.call(fooObj); // 将foo的this指向fooObj
function foo() {
  a(); // 结果:{ msg: 'bar的this指向' }
}
function bar() {
  a = () => {
    console.log(this, 'this指向定义的时候外层第一个普通函数'); // 
  }; // 在bar中定义 this继承于bar函数的this指向
}

从上面栗子中可以得出两点

  1. 箭头函数的this指向定义时所在的外层第一个普通函数,跟使用位置没有关系
  2. 被继承的普通函数的this指向改变,箭头函数的this指向会跟着改变

3. 不能直接修改箭头函数的this指向

上个栗子中的foo函数修改一下,尝试直接修改箭头函数的this指向。

let fnObj = { msg: '尝试直接修改箭头函数的this指向' };
function foo() {
  a.call(fnObj); // 结果:{ msg: 'bar的this指向' }
}

很明显,call显示绑定this指向失败了,包括aaply、bind都一样。

它们(call、aaply、bind)会默认忽略第一个参数,但是可以正常传参。

然后我又通过隐式绑定来尝试同样也失败了,new 调用会报错,这个稍后再说。

SO,箭头函数不能直接修改它的this指向

幸运的是,我们可以通过间接的形式来修改箭头函数的指向:

去修改被继承的普通函数的this指向,然后箭头函数的this指向也会跟着改变,这在上一个栗子中有演示。

bar.call(barObj); // 将bar普通函数的this指向barObj 然后内部的箭头函数也会指向barObj

4. 箭头函数外层没有普通函数,严格模式和非严格模式下它的this都会指向window(全局对象)

唔,这个问题实际上是面试官提出来的,当时我认为的箭头函数规则就是:箭头函数的this指向继承自外层第一个普通函数的this,现在看来真是不严谨(少说一个定义的时候),要是面试官问我:定义和执行不在同一个普通函数中,它又指向哪里,肯定歇菜...

既然箭头函数的this指向在定义的时候继承自外层第一个普通函数的this,那么:

当箭头函数外层没有普通函数,它的this会指向哪里

这里跟我之前写的this绑定规则不太一样(不懂的可以点进去看一下),普通函数的默认绑定规则是:

在非严格模式下,默认绑定的this指向全局对象,严格模式下this指向undefined

如果箭头函数外层没有普通函数继承,它this指向的规则

经过测试,箭头函数在全局作用域下,严格模式和非严格模式下它的this都会指向window(全局对象)

Tip:测试的时候发现严格模式在中途声明无效,必须在全局/函数的开头声明才会生效

a = 1;
'use strict'; // 严格模式无效 必须在一开始就声明严格模式
b = 2; // 不报错

箭头函数的

箭头函数的arguments

箭头函数的this指向全局,使用arguments会报未声明的错误

如果箭头函数的this指向window(全局对象)使用arguments会报错,未声明arguments

let b = () => {
  console.log(arguments);
};
b(1, 2, 3, 4); // Uncaught ReferenceError: arguments is not defined

PS:如果你声明了一个全局变量为arguments,那就不会报错了,但是你为什么要这么做呢?

箭头函数的this指向普通函数时,它的argumens继承于该普通函数

上面是第一种情况:箭头函数的this指向全局对象,会报arguments未声明的错误。

第二种情况是:箭头函数的this如果指向普通函数,它的argumens继承于该普通函数。

function bar() {
  console.log(arguments); // ['外层第二个普通函数的参数']
  bb('外层第一个普通函数的参数');
  function bb() {
    console.log(arguments); // ["外层第一个普通函数的参数"]
    let a = () => {
      console.log(arguments, 'arguments继承this指向的那个普通函数'); // ["外层第一个普通函数的参数"]
    };
    a('箭头函数的参数'); // this指向bb
  }
}
bar('外层第二个普通函数的参数');

那么应该如何来获取箭头函数不定数量的参数呢?答案是:ES6的rest参数(...扩展符)

rest参数获取函数的多余参数

这是ES6的API,用于获取函数不定数量的参数数组,这个API是用来替代arguments的,API用法如下:

let a = (first, ...abc) => {
  console.log(first, abc); // 1 [2, 3, 4]
};
a(1, 2, 3, 4);

上面的栗子展示了,获取函数除第一个确定的参数,以及用一个变量接收其他剩余参数的示例。

也可以直接接收函数的所有参数,rest参数的用法相对于arguments的优点:

  1. 箭头函数和普通函数都可以使用。
  2. 更加灵活,接收参数的数量完全自定义。
  3. 可读性更好

    参数都是在函数括号中定义的,不会突然出现一个arguments,以前刚见到的时候,真的好奇怪了!

  4. rest是一个真正的数组,可以使用数组的API。

    因为arguments是一个类数组的对象,有些人以为它是真正的数组,所以会出现以下场景:

    arguments.push(0); // arguments.push is not a function

    如上,如果我们需要使用数组的API,需要使用扩展符/Array.from来将它转换成真正的数组:

    arguments = [...arguments]; 或者 :arguments = Array.from(arguments);

rest参数有两点需要注意

  1. rest必须是函数的最后一位参数:

    let a = (first, ...rest, three) => {
      console.log(first, rest,three); // 报错:Rest parameter must be last formal parameter
    };
    a(1, 2, 3, 4);
  2. 函数的length属性,不包括 rest 参数

    (function(...a) {}).length  // 0
    (function(a, ...b) {}).length  // 1

扩展运算符还可以用于数组,这里是阮一峰老师的文档

PS:感觉这里写多了,但比较喜欢把一个知识点讲清楚...

使用new调用箭头函数会报错

无论箭头函数的thsi指向哪里,使用new调用箭头函数都会报错,因为箭头函数没有constructor

let a = () => {};
let b = new  a(); // a is not a constructor

箭头函数不支持new.target

new.target是ES6新引入的属性,普通函数如果通过new调用,new.target会返回该函数的引用。

此属性主要:用于确定构造函数是否为new调用的。

  1. 箭头函数的this指向全局对象,在箭头函数中使用箭头函数会报错

    let a = () => {
      console.log(new.target); // 报错:new.target 不允许在这里使用
    };
    a();
  2. 箭头函数的this指向普通函数,它的new.target就是指向该普通函数的引用。

    new bb();
    function bb() {
      let a = () => {
        console.log(new.target); // 指向函数bb:function bb(){...}
      };
      a();
    }

更多关于new.target可以看一下阮一峰老师关于这部分的解释

箭头函数不支持重命名函数参数,普通函数的函数参数支持重命名

如下示例,普通函数的函数参数支持重命名,后面出现的会覆盖前面的,箭头函数会抛出错误:

function func1(a, a) {
  console.log(a, arguments); // 2 [1,2]
}

var func2 = (a,a) => {
  console.log(a); // 报错:在此上下文中不允许重复参数名称
};
func1(1, 2); func2(1, 2);

箭头函数相对于普通函数语法更简洁优雅:

讲道理,语法上的不同,也属与它们两个的区别!

  1. 箭头函数都是匿名函数,并且都不用写function
  2. 只有一个参数的时候可以省略括号:

    var f = a => a; // 传入a 返回a
  3. 函数只有一条语句时可以省略{}return

    var f = (a,b,c) => a; // 传入a,b,c 返回a
  4. 简化回调函数,让你的回调函数更优雅:
[1,2,3].map(function (x) {
  return x * x;
}); // 普通函数写法 
[1,2,3].map(x => x * x); // 箭头函数只需要一行

箭头函数的注意事项及不适用场景

箭头函数的注意事项

  1. 一条语句返回对象字面量,需要加括号,或者直接写成多条语句的return形式,

    否则像func中演示的一样,花括号会被解析为多条语句的花括号,不能正确解析

var func1 = () => { foo: 1 }; // 想返回一个对象,花括号被当成多条语句来解析,执行后返回undefined
var func2 = () => ({foo: 1}); // 用圆括号是正确的写法
var func2 = () => {
  return {
    foo: 1 // 更推荐直接当成多条语句的形式来写,可读性高
  };
};
  1. 箭头函数在参数和箭头之间不能换行!
var func = ()
           => 1;  // 报错: Unexpected token =>
  1. 箭头函数的解析顺序相对靠前

MDN: 虽然箭头函数中的箭头不是运算符,但箭头函数具有与常规函数不同的特殊运算符优先级解析规则

let a = false || function() {}; // ok
let b = false || () => {}; // Malformed arrow function parameter list
let c = false || (() => {}); // ok

箭头函数不适用场景:

围绕两点:箭头函数的this意外指向和代码的可读性。

  1. 定义字面量方法,this的意外指向。

因为箭头函数的简洁

const obj = {
  array: [1, 2, 3],
  sum: () => {
    // 根据上文学到的:外层没有普通函数this会指向全局对象
    return this.array.push('全局对象下没有array,这里会报错'); // 找不到push方法
  }
};
obj.sum();

上述栗子使用普通函数或者ES6中的方法简写的来定义方法,就没有问题了:

// 这两种写法是等价的
sum() {
  return this.array.push('this指向obj');
}
sum: function() {
  return this.array.push('this指向obj');
}

还有一种情况是给普通函数的原型定义方法的时候,通常会在普通函数的外部进行定义,比如说继承/添加方法的时候。

这时候因为没有在普通函数的内部进行定义,所以this会指向其他普通函数,或者全局对象上,导致bug!

  1. 回调函数的动态this

下文是一个修改dom文本的操作,因为this指向错误,导致修改失败:

const button = document.getElementById('myButton');
button.addEventListener('click', () => {
    this.innerHTML = 'Clicked button'; // this又指向了全局
});

相信你也知道了,改成普通函数就成了。

  1. 考虑代码的可读性,使用普通函数

    • 函数体复杂:

      具体表现就是箭头函数中使用多个三元运算符号,就是不换行,非要在一行内写完,非常恶心!

    • 行数较多
    • 函数内部有大量操作

文章内容小结:

普通函数和箭头函数的区别:

  1. 箭头函数没有prototype(原型),所以箭头函数本身没有this
  2. 箭头函数的this在定义的时候继承自外层第一个普通函数的this。
  3. 如果箭头函数外层没有普通函数,严格模式和非严格模式下它的this都会指向window(全局对象)
  4. 箭头函数本身的this指向不能改变,但可以修改它要继承的对象的this。
  5. 箭头函数的this指向全局,使用arguments会报未声明的错误。
  6. 箭头函数的this指向普通函数时,它的argumens继承于该普通函数
  7. 使用new调用箭头函数会报错,因为箭头函数没有constructor
  8. 箭头函数不支持new.target
  9. 箭头函数不支持重命名函数参数,普通函数的函数参数支持重命名
  10. 箭头函数相对于普通函数语法更简洁优雅

箭头函数的注意事项及不适用场景

箭头函数的注意事项

  1. 箭头函数一条语句返回对象字面量,需要加括号
  2. 箭头函数在参数和箭头之间不能换行
  3. 箭头函数的解析顺序相对||靠前

不适用场景:箭头函数的this意外指向和代码的可读性。


结语

呕心沥血,可以说是很全了,反正第一次问到我的时候只能想到箭头函数的this是继承而来的,以及语法上的简洁性,其他的我都不知道,希望这篇文章能够帮助各位同学学到知识。

PS:目前找工作中,求大佬们内推,中高级前端,偏JS,Vue,上海杨浦。

博客前端积累文档公众号GitHub、wx:OBkoro1、邮箱:obkoro1@foxmail.com

以上2019.03.22

参考资料:

MDN 箭头函数

阮一峰-ES6入门

什么时候你不能使用箭头函数?

查看原文

OBKoro1 关注了用户 · 2019-03-22

高阳Sunny @sunny

SegmentFault 思否 CEO
C14Z.Group Founder
Forbes China 30U30

独立思考 敢于否定

曾经是个话痨... 要做一个有趣的人!

任何问题可以给我发私信或者发邮件 sunny@sifou.com

关注 2162

OBKoro1 评论了文章 · 2019-03-22

论普通函数和箭头函数的区别以及箭头函数的注意事项、不适用场景

箭头函数是ES6的API,相信很多人都知道,因为其语法上相对于普通函数更简洁,深受大家的喜爱。就是这种我们日常开发中一直在使用的API,大部分同学却对它的了解程度还是不够深...

普通函数和箭头函数的区别:

箭头函数的this指向规则:

1. 箭头函数没有prototype(原型),所以箭头函数本身没有this

let a = () =>{};
console.log(a.prototype); // undefined

2. 箭头函数的this指向在定义的时候继承自外层第一个普通函数的this。

下面栗子中在一个函数中定义箭头函数,然后在另一个函数中执行箭头函数。

let a,
  barObj = { msg: 'bar的this指向' };
fooObj = { msg: 'foo的this指向' };
bar.call(barObj); // 将bar的this指向barObj
foo.call(fooObj); // 将foo的this指向fooObj
function foo() {
  a(); // 结果:{ msg: 'bar的this指向' }
}
function bar() {
  a = () => {
    console.log(this, 'this指向定义的时候外层第一个普通函数'); // 
  }; // 在bar中定义 this继承于bar函数的this指向
}

从上面栗子中可以得出两点

  1. 箭头函数的this指向定义时所在的外层第一个普通函数,跟使用位置没有关系
  2. 被继承的普通函数的this指向改变,箭头函数的this指向会跟着改变

3. 不能直接修改箭头函数的this指向

上个栗子中的foo函数修改一下,尝试直接修改箭头函数的this指向。

let fnObj = { msg: '尝试直接修改箭头函数的this指向' };
function foo() {
  a.call(fnObj); // 结果:{ msg: 'bar的this指向' }
}

很明显,call显示绑定this指向失败了,包括aaply、bind都一样。

它们(call、aaply、bind)会默认忽略第一个参数,但是可以正常传参。

然后我又通过隐式绑定来尝试同样也失败了,new 调用会报错,这个稍后再说。

SO,箭头函数不能直接修改它的this指向

幸运的是,我们可以通过间接的形式来修改箭头函数的指向:

去修改被继承的普通函数的this指向,然后箭头函数的this指向也会跟着改变,这在上一个栗子中有演示。

bar.call(barObj); // 将bar普通函数的this指向barObj 然后内部的箭头函数也会指向barObj

4. 箭头函数外层没有普通函数,严格模式和非严格模式下它的this都会指向window(全局对象)

唔,这个问题实际上是面试官提出来的,当时我认为的箭头函数规则就是:箭头函数的this指向继承自外层第一个普通函数的this,现在看来真是不严谨(少说一个定义的时候),要是面试官问我:定义和执行不在同一个普通函数中,它又指向哪里,肯定歇菜...

既然箭头函数的this指向在定义的时候继承自外层第一个普通函数的this,那么:

当箭头函数外层没有普通函数,它的this会指向哪里

这里跟我之前写的this绑定规则不太一样(不懂的可以点进去看一下),普通函数的默认绑定规则是:

在非严格模式下,默认绑定的this指向全局对象,严格模式下this指向undefined

如果箭头函数外层没有普通函数继承,它this指向的规则

经过测试,箭头函数在全局作用域下,严格模式和非严格模式下它的this都会指向window(全局对象)

Tip:测试的时候发现严格模式在中途声明无效,必须在全局/函数的开头声明才会生效

a = 1;
'use strict'; // 严格模式无效 必须在一开始就声明严格模式
b = 2; // 不报错

箭头函数的

箭头函数的arguments

箭头函数的this指向全局,使用arguments会报未声明的错误

如果箭头函数的this指向window(全局对象)使用arguments会报错,未声明arguments

let b = () => {
  console.log(arguments);
};
b(1, 2, 3, 4); // Uncaught ReferenceError: arguments is not defined

PS:如果你声明了一个全局变量为arguments,那就不会报错了,但是你为什么要这么做呢?

箭头函数的this指向普通函数时,它的argumens继承于该普通函数

上面是第一种情况:箭头函数的this指向全局对象,会报arguments未声明的错误。

第二种情况是:箭头函数的this如果指向普通函数,它的argumens继承于该普通函数。

function bar() {
  console.log(arguments); // ['外层第二个普通函数的参数']
  bb('外层第一个普通函数的参数');
  function bb() {
    console.log(arguments); // ["外层第一个普通函数的参数"]
    let a = () => {
      console.log(arguments, 'arguments继承this指向的那个普通函数'); // ["外层第一个普通函数的参数"]
    };
    a('箭头函数的参数'); // this指向bb
  }
}
bar('外层第二个普通函数的参数');

那么应该如何来获取箭头函数不定数量的参数呢?答案是:ES6的rest参数(...扩展符)

rest参数获取函数的多余参数

这是ES6的API,用于获取函数不定数量的参数数组,这个API是用来替代arguments的,API用法如下:

let a = (first, ...abc) => {
  console.log(first, abc); // 1 [2, 3, 4]
};
a(1, 2, 3, 4);

上面的栗子展示了,获取函数除第一个确定的参数,以及用一个变量接收其他剩余参数的示例。

也可以直接接收函数的所有参数,rest参数的用法相对于arguments的优点:

  1. 箭头函数和普通函数都可以使用。
  2. 更加灵活,接收参数的数量完全自定义。
  3. 可读性更好

    参数都是在函数括号中定义的,不会突然出现一个arguments,以前刚见到的时候,真的好奇怪了!

  4. rest是一个真正的数组,可以使用数组的API。

    因为arguments是一个类数组的对象,有些人以为它是真正的数组,所以会出现以下场景:

    arguments.push(0); // arguments.push is not a function

    如上,如果我们需要使用数组的API,需要使用扩展符/Array.from来将它转换成真正的数组:

    arguments = [...arguments]; 或者 :arguments = Array.from(arguments);

rest参数有两点需要注意

  1. rest必须是函数的最后一位参数:

    let a = (first, ...rest, three) => {
      console.log(first, rest,three); // 报错:Rest parameter must be last formal parameter
    };
    a(1, 2, 3, 4);
  2. 函数的length属性,不包括 rest 参数

    (function(...a) {}).length  // 0
    (function(a, ...b) {}).length  // 1

扩展运算符还可以用于数组,这里是阮一峰老师的文档

PS:感觉这里写多了,但比较喜欢把一个知识点讲清楚...

使用new调用箭头函数会报错

无论箭头函数的thsi指向哪里,使用new调用箭头函数都会报错,因为箭头函数没有constructor

let a = () => {};
let b = new  a(); // a is not a constructor

箭头函数不支持new.target

new.target是ES6新引入的属性,普通函数如果通过new调用,new.target会返回该函数的引用。

此属性主要:用于确定构造函数是否为new调用的。

  1. 箭头函数的this指向全局对象,在箭头函数中使用箭头函数会报错

    let a = () => {
      console.log(new.target); // 报错:new.target 不允许在这里使用
    };
    a();
  2. 箭头函数的this指向普通函数,它的new.target就是指向该普通函数的引用。

    new bb();
    function bb() {
      let a = () => {
        console.log(new.target); // 指向函数bb:function bb(){...}
      };
      a();
    }

更多关于new.target可以看一下阮一峰老师关于这部分的解释

箭头函数不支持重命名函数参数,普通函数的函数参数支持重命名

如下示例,普通函数的函数参数支持重命名,后面出现的会覆盖前面的,箭头函数会抛出错误:

function func1(a, a) {
  console.log(a, arguments); // 2 [1,2]
}

var func2 = (a,a) => {
  console.log(a); // 报错:在此上下文中不允许重复参数名称
};
func1(1, 2); func2(1, 2);

箭头函数相对于普通函数语法更简洁优雅:

讲道理,语法上的不同,也属与它们两个的区别!

  1. 箭头函数都是匿名函数,并且都不用写function
  2. 只有一个参数的时候可以省略括号:

    var f = a => a; // 传入a 返回a
  3. 函数只有一条语句时可以省略{}return

    var f = (a,b,c) => a; // 传入a,b,c 返回a
  4. 简化回调函数,让你的回调函数更优雅:
[1,2,3].map(function (x) {
  return x * x;
}); // 普通函数写法 
[1,2,3].map(x => x * x); // 箭头函数只需要一行

箭头函数的注意事项及不适用场景

箭头函数的注意事项

  1. 一条语句返回对象字面量,需要加括号,或者直接写成多条语句的return形式,

    否则像func中演示的一样,花括号会被解析为多条语句的花括号,不能正确解析

var func1 = () => { foo: 1 }; // 想返回一个对象,花括号被当成多条语句来解析,执行后返回undefined
var func2 = () => ({foo: 1}); // 用圆括号是正确的写法
var func2 = () => {
  return {
    foo: 1 // 更推荐直接当成多条语句的形式来写,可读性高
  };
};
  1. 箭头函数在参数和箭头之间不能换行!
var func = ()
           => 1;  // 报错: Unexpected token =>
  1. 箭头函数的解析顺序相对靠前

MDN: 虽然箭头函数中的箭头不是运算符,但箭头函数具有与常规函数不同的特殊运算符优先级解析规则

let a = false || function() {}; // ok
let b = false || () => {}; // Malformed arrow function parameter list
let c = false || (() => {}); // ok

箭头函数不适用场景:

围绕两点:箭头函数的this意外指向和代码的可读性。

  1. 定义字面量方法,this的意外指向。

因为箭头函数的简洁

const obj = {
  array: [1, 2, 3],
  sum: () => {
    // 根据上文学到的:外层没有普通函数this会指向全局对象
    return this.array.push('全局对象下没有array,这里会报错'); // 找不到push方法
  }
};
obj.sum();

上述栗子使用普通函数或者ES6中的方法简写的来定义方法,就没有问题了:

// 这两种写法是等价的
sum() {
  return this.array.push('this指向obj');
}
sum: function() {
  return this.array.push('this指向obj');
}

还有一种情况是给普通函数的原型定义方法的时候,通常会在普通函数的外部进行定义,比如说继承/添加方法的时候。

这时候因为没有在普通函数的内部进行定义,所以this会指向其他普通函数,或者全局对象上,导致bug!

  1. 回调函数的动态this

下文是一个修改dom文本的操作,因为this指向错误,导致修改失败:

const button = document.getElementById('myButton');
button.addEventListener('click', () => {
    this.innerHTML = 'Clicked button'; // this又指向了全局
});

相信你也知道了,改成普通函数就成了。

  1. 考虑代码的可读性,使用普通函数

    • 函数体复杂:

      具体表现就是箭头函数中使用多个三元运算符号,就是不换行,非要在一行内写完,非常恶心!

    • 行数较多
    • 函数内部有大量操作

文章内容小结:

普通函数和箭头函数的区别:

  1. 箭头函数没有prototype(原型),所以箭头函数本身没有this
  2. 箭头函数的this在定义的时候继承自外层第一个普通函数的this。
  3. 如果箭头函数外层没有普通函数,严格模式和非严格模式下它的this都会指向window(全局对象)
  4. 箭头函数本身的this指向不能改变,但可以修改它要继承的对象的this。
  5. 箭头函数的this指向全局,使用arguments会报未声明的错误。
  6. 箭头函数的this指向普通函数时,它的argumens继承于该普通函数
  7. 使用new调用箭头函数会报错,因为箭头函数没有constructor
  8. 箭头函数不支持new.target
  9. 箭头函数不支持重命名函数参数,普通函数的函数参数支持重命名
  10. 箭头函数相对于普通函数语法更简洁优雅

箭头函数的注意事项及不适用场景

箭头函数的注意事项

  1. 箭头函数一条语句返回对象字面量,需要加括号
  2. 箭头函数在参数和箭头之间不能换行
  3. 箭头函数的解析顺序相对||靠前

不适用场景:箭头函数的this意外指向和代码的可读性。


结语

呕心沥血,可以说是很全了,反正第一次问到我的时候只能想到箭头函数的this是继承而来的,以及语法上的简洁性,其他的我都不知道,希望这篇文章能够帮助各位同学学到知识。

PS:目前找工作中,求大佬们内推,中高级前端,偏JS,Vue,上海杨浦。

博客前端积累文档公众号GitHub、wx:OBkoro1、邮箱:obkoro1@foxmail.com

以上2019.03.22

参考资料:

MDN 箭头函数

阮一峰-ES6入门

什么时候你不能使用箭头函数?

查看原文

OBKoro1 发布了文章 · 2019-03-22

你或许不知道Vue的这些小技巧

前言

用Vue开发一个网页并不难,但是也经常会遇到一些问题,其实大部分的问题都在文档中有所提及,再不然我们通过谷歌也能成功搜索到问题的答案,为了帮助小伙伴们提前踩坑,在遇到问题的时候,心里大概有个谱知道该如何去解决问题。这篇文章是将自己知道的一些小技巧,结合查阅资料整理成的一篇文章,如果喜欢的话可以点波赞/关注,支持一下,希望大家看完本文可以有所收获。

个人博客了解一下:obkoro1.com

文章内容总结:

  1. 组件style的scoped
  2. Vue 数组/对象更新 视图不更新
  3. vue filters 过滤器的使用
  4. 列表渲染相关
  5. 深度watch与watch立即触发回调
  6. 这些情况下不要使用箭头函数
  7. 路由懒加载写法
  8. 路由的项目启动页和404页面
  9. Vue调试神器:vue-devtools

组件style的scoped:

问题:在组件中用js动态创建的dom,添加样式不生效。

场景:

    <template>
         <div class="test"></div>
    </template>
    <script>
        let a=document.querySelector('.test');
        let newDom=document.createElement("div"); // 创建dom
        newDom.setAttribute("class","testAdd" ); // 添加样式
        a.appendChild(newDom); // 插入dom
    </script>
    <style scoped>
    .test{
       background:blue;
        height:100px;
        width:100px;
    }
    .testAdd{
        background:red;
        height:100px;
        width:100px;
    }
    </style>

结果

// test生效   testAdd 不生效
<div data-v-1b971ada class="test"><div class="testAdd"></div></div>
.test[data-v-1b971ada]{ // 注意data-v-1b971ada
    background:blue;
    height:100px;
    width:100px;
}

原因:

<style> 标签有 scoped 属性时,它的 CSS 只作用于当前组件中的元素。

它会为组件中所有的标签和class样式添加一个scoped标识,就像上面结果中的data-v-1b971ada

所以原因就很清楚了:因为动态添加的dom没有scoped添加的标识,没有跟testAdd的样式匹配起来,导致样式失效。

解决方式

  • 推荐:去掉该组件的scoped

每个组件的css并不会很多,当设计到动态添加dom,并为dom添加样式的时候,就可以去掉scoped,会比下面的方法方便很多。

  • 可以动态添加style

    // 上面的栗子可以这样添加样式
    newDom.style.height='100px';
    newDom.style.width='100px';
    newDom.style.background='red';


Vue 数组/对象更新 视图不更新

很多时候,我们习惯于这样操作数组和对象:

     data() { // data数据
        return {
          arr: [1,2,3],
          obj:{
              a: 1,
              b: 2
          }
        };
      },
   // 数据更新 数组视图不更新
    this.arr[0] = 'OBKoro1';
    this.arr.length = 1;
    console.log(arr);// ['OBKoro1'];
    // 数据更新 对象视图不更新
    this.obj.c = 'OBKoro1';
    delete this.obj.a;
    console.log(obj);  // {b:2,c:'OBKoro1'}

由于js的限制,Vue 不能检测以上数组的变动,以及对象的添加/删除,很多人会因为像上面这样操作,出现视图没有更新的问题。

解决方式:

  1. this.$set(你要改变的数组/对象,你要改变的位置/key,你要改成什么value)

    this.$set(this.arr, 0, "OBKoro1"); // 改变数组
    this.$set(this.obj, "c", "OBKoro1"); // 改变对象

如果还是不懂的话,可以看看这个codependemo

  1. 数组原生方法触发视图更新:

Vue可以监测到数组变化的,数组原生方法:

    splice()、 push()、pop()、shift()、unshift()、sort()、reverse()

意思是使用这些方法不用我们再进行额外的操作,视图自动进行更新

推荐使用splice方法会比较好自定义,因为slice可以在数组的任何位置进行删除/添加操作,这部分可以看看我前几天写的一篇文章:【干货】js 数组详细操作方法及解析合集

  1. 替换数组/对象

比方说:你想遍历这个数组/对象,对每个元素进行处理,然后触发视图更新。

   // 文档中的栗子: filter遍历数组,返回一个新数组,用新数组替换旧数组
    example1.items = example1.items.filter(function (item) {
      return item.message.match(/Foo/)
    })

举一反三:可以先把这个数组/对象保存在一个变量中,然后对这个变量进行遍历,等遍历结束后再用变量替换对象/数组

并不会重新渲染整个列表:

Vue 为了使得 DOM 元素得到最大范围的重用而实现了一些智能的、启发式的方法,所以用一个含有相同元素的数组去替换原来的数组是非常高效的操作。

如果你还是很困惑,可以看看Vue文档中关于这部分的解释。


vue filters 过滤器的使用:

过滤器,通常用于后台管理系统,或者一些约定类型,过滤。Vue过滤器用法是很简单,但是很多朋友可能都没有用过,这里稍微讲解一下。

在html模板中的两种用法

    <!-- 在双花括号中 -->
    {{ message | filterTest }}
    <!-- 在 `v-bind` 中 -->
    <div :id="message | filterTest"></div>

在组件script中的用法:

export default {    
     data() {
        return {
         message:1   
        }
     },
    filters: {  
        filterTest(value) {
            // value在这里是message的值
            if(value===1){
                return '最后输出这个值';
            }
        }
    }
}

用法就是上面讲的这样,可以自己在组件中试一试就知道了,很简单很好用的。

如果不想自己试,可以点这个demo里面修改代码就可以了,demo中包括过滤器串联过滤器传参

推荐看Vue过滤器文档,你会更了解它的。


列表渲染相关

v-for循环绑定model:

input在v-for中可以像如下这么进行绑定,我敢打赌很多人不知道。

    // 数据    
      data() {
          return{
           obj: {
              ob: "OB",
              koro1: "Koro1"
            },
            model: {
              ob: "默认ob",
              koro1: "默认koro1"
            }   
          }
      },
    // html模板
    <div v-for="(value,key) in obj">
       <input type="text" v-model="model[key]">
    </div>
      // input就跟数据绑定在一起了,那两个默认数据也会在input中显示

为此,我做了个demo,你可以点进去试试。

一段取值的v-for

如果我们有一段重复的html模板要渲染,又没有数据关联,我们可以:

    <div v-for="n in 5">
        <span>这里会被渲染5次,渲染模板{{n}}</span>
     </div>

v-if尽量不要与v-for在同一节点使用:

v-for 的优先级比 v-if 更高,如果它们处于同一节点的话,那么每一个循环都会运行一遍v-if。

如果你想根据循环中的每一项的数据来判断是否渲染,那么你这样做是对的:

    <li v-for="todo in todos" v-if="todo.type===1">
      {{ todo }}
    </li>

如果你想要根据某些条件跳过循环,而又跟将要渲染的每一项数据没有关系的话,你可以将v-if放在v-for的父节点

    // 根据elseData是否为true 来判断是否渲染,跟每个元素没有关系    
     <ul v-if="elseData">
      <li v-for="todo in todos">
        {{ todo }}
      </li>
    </ul>
    // 数组是否有数据 跟每个元素没有关系
    <ul v-if="todos.length">
      <li v-for="todo in todos">
        {{ todo }}
      </li>
    </ul>
    <p v-else>No todos left!</p>

如上,正确使用v-for与v-if优先级的关系,可以为你节省大量的性能。


深度watch与watch立即触发回调

watch很多人都在用,但是这watch中的这两个选项deepimmediate,或许不是很多人都知道,我猜。

选项:deep

在选项参数中指定 deep: true,可以监听对象中属性的变化。

选项:immediate

在选项参数中指定 immediate: true, 将立即以表达式的当前值触发回调,也就是默认触发一次。

    watch: {
        obj: {
          handler(val, oldVal) {
            console.log('属性发生变化触发这个回调',val, oldVal);
          },
          deep: true // 监听这个对象中的每一个属性变化
        },
        step: { // 属性
          //watch
          handler(val, oldVal) {
            console.log("默认触发一次", val, oldVal);
          },
          immediate: true // 默认触发一次
        },
      },

这两个选项可以同时使用,另外:是的,又有一个demo

还有下面这一点需要注意。


这些情况下不要使用箭头函数:

  • 不应该使用箭头函数来定义一个生命周期方法
  • 不应该使用箭头函数来定义 method 函数
  • 不应该使用箭头函数来定义计算属性函数
  • 不应该对 data 属性使用箭头函数
  • 不应该使用箭头函数来定义 watcher 函数

示例:

    // 上面watch的栗子:
    handler:(val, oldVal)=> { // 可以执行
     console.log("默认触发一次", val, oldVal);
   },
   // method:
     methods: {
        plus: () => { // 可以执行
          // do something
        }
      }
   // 生命周期:
     created:()=>{ // 可以执行
       console.log('lala',this.obj) 
      },

是的,没错,这些都能执行。

but:

箭头函数绑定了父级作用域的上下文,this 将不会按照期望指向 Vue 实例

也就是说,你不能使用this来访问你组件中的data数据以及method方法了

this将会指向undefined。


路由懒加载写法:

    // 我所采用的方法,个人感觉比较简洁一些,少了一步引入赋值。
    const router = new VueRouter({
      routes: [
        path: '/app',
        component: () => import('./app'),  // 引入组件
      ]
    })
    // Vue路由文档的写法:
    const app = () => import('./app.vue') // 引入组件
    const router = new VueRouter({
      routes: [
        { path: '/app', component: app }
      ]
    })

文档的写法在于问题在于:如果我们的路由比较多的话,是不是要在路由上方引入赋值十几行组件?

第一种跟第二种方法相比就是把引入赋值的一步,直接写在component上面,本质上是一样的。两种方式都可以的,大家自由选择哈。


路由的项目启动页和404页面

实际上这也就是一个设置而已:

    export default new Router({
      routes: [
        {
          path: '/', // 项目启动页
          redirect:'/login'  // 重定向到下方声明的路由 
        },
        {
          path: '*', // 404 页面 
          component: () => import('./notFind') // 或者使用component也可以的
        },
      ]
    })

比如你的域名为:www.baidu.com

项目启动页指的是: 当你进入www.baidu.com,会自动跳转到login登录页。

404页面指的是: 当进入一个没有 声明/没有匹配 的路由页面时就会跳转到404页面。

比如进入www.baidu.com/testRouter,就会自动跳转到notFind页面。

当你没有声明一个404页面,进入www.baidu.com/testRouter,显示的页面是一片空白。


Vue调试神器:vue-devtools

每次调试的时候,写一堆console是否很烦?想要更快知道组件/Vuex内数据的变化

那么这款尤大开发的调试神器:vue-devtools,你真的要了解一下了。

这波稳赚不赔,真的能提高开发效率。

安装方法

  • 谷歌商店+科学上网,搜索vue-devtools即可安装。
  • 不会科学上网?手动安装

安装之后

在chrome开发者工具中会看一个vue的一栏,如下对我们网页应用内数据变化,组件层级等信息能够有更准确快速的了解。


前几个月也写过一篇类似的:

Vue 实践过程中的几个问题


结语

本文的内容很多都在Vue文档里面有过说明,推荐大家可以多看看Vue文档,不止看教程篇,还有文档的Api什么的,也都可以看。然后其实还有两三点想写的,因为预计篇幅都会比较长一点,所以准备留到以后的文章里面吧~

文章如有不正确的地方欢迎各位路过的大佬鞭策!希望大家看完可以有所收获,喜欢的话,赶紧点波订阅关注/喜欢。

看完的朋友可以点个喜欢/关注,您的支持是对我最大的鼓励。

个人blog and 掘金个人主页,如需转载,请放上原文链接并署名。码字不易,感谢支持!

如果喜欢本文的话,欢迎关注我的订阅号,漫漫技术路,期待未来共同学习成长。

以上2018.6.3

参考资料:

Vue文档

Vue Api文档

查看原文

赞 22 收藏 33 评论 4

OBKoro1 发布了文章 · 2019-03-22

论普通函数和箭头函数的区别以及箭头函数的注意事项、不适用场景

箭头函数是ES6的API,相信很多人都知道,因为其语法上相对于普通函数更简洁,深受大家的喜爱。就是这种我们日常开发中一直在使用的API,大部分同学却对它的了解程度还是不够深...

普通函数和箭头函数的区别:

箭头函数的this指向规则:

1. 箭头函数没有prototype(原型),所以箭头函数本身没有this

let a = () =>{};
console.log(a.prototype); // undefined

2. 箭头函数的this指向在定义的时候继承自外层第一个普通函数的this。

下面栗子中在一个函数中定义箭头函数,然后在另一个函数中执行箭头函数。

let a,
  barObj = { msg: 'bar的this指向' };
fooObj = { msg: 'foo的this指向' };
bar.call(barObj); // 将bar的this指向barObj
foo.call(fooObj); // 将foo的this指向fooObj
function foo() {
  a(); // 结果:{ msg: 'bar的this指向' }
}
function bar() {
  a = () => {
    console.log(this, 'this指向定义的时候外层第一个普通函数'); // 
  }; // 在bar中定义 this继承于bar函数的this指向
}

从上面栗子中可以得出两点

  1. 箭头函数的this指向定义时所在的外层第一个普通函数,跟使用位置没有关系
  2. 被继承的普通函数的this指向改变,箭头函数的this指向会跟着改变

3. 不能直接修改箭头函数的this指向

上个栗子中的foo函数修改一下,尝试直接修改箭头函数的this指向。

let fnObj = { msg: '尝试直接修改箭头函数的this指向' };
function foo() {
  a.call(fnObj); // 结果:{ msg: 'bar的this指向' }
}

很明显,call显示绑定this指向失败了,包括aaply、bind都一样。

它们(call、aaply、bind)会默认忽略第一个参数,但是可以正常传参。

然后我又通过隐式绑定来尝试同样也失败了,new 调用会报错,这个稍后再说。

SO,箭头函数不能直接修改它的this指向

幸运的是,我们可以通过间接的形式来修改箭头函数的指向:

去修改被继承的普通函数的this指向,然后箭头函数的this指向也会跟着改变,这在上一个栗子中有演示。

bar.call(barObj); // 将bar普通函数的this指向barObj 然后内部的箭头函数也会指向barObj

4. 箭头函数外层没有普通函数,严格模式和非严格模式下它的this都会指向window(全局对象)

唔,这个问题实际上是面试官提出来的,当时我认为的箭头函数规则就是:箭头函数的this指向继承自外层第一个普通函数的this,现在看来真是不严谨(少说一个定义的时候),要是面试官问我:定义和执行不在同一个普通函数中,它又指向哪里,肯定歇菜...

既然箭头函数的this指向在定义的时候继承自外层第一个普通函数的this,那么:

当箭头函数外层没有普通函数,它的this会指向哪里

这里跟我之前写的this绑定规则不太一样(不懂的可以点进去看一下),普通函数的默认绑定规则是:

在非严格模式下,默认绑定的this指向全局对象,严格模式下this指向undefined

如果箭头函数外层没有普通函数继承,它this指向的规则

经过测试,箭头函数在全局作用域下,严格模式和非严格模式下它的this都会指向window(全局对象)

Tip:测试的时候发现严格模式在中途声明无效,必须在全局/函数的开头声明才会生效

a = 1;
'use strict'; // 严格模式无效 必须在一开始就声明严格模式
b = 2; // 不报错

箭头函数的

箭头函数的arguments

箭头函数的this指向全局,使用arguments会报未声明的错误

如果箭头函数的this指向window(全局对象)使用arguments会报错,未声明arguments

let b = () => {
  console.log(arguments);
};
b(1, 2, 3, 4); // Uncaught ReferenceError: arguments is not defined

PS:如果你声明了一个全局变量为arguments,那就不会报错了,但是你为什么要这么做呢?

箭头函数的this指向普通函数时,它的argumens继承于该普通函数

上面是第一种情况:箭头函数的this指向全局对象,会报arguments未声明的错误。

第二种情况是:箭头函数的this如果指向普通函数,它的argumens继承于该普通函数。

function bar() {
  console.log(arguments); // ['外层第二个普通函数的参数']
  bb('外层第一个普通函数的参数');
  function bb() {
    console.log(arguments); // ["外层第一个普通函数的参数"]
    let a = () => {
      console.log(arguments, 'arguments继承this指向的那个普通函数'); // ["外层第一个普通函数的参数"]
    };
    a('箭头函数的参数'); // this指向bb
  }
}
bar('外层第二个普通函数的参数');

那么应该如何来获取箭头函数不定数量的参数呢?答案是:ES6的rest参数(...扩展符)

rest参数获取函数的多余参数

这是ES6的API,用于获取函数不定数量的参数数组,这个API是用来替代arguments的,API用法如下:

let a = (first, ...abc) => {
  console.log(first, abc); // 1 [2, 3, 4]
};
a(1, 2, 3, 4);

上面的栗子展示了,获取函数除第一个确定的参数,以及用一个变量接收其他剩余参数的示例。

也可以直接接收函数的所有参数,rest参数的用法相对于arguments的优点:

  1. 箭头函数和普通函数都可以使用。
  2. 更加灵活,接收参数的数量完全自定义。
  3. 可读性更好

    参数都是在函数括号中定义的,不会突然出现一个arguments,以前刚见到的时候,真的好奇怪了!

  4. rest是一个真正的数组,可以使用数组的API。

    因为arguments是一个类数组的对象,有些人以为它是真正的数组,所以会出现以下场景:

    arguments.push(0); // arguments.push is not a function

    如上,如果我们需要使用数组的API,需要使用扩展符/Array.from来将它转换成真正的数组:

    arguments = [...arguments]; 或者 :arguments = Array.from(arguments);

rest参数有两点需要注意

  1. rest必须是函数的最后一位参数:

    let a = (first, ...rest, three) => {
      console.log(first, rest,three); // 报错:Rest parameter must be last formal parameter
    };
    a(1, 2, 3, 4);
  2. 函数的length属性,不包括 rest 参数

    (function(...a) {}).length  // 0
    (function(a, ...b) {}).length  // 1

扩展运算符还可以用于数组,这里是阮一峰老师的文档

PS:感觉这里写多了,但比较喜欢把一个知识点讲清楚...

使用new调用箭头函数会报错

无论箭头函数的thsi指向哪里,使用new调用箭头函数都会报错,因为箭头函数没有constructor

let a = () => {};
let b = new  a(); // a is not a constructor

箭头函数不支持new.target

new.target是ES6新引入的属性,普通函数如果通过new调用,new.target会返回该函数的引用。

此属性主要:用于确定构造函数是否为new调用的。

  1. 箭头函数的this指向全局对象,在箭头函数中使用箭头函数会报错

    let a = () => {
      console.log(new.target); // 报错:new.target 不允许在这里使用
    };
    a();
  2. 箭头函数的this指向普通函数,它的new.target就是指向该普通函数的引用。

    new bb();
    function bb() {
      let a = () => {
        console.log(new.target); // 指向函数bb:function bb(){...}
      };
      a();
    }

更多关于new.target可以看一下阮一峰老师关于这部分的解释

箭头函数不支持重命名函数参数,普通函数的函数参数支持重命名

如下示例,普通函数的函数参数支持重命名,后面出现的会覆盖前面的,箭头函数会抛出错误:

function func1(a, a) {
  console.log(a, arguments); // 2 [1,2]
}

var func2 = (a,a) => {
  console.log(a); // 报错:在此上下文中不允许重复参数名称
};
func1(1, 2); func2(1, 2);

箭头函数相对于普通函数语法更简洁优雅:

讲道理,语法上的不同,也属与它们两个的区别!

  1. 箭头函数都是匿名函数,并且都不用写function
  2. 只有一个参数的时候可以省略括号:

    var f = a => a; // 传入a 返回a
  3. 函数只有一条语句时可以省略{}return

    var f = (a,b,c) => a; // 传入a,b,c 返回a
  4. 简化回调函数,让你的回调函数更优雅:
[1,2,3].map(function (x) {
  return x * x;
}); // 普通函数写法 
[1,2,3].map(x => x * x); // 箭头函数只需要一行

箭头函数的注意事项及不适用场景

箭头函数的注意事项

  1. 一条语句返回对象字面量,需要加括号,或者直接写成多条语句的return形式,

    否则像func中演示的一样,花括号会被解析为多条语句的花括号,不能正确解析

var func1 = () => { foo: 1 }; // 想返回一个对象,花括号被当成多条语句来解析,执行后返回undefined
var func2 = () => ({foo: 1}); // 用圆括号是正确的写法
var func2 = () => {
  return {
    foo: 1 // 更推荐直接当成多条语句的形式来写,可读性高
  };
};
  1. 箭头函数在参数和箭头之间不能换行!
var func = ()
           => 1;  // 报错: Unexpected token =>
  1. 箭头函数的解析顺序相对靠前

MDN: 虽然箭头函数中的箭头不是运算符,但箭头函数具有与常规函数不同的特殊运算符优先级解析规则

let a = false || function() {}; // ok
let b = false || () => {}; // Malformed arrow function parameter list
let c = false || (() => {}); // ok

箭头函数不适用场景:

围绕两点:箭头函数的this意外指向和代码的可读性。

  1. 定义字面量方法,this的意外指向。

因为箭头函数的简洁

const obj = {
  array: [1, 2, 3],
  sum: () => {
    // 根据上文学到的:外层没有普通函数this会指向全局对象
    return this.array.push('全局对象下没有array,这里会报错'); // 找不到push方法
  }
};
obj.sum();

上述栗子使用普通函数或者ES6中的方法简写的来定义方法,就没有问题了:

// 这两种写法是等价的
sum() {
  return this.array.push('this指向obj');
}
sum: function() {
  return this.array.push('this指向obj');
}

还有一种情况是给普通函数的原型定义方法的时候,通常会在普通函数的外部进行定义,比如说继承/添加方法的时候。

这时候因为没有在普通函数的内部进行定义,所以this会指向其他普通函数,或者全局对象上,导致bug!

  1. 回调函数的动态this

下文是一个修改dom文本的操作,因为this指向错误,导致修改失败:

const button = document.getElementById('myButton');
button.addEventListener('click', () => {
    this.innerHTML = 'Clicked button'; // this又指向了全局
});

相信你也知道了,改成普通函数就成了。

  1. 考虑代码的可读性,使用普通函数

    • 函数体复杂:

      具体表现就是箭头函数中使用多个三元运算符号,就是不换行,非要在一行内写完,非常恶心!

    • 行数较多
    • 函数内部有大量操作

文章内容小结:

普通函数和箭头函数的区别:

  1. 箭头函数没有prototype(原型),所以箭头函数本身没有this
  2. 箭头函数的this在定义的时候继承自外层第一个普通函数的this。
  3. 如果箭头函数外层没有普通函数,严格模式和非严格模式下它的this都会指向window(全局对象)
  4. 箭头函数本身的this指向不能改变,但可以修改它要继承的对象的this。
  5. 箭头函数的this指向全局,使用arguments会报未声明的错误。
  6. 箭头函数的this指向普通函数时,它的argumens继承于该普通函数
  7. 使用new调用箭头函数会报错,因为箭头函数没有constructor
  8. 箭头函数不支持new.target
  9. 箭头函数不支持重命名函数参数,普通函数的函数参数支持重命名
  10. 箭头函数相对于普通函数语法更简洁优雅

箭头函数的注意事项及不适用场景

箭头函数的注意事项

  1. 箭头函数一条语句返回对象字面量,需要加括号
  2. 箭头函数在参数和箭头之间不能换行
  3. 箭头函数的解析顺序相对||靠前

不适用场景:箭头函数的this意外指向和代码的可读性。


结语

呕心沥血,可以说是很全了,反正第一次问到我的时候只能想到箭头函数的this是继承而来的,以及语法上的简洁性,其他的我都不知道,希望这篇文章能够帮助各位同学学到知识。

PS:目前找工作中,求大佬们内推,中高级前端,偏JS,Vue,上海杨浦。

博客前端积累文档公众号GitHub、wx:OBkoro1、邮箱:obkoro1@foxmail.com

以上2019.03.22

参考资料:

MDN 箭头函数

阮一峰-ES6入门

什么时候你不能使用箭头函数?

查看原文

赞 77 收藏 64 评论 5