胡小喵

胡小喵 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

胡小喵 关注了问题 · 2020-12-29

React17+ts 使用 antd <Spin>组件报错?

React17+ts 使用 antd <Spin>组件

报错内容
'Spin' cannot be used as a JSX component.
Its instance type 'Spin' is not a valid JSX element.

Type 'Spin' is missing the following properties from type 'ElementClass': context, setState, forceUpdate, props, and 2 more.ts(2786)

image.png

也不能给 <Spin> 传递props
image.png

请问这是什么原因?
下面附上我的项目配置

tsconfig

{
    "compilerOptions": {
        "emitDecoratorMetadata": true,
        "experimentalDecorators": true,
        "target": "ES2017",
        "allowSyntheticDefaultImports": true,
        "strict": true,
        "forceConsistentCasingInFileNames": true,
        "allowJs": true,
        "outDir": "./dist/",
        "esModuleInterop": true,
        "noImplicitAny": false,
        "sourceMap": true,
        "module": "esnext",
        "moduleResolution": "node",
        "isolatedModules": true,
        "importHelpers": true,
        "lib": ["esnext", "dom", "dom.iterable"],
        "skipLibCheck": false,
        "jsx": "react",
        "typeRoots": ["node", "node_modules/@types", "./typings"],
        "rootDirs": ["./src"],
        "baseUrl": "./src",
        "paths": {
            "src/*": ["*"],
            "assets/*": ["assets/*"],
            "components/*": ["components/*"],
            "pages/*": ["pages/*"],
            "utils/*": ["utils/*"],
            "servers/*": ["servers/*"],
            "actions/*": ["actions/*"],
            "config": ["config"],
            "routeConfig": ["routeConfig"],
            "request": ["request"]
        }
    },
    "include": ["./src/**/*", "./declaration.d.ts"],
    "exclude": ["node_modules"]
}

webpack配置

const webpack = require('webpack');
const path = require('path');
const tsImportPluginFactory = require('ts-import-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const AntdDayjsWebpackPlugin = require('antd-dayjs-webpack-plugin');

const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');

module.exports = {
    stats: {
        entrypoints: false,
        children: false
    },
    optimization: {
        minimizer: [
            // 压缩js
            new TerserPlugin({
                test: /\.(ts|tsx|js|jsx)$/,
                extractComments: true,
                parallel: true,
                cache: true
            })
        ],
        splitChunks: {
            cacheGroups: {
                vendors: {
                    //node_modules里的代码
                    test: /[\\/]node_modules[\\/]/,
                    chunks: 'all',
                    name: 'vendors', //chunks name
                    priority: 10, //优先级
                    enforce: true
                }
            }
        }
    },
    resolve: {
        extensions: ['.ts', '.tsx', '.js', '.jsx', '.css', '.less', '.json'],
        alias: {
            src: path.resolve(__dirname, '../src/'),
            assets: path.resolve(__dirname, '../src/assets/'),
            components: path.resolve(__dirname, '../src/components/'),
            utils: path.resolve(__dirname, '../src/utils/'),
            servers: path.resolve(__dirname, '../src/servers/'),
            actions: path.resolve(__dirname, '../src/actions/'),
            pages: path.resolve(__dirname, '../src/pages/'),
            request: path.resolve(__dirname, '../src/request.ts'),
            config: path.resolve(__dirname, '../src/config.ts'),
            routeConfig: path.resolve(__dirname, '../src/routeConfig.tsx')
        }
    },
    module: {
        rules: [
            {
                enforce: 'pre',
                test: /\.(ts|tsx|js|jsx)$/,
                exclude: /node_modules/,
                loader: 'eslint-loader',
                options: {
                    cache: true,
                    emitWarning: true,
                    failOnError: true
                }
            },
            {
                test: /\.(tsx|ts)?$/,
                exclude: /node_modules/,
                loader: 'awesome-typescript-loader',
                options: {
                    getCustomTransformers: () => ({
                        before: [
                            tsImportPluginFactory([
                                {
                                    libraryName: 'antd',
                                    libraryDirectory: 'lib',
                                    style: 'css'
                                }
                            ])
                        ]
                    })
                }
            },
            {
                test: /\.(js|jsx)$/,
                loader: 'babel-loader'
            },
            {
                test: /\.(css|less)$/,
                use: [
                    process.env.ENV_LWD == 'development' ? { loader: 'style-loader' } : MiniCssExtractPlugin.loader,
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 1
                        }
                    },
                    {
                        loader: 'less-loader',
                        options: {
                            javascriptEnabled: true
                        }
                    }
                ]
            },
            {
                test: /\.(png|svg|jpg|gif|jpeg)$/,
                loader: 'file-loader',
                options: {
                    outputPath: './assets/images',
                    publicPath: '../assets/images/',
                    esModule: false
                }
            },
            {
                test: /\.(mp3)$/,
                loader: 'file-loader',
                options: {
                    outputPath: './assets/audio',
                    publicPath: '../assets/audio/',
                    esModule: false
                }
            },
            {
                test: /\.(woff|woff2|eot|ttf|otf)$/,
                loader: 'file-loader',
                options: {
                    esModule: false
                }
            }
        ]
    },
    plugins: {
        // 配置入口页面
        html: new HtmlWebpackPlugin({
            title: 'commonVideoClient',
            template: 'public/index.html',
            removeComments: true,
            collapseWhitespace: true,
            removeRedundantAttributes: true,
            useShortDoctype: true,
            removeEmptyAttributes: true,
            removeStyleLinkTypeAttributes: true,
            keepClosingSlash: true,
            minifyJS: true,
            minifyCSS: true,
            minifyURLs: true
        }),
        // 清理dist包
        cleanWebpack: new CleanWebpackPlugin(),
        // 抽取css
        miniCssExtract: new MiniCssExtractPlugin({
            filename: process.env.ENV_LWD == 'development' ? './css/[id].css' : './css/[id].[hash].css',
            chunkFilename: process.env.ENV_LWD == 'development' ? './css/[id].css' : './css/[id].[hash].css',
            ignoreOrder: true
        }),
        namedModules: new webpack.NamedModulesPlugin(),
        // 压缩css
        optimizeCssAssets: new OptimizeCssAssetsPlugin(),
        // 生成包依赖图
        bundleAnalyzer: new BundleAnalyzerPlugin({ analyzerPort: 8081 }),
        // 打包进度
        progressBarPlugin: new ProgressBarPlugin(),
        // 加载中文包
        ContextReplacementPlugin: new webpack.ContextReplacementPlugin(/moment\/locale$/, /zh-cn/),
        CompressionPlugin: new CompressionPlugin({
            filename: '[path].gz[query]',
            algorithm: 'gzip',
            test: /\.js$|\.css$|\.jsx$|\.less$|\.html$/,
            threshold: 10240,
            minRatio: 0.8
        }),
        AntdDayjsWebpackPlugin: new AntdDayjsWebpackPlugin({ preset: 'antdv3' }),
        DefinePlugin: new webpack.DefinePlugin({
            'process.env.NODE_ENV': JSON.stringify(process.env.ENV_LWD)
        }),
        CopyPlugin: new CopyPlugin([{ from: './src/assets/js', to: '../dist/assets/js', toType: 'dir' }]),
        HotModuleReplacementPlugin: new webpack.HotModuleReplacementPlugin()
    },
    devServer: {
        contentBase: path.resolve(__dirname, 'dist'),
        hot: true,
        historyApiFallback: true,
        compress: true
    },
    watchOptions: {
        aggregateTimeout: 600
    },
    // externals 排除对应的包,注:排除掉的包必须要用script标签引入下
    externals: {
        react: 'React',
        'react-dom': 'ReactDOM',
        antd: 'antd'
    }
};

关注 1 回答 0

胡小喵 提出了问题 · 2020-12-28

React17+ts 使用 antd <Spin>组件报错?

React17+ts 使用 antd <Spin>组件

报错内容
'Spin' cannot be used as a JSX component.
Its instance type 'Spin' is not a valid JSX element.

Type 'Spin' is missing the following properties from type 'ElementClass': context, setState, forceUpdate, props, and 2 more.ts(2786)

image.png

也不能给 <Spin> 传递props
image.png

请问这是什么原因?
下面附上我的项目配置

tsconfig

{
    "compilerOptions": {
        "emitDecoratorMetadata": true,
        "experimentalDecorators": true,
        "target": "ES2017",
        "allowSyntheticDefaultImports": true,
        "strict": true,
        "forceConsistentCasingInFileNames": true,
        "allowJs": true,
        "outDir": "./dist/",
        "esModuleInterop": true,
        "noImplicitAny": false,
        "sourceMap": true,
        "module": "esnext",
        "moduleResolution": "node",
        "isolatedModules": true,
        "importHelpers": true,
        "lib": ["esnext", "dom", "dom.iterable"],
        "skipLibCheck": false,
        "jsx": "react",
        "typeRoots": ["node", "node_modules/@types", "./typings"],
        "rootDirs": ["./src"],
        "baseUrl": "./src",
        "paths": {
            "src/*": ["*"],
            "assets/*": ["assets/*"],
            "components/*": ["components/*"],
            "pages/*": ["pages/*"],
            "utils/*": ["utils/*"],
            "servers/*": ["servers/*"],
            "actions/*": ["actions/*"],
            "config": ["config"],
            "routeConfig": ["routeConfig"],
            "request": ["request"]
        }
    },
    "include": ["./src/**/*", "./declaration.d.ts"],
    "exclude": ["node_modules"]
}

webpack配置

const webpack = require('webpack');
const path = require('path');
const tsImportPluginFactory = require('ts-import-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const AntdDayjsWebpackPlugin = require('antd-dayjs-webpack-plugin');

const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');

module.exports = {
    stats: {
        entrypoints: false,
        children: false
    },
    optimization: {
        minimizer: [
            // 压缩js
            new TerserPlugin({
                test: /\.(ts|tsx|js|jsx)$/,
                extractComments: true,
                parallel: true,
                cache: true
            })
        ],
        splitChunks: {
            cacheGroups: {
                vendors: {
                    //node_modules里的代码
                    test: /[\\/]node_modules[\\/]/,
                    chunks: 'all',
                    name: 'vendors', //chunks name
                    priority: 10, //优先级
                    enforce: true
                }
            }
        }
    },
    resolve: {
        extensions: ['.ts', '.tsx', '.js', '.jsx', '.css', '.less', '.json'],
        alias: {
            src: path.resolve(__dirname, '../src/'),
            assets: path.resolve(__dirname, '../src/assets/'),
            components: path.resolve(__dirname, '../src/components/'),
            utils: path.resolve(__dirname, '../src/utils/'),
            servers: path.resolve(__dirname, '../src/servers/'),
            actions: path.resolve(__dirname, '../src/actions/'),
            pages: path.resolve(__dirname, '../src/pages/'),
            request: path.resolve(__dirname, '../src/request.ts'),
            config: path.resolve(__dirname, '../src/config.ts'),
            routeConfig: path.resolve(__dirname, '../src/routeConfig.tsx')
        }
    },
    module: {
        rules: [
            {
                enforce: 'pre',
                test: /\.(ts|tsx|js|jsx)$/,
                exclude: /node_modules/,
                loader: 'eslint-loader',
                options: {
                    cache: true,
                    emitWarning: true,
                    failOnError: true
                }
            },
            {
                test: /\.(tsx|ts)?$/,
                exclude: /node_modules/,
                loader: 'awesome-typescript-loader',
                options: {
                    getCustomTransformers: () => ({
                        before: [
                            tsImportPluginFactory([
                                {
                                    libraryName: 'antd',
                                    libraryDirectory: 'lib',
                                    style: 'css'
                                }
                            ])
                        ]
                    })
                }
            },
            {
                test: /\.(js|jsx)$/,
                loader: 'babel-loader'
            },
            {
                test: /\.(css|less)$/,
                use: [
                    process.env.ENV_LWD == 'development' ? { loader: 'style-loader' } : MiniCssExtractPlugin.loader,
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 1
                        }
                    },
                    {
                        loader: 'less-loader',
                        options: {
                            javascriptEnabled: true
                        }
                    }
                ]
            },
            {
                test: /\.(png|svg|jpg|gif|jpeg)$/,
                loader: 'file-loader',
                options: {
                    outputPath: './assets/images',
                    publicPath: '../assets/images/',
                    esModule: false
                }
            },
            {
                test: /\.(mp3)$/,
                loader: 'file-loader',
                options: {
                    outputPath: './assets/audio',
                    publicPath: '../assets/audio/',
                    esModule: false
                }
            },
            {
                test: /\.(woff|woff2|eot|ttf|otf)$/,
                loader: 'file-loader',
                options: {
                    esModule: false
                }
            }
        ]
    },
    plugins: {
        // 配置入口页面
        html: new HtmlWebpackPlugin({
            title: 'commonVideoClient',
            template: 'public/index.html',
            removeComments: true,
            collapseWhitespace: true,
            removeRedundantAttributes: true,
            useShortDoctype: true,
            removeEmptyAttributes: true,
            removeStyleLinkTypeAttributes: true,
            keepClosingSlash: true,
            minifyJS: true,
            minifyCSS: true,
            minifyURLs: true
        }),
        // 清理dist包
        cleanWebpack: new CleanWebpackPlugin(),
        // 抽取css
        miniCssExtract: new MiniCssExtractPlugin({
            filename: process.env.ENV_LWD == 'development' ? './css/[id].css' : './css/[id].[hash].css',
            chunkFilename: process.env.ENV_LWD == 'development' ? './css/[id].css' : './css/[id].[hash].css',
            ignoreOrder: true
        }),
        namedModules: new webpack.NamedModulesPlugin(),
        // 压缩css
        optimizeCssAssets: new OptimizeCssAssetsPlugin(),
        // 生成包依赖图
        bundleAnalyzer: new BundleAnalyzerPlugin({ analyzerPort: 8081 }),
        // 打包进度
        progressBarPlugin: new ProgressBarPlugin(),
        // 加载中文包
        ContextReplacementPlugin: new webpack.ContextReplacementPlugin(/moment\/locale$/, /zh-cn/),
        CompressionPlugin: new CompressionPlugin({
            filename: '[path].gz[query]',
            algorithm: 'gzip',
            test: /\.js$|\.css$|\.jsx$|\.less$|\.html$/,
            threshold: 10240,
            minRatio: 0.8
        }),
        AntdDayjsWebpackPlugin: new AntdDayjsWebpackPlugin({ preset: 'antdv3' }),
        DefinePlugin: new webpack.DefinePlugin({
            'process.env.NODE_ENV': JSON.stringify(process.env.ENV_LWD)
        }),
        CopyPlugin: new CopyPlugin([{ from: './src/assets/js', to: '../dist/assets/js', toType: 'dir' }]),
        HotModuleReplacementPlugin: new webpack.HotModuleReplacementPlugin()
    },
    devServer: {
        contentBase: path.resolve(__dirname, 'dist'),
        hot: true,
        historyApiFallback: true,
        compress: true
    },
    watchOptions: {
        aggregateTimeout: 600
    },
    // externals 排除对应的包,注:排除掉的包必须要用script标签引入下
    externals: {
        react: 'React',
        'react-dom': 'ReactDOM',
        antd: 'antd'
    }
};

关注 1 回答 0

胡小喵 赞了文章 · 2020-12-07

前端装逼技巧 108 式(二)—— 不讲武德

“小马同学,背一下《陋室铭》。”“山不在高,有仙则名。水不在深,有龙则灵。斯是陋室,惟馨。”“停,怎么少了俩字?”“年轻人不讲吾德。”

系列文章汇总:

文章风格所限,引用资料部分,将在对应小节末尾标出。

第十九式:西施宜笑复宜颦,丑女效之徒累身 —— window.btoawindow.atob,命名这么随意的API可以用来干什么?

  单从命名来看,完全让人摸不着头脑的两个API,我们到底可以用他们来干些什么呢?(我甚至怀疑,如果在项目中使用这样的命名,完全可能被同事打,哈哈)

  • window.atob() 函数用来解码一个已经被base-64编码过的数据。你可以使用 window.btoa() 方法来编码一个可能在传输过程中出现问题的数据,并且在接受数据之后,使用 window.atob() 方法来将数据解码。例如:你可以把ASCII里面数值0到31的控制字符进行编码,传输和解码。
  • window.btoa():将ASCII字符串或二进制数据转换成一个base64编码过的字符串,该方法不能直接作用于Unicode字符串
  • 在各浏览器中,使用 window.btoa 对Unicode字符串进行编码都会触发一个字符越界的异常。
  • 前端可以使用这两个API对URL路由参数、敏感信息等进行转码,防止明文暴露。
let encodedData = window.btoa("Hello, world"); // 编码
console.log(encodedData);                      // SGVsbG8sIHdvcmxk
let decodedData = window.atob(encodedData);    // 解码
console.log(decodedData);                      // Hello, world
let encodeUTF = window.btoa(encodeURIComponent('啊'));
console.log(encodeUTF);                        // JUU1JTk1JThB
let decodedUTF = decodeURIComponent(atob(encodeUTF));
console.log(decodedUTF);                       // 啊
资料参考:window.atob()与window.btoa()方法实现编码与解码 | WindowOrWorkerGlobalScope.atob() | WindowOrWorkerGlobalScope.btoa()

第二十式:escapeencodeURIencodeURIComponent,这些编码 API 有什么区别?

  • escape 是对字符串(string)进行编码(而另外两种是对 URL),作用是让它们在所有电脑上可读。编码之后的效果是%XX或者%uXXXX这种形式。其中 ASCII 字母、数字、@*/+ ,这几个字符不会被编码,其余的都会。最关键的是,当你需要对 URL 编码时,请忘记这个方法,这个方法是针对字符串使用的,不适用于 URL
  • encodeURIencodeURIComponent 都是编码 URL,唯一区别就是编码的字符范围;
  • encodeURI 方法不会对下列字符编码:ASCII 字母、数字、~!@#$&*()=:/,;?+'
  • encodeURIComponent 方法不会对下列字符编码:ASCII 字母、数字、~!*()'
  • 也就是 encodeURIComponent 编码的范围更广,会将http://XXX中的//也编码,会导致 URL 不可用。(其实 java 中的 URLEncoder.encode(str,char)也类似于这个方法,会导致 URL 不可用)。
  • 使用场景:

    • 如果只是编码字符串,不和 URL 有半毛钱关系,那么用 escape,而且这个方法一般不会用到;
    • 如果你需要编码整个 URL,然后需要使用这个 URL,那么用 encodeURI
    • 当你需要编码 URL 中的参数的时候,那么 encodeURIComponent 是最好方法;
    • 某些场景下,编码之后导致URL不可用(比如笔者曾遇到预览附件时某些附件URL无法打开的问题),可尝试考虑是否是因为特殊字符导致的。
  • 如果不生效可以用两次编码:关于两次编码的原因
资料参考:escape、encodeURI 和 encodeURIComponent 的区别

第二十一式:这不是我访问的页面 —— 经常碰到移动端DNS域名劫持问题?来一起了解下HTTPDNS是什么,解决了什么问题吧

  对于互联网,域名是访问的第一跳,而这一跳很多时候会“失足”(尤其是移动端网络),导致访问错误内容、失败连接等,让用户在互联网上畅游的爽快瞬间消失。但凡使用域名来给用户提供服务的互联网企业,都或多或少地无法避免在有中国特色的互联网环境中遭遇到各种域名被缓存、用户跨网访问缓慢等问题。

  • DNS 解析过程:

DNS 解析过程

  • 什么HttpDNS:

  HTTPDNS 利用 HTTP 协议与 DNS 服务器交互,代替了传统的基于 UDP 协议的 DNS 交互,绕开了运营商的 Local DNS,有效防止了域名劫持,提高域名解析效率。另外,由于 DNS 服务器端获取的是真实客户端 IP 而非 Local DNS 的 IP,能够精确定位客户端地理位置、运营商信息,从而有效改进调度精确性

  • HttpDNS 主要解决的问题:

    • Local DNS 劫持:由于 HttpDns 是通过 IP 直接请求 HTTP 获取服务器 A 记录地址,不存在向本地运营商询问 domain 解析过程,所以从根本避免了劫持问题。
    • 平均访问延迟下降:由于是 IP 直接访问省掉了一次 domain 解析过程,通过智能算法排序后找到最快节点进行访问。
    • 用户连接失败率下降:通过算法降低以往失败率过高的服务器排序,通过时间近期访问过的数据提高服务器排序,通过历史访问成功记录提高服务器排序。
  • HttpDNS的原理

    • 客户端直接访问HttpDNS接口,获取业务在域名配置管理系统上配置的访问延迟最优的IP。(基于容灾考虑,还是保留次选使用运营商LocalDNS解析域名的方式);
    • 客户端获取到IP后就直接向此IP发送业务协议请求。以Http请求为例,通过在header中指定host字段,向HttpDNS返回的IP发送标准的Http请求即可。
详细资料参考:全面了解移动端DNS域名劫持等杂症:原理、根源、HttpDNS解决方案等

第二十二式:depcheck一下你的前端项目中是否存在未使用的依赖包

  很多时候,也许我们的前端项目是基于某个已有的项目进行”复制搭建“,或者直接使用UmiJS这样的企业级 react 应用框架,又或者基于Ant Design Pro等开源项目进行删改,难免会存在未使用到的依赖包被安装,拖累项目安装速度,增大项目打包体积等,这时,我们就可以考虑使用depcheck找出那些未使用的依赖包进行移除。

  • npm install depcheck -g
  • cd 到要检查的项目目录,运行 depcheck

      D:\project>depcheck
      Unused devDependencies  #未使用的依赖
        * @antv/data-set
        * echarts
        * echarts-for-react
        * qs
      * Unused devDependencies #未使用的devDependencies
        * chalk
        * enzyme
        * express
      Missing dependencies  #缺少的dependencies
        * immutability-helper: .\src\components\EditColums\EditColumnsTable.js
        * slash2: .\config\config.js
UmiJS学习参考:UmiJS | [react]初识Umi.JS

第二十三式:防止误操作,如何在组件卸载、路由跳转、页面关闭(刷新)之前进行提示

  工作中经常会有大表单填写、提交这样的需求,如果用户写了大量内容,因为误操作,刷新或者关闭了页面,填写信息用没有做缓存,此时用户的内心应该是奔溃的。

  React组件卸载、路由跳转、页面关闭(刷新)之前进行提示(如果是AntD Modal弹窗里面的表单,也可视情考虑将maskClosable属性设置为false,防止误点蒙层导致弹窗关闭):

//监听窗口事件
useEffect(() => {
  const listener = (ev) => {
    ev.preventDefault();
    ev.returnValue = '确定离开吗?';
  };
  window.addEventListener('beforeunload', listener);
  return () => {
    // 在末尾处返回一个函数
    // React 在该函数组件卸载前调用该方法(实现 componentWillUnmount)
    window.removeEventListener('beforeunload', listener);
  };
}, []);

第二十四式:不带括号也能执行函数调用?console.log\`hello world\`会打印出什么

  • 直接看结果:
console.log`hello world` // 打印出一个数组:["hello world", raw: Array(1)]
  • 再看看以下代码:
const name = 'jack'
const gender = false
// 带括号
console.log(`hey, ${name} is a ${gender ? 'girl' : 'boy'}.`) // hey, jack is a boy.
// 不带括号
console.log`hey, ${name} is a ${gender ? 'girl' : 'boy'}.` // ["hey, ", " is a ", ".", raw: Array(3)] 'jack' 'boy'

  从最后一行打印可以看出数组中的项是以'插入表达式'作为分割生成的,并且插入表答式中的内容参数,也会依次打印出来。这就是带标签的模板字符串

  • 模板字符串的语法:
// 普通
`string text`

// 换行
`string text line 1
 string text line 2`

// 插值
`string text ${expression} string text`

// 带标签的模板字符串
tag `string text ${expression} string text`
  • 可以做什么:
const name = 'jack'
const gender = false

function myTagFunc(strings, name, gender) {
    const sex = gender ? 'girl' : 'boy'
    // return 'hello world'
    return strings[0] + name + strings[1] + sex + strings[2]
}

// result 的值是myTagFunc函数的返回值
// 如果myTagFunc返回 hello world,result就是hello world
// 这样可在一定程度上避免在模板字符串内写复杂的逻辑
const result = myTagFunc`hey, ${name} is a ${gender}.`
console.log(result) // hey, jack is a boy.

  在标签函数的第一个参数中,存在一个特殊的属性raw ,我们可以通过它来访问模板字符串的原始字符串,而不经过特殊字符的替换。

function tag(strings) {
  console.log(strings.raw[0]);
}
tag`string text line 1 \n string text line 2`;// "string text line 1 \n string text line 2"
console.log`string text line 1 \n string text line 2` // ["string text line 1 ↵ string text line 2", raw: Array(1)]

原始字符串

参考资料:MDN-带标签的模板字符串 | 带标签的模板字符串

第二十五式:还在用闭包实现自增Id?何不试试优雅大气的Generator?

  • 闭包
function createIdMaker(){
    let id = 1;
    return function (){
        return id ++;
    }
}

const idMaker =  createIdMaker();

console.log(idMaker()) // 1
console.log(idMaker()) // 2
console.log(idMaker()) // 3
  • Generator
function * createIdMaker() {
  let id = 1
  while(true) yield id ++;
}
const idMaker = createIdMaker()
console.log(idMaker.next().value) // 1
console.log(idMaker.next().value) // 2
console.log(idMaker.next().value) // 3

第二十六式:年轻人不讲武德,谁动了我的对象 —— 对象属性会自己偷偷排队?

  程序员眼里只有女神,对象是不会有的,就算有,还能无可挑剔、百依百顺不成?缺对象那就new一个咯,个性化定制,绝对的理想型。控制不了现实里的对象还能控制不了new出来的对象了?事实上,你,真的不能。

  • 试想一下,下面的代码会按照什么顺序输出:
function Foo() {
  this[200] = 'test-200';
  this[1] = 'test-1';
  this[100] = 'test-100';
  this['B'] = 'bar-B';
  this[50] = 'test-50';
  this[9] = 'test-9';
  this[8] = 'test-8';
  this[3] = 'test-3';
  this[5] = 'test-5';
  this['D'] = 'bar-D';
  this['C'] = 'bar-C';
}
var bar = new Foo();

for (key in bar) {
  console.log(`index:${key}  value:${bar[key]}`);
}

  在 ECMAScript 规范中定义了数字属性应该按照索引值大小升序排列,字符串属性根据创建时的顺序升序排列。我们把对象中的数字属性称为排序属性,在 Chrome V8 引擎 中被称为 elements,字符串属性就被称为常规属性,在 V8 中被称为 properties。在 V8 内部,为了有效地提升存储和访问这两种属性的性能,分别使用了两个线性数据结构来分别保存排序属性和常规属性。同时 v8 将部分常规属性直接存储到对象本身,我们把这称为对象内属性 (in-object properties),不过对象内属性的数量是固定的,默认是 10 个。更多详情可参考笔者之前的一篇文章浏览器是如何工作的:Chrome V8让你更懂JavaScript —— 【V8 内部是如何存储对象的:快属性和慢属性】一节。

  • 结果揭晓
//输出:
// index:1  value:test-1
// index:3  value:test-3
// index:5  value:test-5
// index:8  value:test-8
// index:9  value:test-9
// index:50  value:test-50
// index:100  value:test-100
// index:200  value:test-200
// index:B  value:bar-B
// index:D  value:bar-D
// index:C  value:bar-C
资料参考:浏览器是如何工作的:Chrome V8让你更懂JavaScript

第二十七式:VS Code里竟然有谷歌开发者工具面板?它 和 Chrome有什么关系?

  如下图所示,我们经常用的开发工具VSCode竟与浏览器如此相像,莫非他们是失散多年的兄弟?诶,你还别说,还真有那么点意思。(点击帮助【Help】 下的 切换开发人员工具即可打开以下面板)

VSCode

  VS Code 是基于 Electron (原来叫 Atom Shell) 进行开发的。Electron 基于 Node.js(作为后端运行时)和 Chromium(作为前端渲染),使得开发者可以使用 HTML, CSS 和 JavaScript 等前端技术来开发跨平台桌面 GUI 应用程序。Atom, GitHub Desktop, Slack, Microsoft Teams, WordPress Desktop 等知名软件都是基于 Electron 开发的。Electron比你想象的更简单,如果你可以建一个网站,你就可以建一个桌面应用程序

  VS Code 的其他的主要组件有:

参考资料:vs code的界面是用的什么技术? | Electron | Electron 快速入门

第二十八式:"★★★★★☆☆☆☆☆".slice(5 - rate, 10 - rate) —— 一个正经又有点邪气的组件封装

  开始看到这行评级rate组件的代码,是在一篇充满邪气的文章信条|手撕吊打面试官系列面试题里,总觉得这个组件与那篇文章的文风不对应,甚至觉得这个实现还足够机智,值得借鉴,我是不是没救了,哈哈。

{
  let rate = 3;
  "★★★★★☆☆☆☆☆".slice(5 - rate, 10 - rate);
}
参考资料:信条|手撕吊打面试官系列面试题

第二十九式:Uncaught TypeError: obj is not iterablefor of 遍历普通对象报错,如何快速使普通对象可被 for of 遍历?

  for of可以迭代Arrays(数组), Maps(映射), Sets(集合)、NodeList对象、Generator等,甚至连Strings(字符串)都可以迭代,却不能遍历普通对象?

// 字符串
const iterable = 'ES6';
for (const value of iterable) {
  console.log(value);
}
// Output:
// "E"
// "S"
// "6"

// 普通对象
const obj = {
  foo: 'value1',
  bar: 'value2'
}
for(const item of obj){
  console.log(item)
}
// Uncaught TypeError: obj is not iterable

  我们先从对象的几个方法Object.values()Object.keys()Object.entries()看起吧:

const obj = {
  foo: 'value1',
  bar: 'value2'
}
// 打印由value组成的数组
console.log(Object.values(obj))

// 打印由key组成的数组
console.log(Object.keys(obj))

// 打印由[key, value]组成的二维数组
console.log(Object.entries(obj))

// 方法一:使用of遍历普通对象的方法
for(const [, value] of Object.entries(obj)){
  console.log(value)
}

// 普通对象转Map
console.log(new Map(Object.entries(obj)))

// 方法二:遍历普通对象生成的Map
for(const [, value] of new Map(Object.entries(obj))){
  console.log(value)
}

  普通对象为何不可被for of迭代请参考下一式。

第三十式:可以遍历绝大部分数据类型的for of为什么不能遍历普通对象?

  • 普通对象为何不可被 for of 迭代
{
  // 数组
  const iterable = ['a', 'b'];
  for (const value of iterable) {
    console.log(value);
  }
  // Output:
  // a
  // b
}
{
  // Set(集合)
  const iterable = new Set([1, 2, 2, 1]);
  for (const value of iterable) {
    console.log(value);
  }
  // Output:
  // 1
  // 2
}
{
// Arguments Object(参数对象)
function args() {
  for (const arg of arguments) {
    console.log(arg);
  }
}
args('a', 'b');
// Output:
// a
// b
}

iterator1

iterator2

iterator3

  可以看到,这些可被for of迭代的对象,都实现了一个Symbol(Symbol.iterator)方法,而普通对象没有这个方法。

  简单来说,for of 语句创建一个循环来迭代可迭代的对象,可迭代的对象内部实现了Symbol.iterator方法,而普通对象没有实现这一方法,所以普通对象是不可迭代的。

  • 如何实现Symbol.iterator方法,使普通对象可被 for of 迭代
// 普通对象
const obj = {
  foo: 'value1',
  bar: 'value2',
  [Symbol.iterator]() {
    // 不用担心[Symbol.iterator]属性会被Object.keys()获取到,
    // Symbol.iterator需要通过Object.getOwnPropertySymbols(obj)获取,
    // Object.getOwnPropertySymbols() 方法返回一个给定对象自身的所有 Symbol 属性的数组。
    const keys = Object.keys(obj); 
    let index = 0;
    return {
      next: () => {
        if (index < keys.length) {
          return {
            value: this[keys[index++]],
            done: false
          };
        } else {
          return { value: undefined, done: true };
        }
      }
    };
  }
}
for (const value of obj) {
  console.log(value); // value1 value2
}

  上面给obj实现了Symbol.iterator接口后,我们甚至还可以像下面这样把对象转换成数组:

console.log([...obj]); // ["value1", "value2"]
console.log([...{}]); // console.log is not iterable (cannot read property Symbol(Symbol.iterator))

  更多关于for...of的探讨,可参考笔者的一文带你理解:可以迭代大部分数据类型的 for…of 为什么不能遍历普通对象?

参考资料:MDN:for...of | Understanding the JavaScript For...of Loop【译文】)| Iterator 和 for...of 循环

第三十一式:position定位只知道absolutefixedrelativestatic?,sticky其实可以很惊艳

  • absolute:生成绝对定位的元素,相对于 static 定位以外的第一个父元素进行定位。元素的位置通过 "left", "top", "right" 以及 "bottom" 属性进行规定。
  • fixed:生成绝对定位的元素,相对于浏览器窗口进行定位。元素的位置通过 "left", "top", "right" 以及 "bottom" 属性进行规定。
  • relative:生成相对定位的元素,相对于其正常位置进行定位。因此,"left:20" 会向元素的 LEFT 位置添加 20 像素。
  • static:默认值。没有定位,元素出现在正常的流中。
  • sticky:粘性定位,该定位基于用户滚动的位置。当元素在屏幕内,它的行为就像 position:relative;, 而当页面滚动超出目标区域时,它的表现就像 position:fixed;,它会固定在目标位置。

  position:sticky实现的惊艳吸顶效果可点击这里

// 用法:nav元素实现粘性定位
nav {
    position: -webkit-sticky;
    position: sticky;
    top: 0;
}

  使用position:sticky的过程中,也许会有一些坑,比如要想sticky生效,top属性或则left属性(看滚动方向)是必须要有明确的计算值的,否则fixed的表现不会出现。详情可参考《CSS世界》作者张鑫旭大佬的杀了个回马枪,还是说说position:sticky吧

参考资料:CSS position 属性 | 杀了个回马枪,还是说说position:sticky吧

第三十二式:傍能行仁义,莫若妾自知 —— getBoundingClientRect让你找准定位不迷失自我

  • 什么是 getBoundingClientRect

  Element.getBoundingClientRect()方法,用来描述一个元素的具体位置,该位置的四个属性都是相对于视口左上角的位置而言的。对某一节点执行该方法,它的返回值是一个 DOMRect 类型的对象。这个对象表示一个矩形盒子,它含有:left、top、right 和 bottom 等只读属性,具体含义如下图所示:

getBoundingClientRect

  • offset 和 getBoundingClientRect() 区别

    • offset 指偏移,包括这个元素在文档中占用的所有显示宽度,包括滚动条、padding、border,不包括overflow隐藏的部分;
    • offset 的方向值需要考虑到父级,如果父级是定位元素,那么子元素的offset值相对于父元素;如果父元素不是定位元素,那么子元素的offset值相对于可视区窗口;
    • offsetParent:获取当前元素的定位父元素

      • 如果当前元素的父元素,有CSS定位(position为absolute、relative、fixed),那么 offsetParent 获取的是最近的那个父元素。
      • 如果当前元素的父元素,没有CSS定位(position为absolute、relative、fixed),那么offsetParent 获取的是body
    • getBoundingClientRect() 的值只相对于可视区窗口,所以在很多场景下更容易“找准定位”。
  • 能做什么:滚动吸顶效果

  笔者写此节之前有做过一个表格分页器固定在浏览器底部、表头滚动吸顶的效果,主要参考了position:sticky属性和getBoundingClientRect。写此节查阅资料时有发现【前端词典】5 种滚动吸顶实现方式的比较(性能升级版) 这篇文章,对五种吸顶方式做了详尽的分析和对比,大家有兴趣可以看看。同时,《CSS世界》作者张鑫旭大佬在杀了个回马枪,还是说说position:sticky吧sticky定位也有详尽的介绍。本来还想在后续的章节谈谈吸顶,现在可能需要重新评估了,哈哈。

  滚动吸顶表格示例:

position

  【前端词典】5 种滚动吸顶实现方式的比较(性能升级版)一文中的getBoundingClientRect吸顶实现:

// html
<div class="pride_tab_fixed" ref="pride_tab_fixed">
    <div class="pride_tab" :class="titleFixed == true ? 'isFixed' :''">
        // some code
    </div>
</div>

// vue
export default {
    data(){
      return{
        titleFixed: false
      }
    },
    activated(){
      this.titleFixed = false;
      window.addEventListener('scroll', this.handleScroll);
    },
    methods: {
      //滚动监听,头部固定
      handleScroll: function () {
        let offsetTop = this.$refs.pride_tab_fixed.getBoundingClientRect().top;
        this.titleFixed = offsetTop < 0;
        // some code
      }
    }
  }
参考资料:getBoundingClientRect 方法 | 杀了个回马枪,还是说说position:sticky吧 | 【前端词典】5 种滚动吸顶实现方式的比较【性能升级版】 | JS 中的offset、scroll、client总结

第三十三式:Console Importer 让你的浏览器控制台成为更强大的实验场

  平时开发中,我们经常会在控制台尝试一些操作,Console Importer是一个可以在Chrome Console面板安装(引入)loadsh、moment、jQuery、React等资源的插件,语法也很简单,比如$i('moment')即可引入moment库,然后即可在控制台直接验证、使用这些库:

  • 使用示例:

import

  • 效果图:

Console Importer

  • 引入资源方法:
$i('jquery') // 直接引入
$i('jquery@2') // 指定版本
$i('https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js') // cdn地址
$i('https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css') // 引入CSS
链接:Console Importer | Chrome Web Store 地址

第三十四式:误用git reset --hard,我真的没救了吗? —— 认识一下 git reflog 时光穿梭机

  • 我们直奔主题,先看下面的问题:

  懵懂的小白花费一周时间做了git log如下所示的6个功能,每个功能对应一个commit的提交,分别是feature-1 到 feature-6”:

git1

  然后错误的执行了强制回滚,git reset --hard 2216d4e,回滚到了feature-1上,并且回滚的时候加了--hard,导致之前feature-2 到 feature-6的所有代码全部弄丢了,现在git log上显示如下:

git2

  然后,又在此基础上新添加了一个commit提交,信息叫feature-7:

git3

  请问:如何把丢失的代码feature-2 到 feature-6全部恢复回来,并且feature-7的代码也要保留

  • 接下来,我们先回忆几个git命令:

    • git reset --hard撤销工作区中所有未提交的修改内容,将暂存区与工作区都回到上一次版本,并删除之前的所有信息提交,谨慎使用 –hard 参数,它会删除回退点之前的所有信息;
    • git log 命令可以显示所有提交过的版本信息;
    • git reflog 可以查看所有分支的所有操作记录(包括已经被删除的 commit 记录和 reset 的操作);
    • git cherry-pick命令的作用,就是将指定的提交(commit)应用于其他分支。
  • 最后,给出解答:git refloggit cherry-pick

    • 首先,使用 git reflog 查看所以git操作记录,记下feature-7和feature-6的hash码。

    git4

    • 其次,git reset --hard cd52afc回滚到feature-6。此时我们已经完成了要求的一半:成功回到了feature-6上,但是feature-7没了。
    • 最后,git cherry-pick 4c97ff3,执行完成之后,feature-7的代码就回来了,大功告成。
更多git知识点推荐阅读GitHub联合创始人Scott Chacon 和 Ben Straub的开源巨作《Pro Git》

参考资料:git时光穿梭机--女神的侧颜 | git cherry-pick 教程 | Git Reset 三种模式

第三十五式:文件上传只会使用 form 和 Ant Design Upload组件?

  最近有做一个由其他部门提供接口的需求,上传文件的接口文档如下图所示,文件内容是base64格式,且要和其他参数一起传递。笔者以前做的需求,上传文件一般是通过form、Ant Design Upload组件、FormData等方式,上传成功得到一个URL,表单提交时将得到的URL传给后端;下载通过Blob、后端返回URL、发送邮件、或者前端生成Excel等方式。这次的上传使用了FileReader,简单记录相关实现。关于大文件的上传和下载,之后的章节会进行探讨。

fileupload

  • FileReader上传代码实现
  // DOM 
  <input type='file' id='file' onChange={(e) => this.uploadFile(e)} />
  // js
  uploadFile(e) {
    const file = e.target.files[0];
    const reader = new FileReader();
    // 处理loadend事件。该事件在读取操作结束时(要么成功,要么失败)触发。
    reader.onloadend = () => {
      this.setState({
        // 存储
        XXXFile: {
          // 除了name外,file中可被读取的属性还包括size、type、lastModifiedDate
          Name: file.name,
          // base64格式文件数据
          // 一次性发送大量的base64数据会导致浏览器卡顿,服务器端接收这样的数据可能也会出现问题。
          Buffer: reader.result.replace(/data.*base64[,]/, '')
        }
      })
    }
    reader.readAsDataURL(file);
  }
  • FileReader方法拓展:

    • FileReader.abort():中止读取操作。在返回时,readyState属性为DONE。
    • FileReader.readAsArrayBuffer():开始读取指定的 Blob中的内容, 一旦完成, result 属性中保存的将是被读取文件的 ArrayBuffer 数据对象.
    • FileReader.readAsBinaryString():开始读取指定的Blob中的内容。一旦完成,result属性中将包含所读取文件的原始二进制数据。
    • FileReader.readAsDataURL():开始读取指定的Blob中的内容。一旦完成,result属性中将包含一个data: URL格式的Base64字符串以表示所读取文件的内容。
    • FileReader.readAsText():开始读取指定的Blob中的内容。一旦完成,result属性中将包含一个字符串以表示所读取的文件内容。
  • 其他文件上传参考资料:

第三十六式:2**53 === 2 ** 53 + 1?如果没有BigInt,该如何进行大数求和?

  • Number.MAX_SAFE_INTEGER:值为9007199254740991,即2 ** 53 - 1,小于该值能精确表示。然后我们会发现2**53 === 2 ** 53 + 1true
  • Number.MAX_VALUE::值为1.7976931348623157e+308,大于该值得到的是Infinity,介于Infinity和安全值之间的无法精确表示。

  既然我们不能实现直接相加,我们可以利用字符串分割成字符串数组的方式来对每一位进行相加。

  • 大数相加实现
function add (str1, str2) {
    // 转为单字符串数组
    str1=(str1+'').split('');
    str2=(str2+'').split('');
    let result='';//存储结果
    let flag=0; // 存储进位
    while(str1.length || str2.length || flag){// 是否还有没有相加的位或者大于0的进位
    // ~~str1.pop()得到最右边一位,并转成数字(~为按位取反运算符,详见第十四式)
    // 对应位数字相加,再加上进位
        flag += ~~str1.pop() + ~~str2.pop();
    // 去除进位,然后进行字符串拼接
        result = flag%10 + result;
    // 进位,0或1
        flag = +(flag>9);
    }
  // 去除开头(高位)的0
  return result.replace(/^0+/, '');
};
// 2 ** 53:9007199254740992
// add(2**53, 1): "9007199254740993"
// 2**53+1: 9007199254740992
// BigInt结果
// 2n**53n+1n:9007199254740993n
  • 加减乘除:

  关于加减乘除的实现可参考大数运算js实现,基本思路:

1、大数加法和减法是一个道理,既然我们不能实现直接相加减,我们可以利用字符串分割成字符串数组的方式。
2、乘法:每个位数两两相乘,最后错位相加。
参考资料:JS 大数相加 | 前端应该知道的JavaScript浮点数和大数的原理

本文首发于个人博客,欢迎指正和star

查看原文

赞 79 收藏 58 评论 6

胡小喵 赞了文章 · 2020-12-07

一文带你理解:可以迭代大部分数据类型的 for…of 为什么不能遍历普通对象?

for…of 及其使用

  我们知道,ES6 中引入 for...of 循环,很多时候用以替代 for...inforEach() ,并支持新的迭代协议。for...of 允许你遍历 Array(数组), String(字符串), Map(映射), Set(集合),TypedArray(类型化数组)、arguments、NodeList对象、Generator等可迭代的数据结构等。for...of语句在可迭代对象上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的执行语句。

for...of的语法:

for (variable of iterable) {
    // statement
}
// variable:每个迭代的属性值被分配给该变量。
// iterable:一个具有可枚举属性并且可以迭代的对象。

常用用法

{
  // 迭代字符串
  const iterable = 'ES6';
  for (const value of iterable) {
    console.log(value);
  }
  // Output:
  // "E"
  // "S"
  // "6"
}
{
  // 迭代数组
  const iterable = ['a', 'b'];
  for (const value of iterable) {
    console.log(value);
  }
  // Output:
  // a
  // b
}
{
  // 迭代Set(集合)
  const iterable = new Set([1, 2, 2, 1]);
  for (const value of iterable) {
    console.log(value);
  }
  // Output:
  // 1
  // 2
}
{
  // 迭代Map
  const iterable = new Map([["a", 1], ["b", 2], ["c", 3]]);
  for (const entry of iterable) {
    console.log(entry);
  }
  // Output:
  // ["a", 1]
  // ["b", 2]
  // ["c", 3]

  for (const [key, value] of iterable) {
    console.log(value);
  }
  // Output:
  // 1
  // 2
  // 3
}
{
  // 迭代Arguments Object(参数对象)
  function args() {
    for (const arg of arguments) {
      console.log(arg);
    }
  }
  args('a', 'b');
  // Output:
  // a
  // b
}
{
  // 迭代生成器
  function* foo(){ 
    yield 1; 
    yield 2; 
    yield 3; 
  }; 

  for (let o of foo()) { 
    console.log(o); 
  }
  // Output:
  // 1
  // 2
  // 3
}

Uncaught TypeError: obj is not iterable

// 普通对象
const obj = {
  foo: 'value1',
  bar: 'value2'
}
for(const item of obj){
  console.log(item)
}
// Uncaught TypeError: obj is not iterable

  可以看出,for of可以迭代大部分对象甚至字符串,却不能遍历普通对象。

如何用for...of迭代普通对象

  通过前面的基本用法,我们知道,for...of可以迭代数组、Map等数据结构,顺着这个思路,我们可以结合对象的Object.values()Object.keys()Object.entries()方法以及解构赋值的知识来用for...of遍历普通对象。

  • Object.values()Object.keys()Object.entries()用法及返回值
const obj = {
  foo: 'value1',
  bar: 'value2'
}
// 打印由value组成的数组
console.log(Object.values(obj)) // ["value1", "value2"]

// 打印由key组成的数组
console.log(Object.keys(obj)) // ["foo", "bar"]

// 打印由[key, value]组成的二维数组
// copy(Object.entries(obj))可以把输出结果直接拷贝到剪贴板,然后黏贴
console.log(Object.entries(obj)) // [["foo","value1"],["bar","value2"]]
  • 因为for...of可以迭代数组和Map,所以我们得到以下遍历普通对象的方法
const obj = {
  foo: 'value1',
  bar: 'value2'
}
// 方法一:使用for of迭代Object.entries(obj)形成的二维数组,利用解构赋值得到value
for(const [, value] of Object.entries(obj)){
  console.log(value) // value1, value2
}

// 方法二:Map
// 普通对象转Map
// Map 可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组
console.log(new Map(Object.entries(obj)))

// 遍历普通对象生成的Map
for(const [, value] of new Map(Object.entries(obj))){
  console.log(value) // value1, value2
}

// 方法三:继续使用for in
for(const key in obj){
  console.log(obj[key]) // value1, value2
}

{
  // 方法四:将【类数组(array-like)对象】转换为数组
  // 该对象需具有一个 length 属性,且其元素必须可以被索引。
  const obj = {
    length: 3, // length是必须的,否则什么也不会打印
    0: 'foo',
    1: 'bar',
    2: 'baz',
    a: 12  // 非数字属性是不会打印的
  };
  const array = Array.from(obj); // ["foo", "bar", "baz"]
  for (const value of array) { 
      console.log(value);
  }
  // Output: foo bar baz
}
{
  // 方法五:给【类数组】部署数组的[Symbol.iterator]方法【对普通字符串属性对象无效】
  const iterable = {
    0: 'a',
    1: 'b',
    2: 'c',
    length: 3,
    [Symbol.iterator]: Array.prototype[Symbol.iterator]
  };
  for (let item of iterable) {
    console.log(item); // 'a', 'b', 'c'
  }
}

注意事项

  • 有别于不可终止遍历的forEachfor...of的循环可由breakthrowcontinuereturn终止,在这些情况下,迭代器关闭。
  const obj = {
    foo: 'value1',
    bar: 'value2',
    baz: 'value3'
  }
  for(const [, value] of Object.entries(obj)){
    if (value === 'value2') break // 不会再执行下次迭代
    console.log(value) // value1
  };
  [1,2].forEach(item => {
      if(item == 1) break // Uncaught SyntaxError: Illegal break statement
      console.log(item)
  });
  [1,2].forEach(item => {
      if(item == 1) continue // Uncaught SyntaxError: Illegal continue statement: no surrounding iteration statement
      console.log(item)
  });
  [1,2].forEach(item => {
      if(item == 1) return // 仍然会继续执行下一次循环,打印2
      console.log(item) // 2
  })
  • For…ofFor…in对比

    • for...in 不仅枚举数组声明,它还从构造函数的原型中查找继承的非枚举属性;
    • for...of 不考虑构造函数原型上的不可枚举属性(或者说for...of语句遍历可迭代对象定义要迭代的数据。);
    • for...of 更多用于特定的集合(如数组等对象),但不是所有对象都可被for...of迭代。
      Array.prototype.newArr = () => {};
      Array.prototype.anotherNewArr = () => {};
      const array = ['foo', 'bar', 'baz'];
      for (const value in array) { 
        console.log(value); // 0 1 2 newArr anotherNewArr
      }
      for (const value of array) { 
        console.log(value); // 'foo', 'bar', 'baz'
      }

普通对象为何不能被 for of 迭代

  前面我们有提到一个词叫“可迭代”数据结构,当用for of迭代普通对象时,也会报一个“not iterable”的错误。实际上,任何具有 Symbol.iterator 属性的元素都是可迭代的。我们可以简单查看几个可被for of迭代的对象,看看和普通对象有何不同:

iterator1

iterator2

iterator3

  可以看到,这些可被for of迭代的对象,都实现了一个Symbol(Symbol.iterator)方法,而普通对象没有这个方法。

  简单来说,for of 语句创建一个循环来迭代可迭代的对象,可迭代的对象内部实现了Symbol.iterator方法,而普通对象没有实现这一方法,所以普通对象是不可迭代的。

Iterator(遍历器)

  关于Iterator(遍历器)的概念,可以参照阮一峰大大的《ECMAScript 6 入门》——Iterator(遍历器)的概念

iterator

  简单来说,ES6 为了统一集合类型数据结构的处理,增加了 iterator 接口,供 for...of 使用,简化了不同结构数据的处理。而 iterator 的遍历过程,则是类似 Generator 的方式,迭代时不断调用next方法,返回一个包含value(值)和done属性(标识是否遍历结束)的对象。

如何实现Symbol.iterator方法,使普通对象可被 for of 迭代

  依据上文的指引,我们先看看数组的Symbol.iterator接口:

const arr = [1,2,3];
const iterator = arr[Symbol.iterator]();
console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: 3, done: false}
console.log(iterator.next()); // {value: undefined, done: true}

  我们可以尝试给普通对象实现一个Symbol.iterator接口:

// 普通对象
const obj = {
  foo: 'value1',
  bar: 'value2',
  [Symbol.iterator]() {
    // 这里Object.keys不会获取到Symbol.iterator属性,原因见下文
    const keys = Object.keys(obj); 
    let index = 0;
    return {
      next: () => {
        if (index < keys.length) {
          // 迭代结果 未结束
          return {
            value: this[keys[index++]],
            done: false
          };
        } else {
          // 迭代结果 结束
          return { value: undefined, done: true };
        }
      }
    };
  }
}
for (const value of obj) {
  console.log(value); // value1 value2
};

  上面给obj实现了Symbol.iterator接口后,我们甚至还可以像下面这样把对象转换成数组:

console.log([...obj]); // ["value1", "value2"]
console.log([...{}]); // console.log is not iterable (cannot read property Symbol(Symbol.iterator))

  我们给obj对象实现了一个Symbol.iterator接口,在此,有一点需要说明的是,不用担心[Symbol.iterator]属性会被Object.keys()获取到导致遍历结果出错,因为Symbol.iterator这样的Symbol属性,需要通过Object.getOwnPropertySymbols(obj)才能获取,Object.getOwnPropertySymbols() 方法返回一个给定对象自身的所有 Symbol 属性的数组。

  有一些场合会默认调用 Iterator 接口(即Symbol.iterator方法:

  • 扩展运算符...:这提供了一种简便机制,可以将任何部署了 Iterator 接口的数据结构,转为数组。也就是说,只要某个数据结构部署了 Iterator 接口,就可以对它使用扩展运算符,将其转为数组(毫不意外的,代码[...{}]会报错,而[...'123']会输出数组['1','2','3'])。
  • 数组和可迭代对象的解构赋值(解构是ES6提供的语法糖,其实内在是针对可迭代对象Iterator接口,通过遍历器按顺序获取对应的值进行赋值。而普通对象解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。);
  • yield*_yield*后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口;
  • 由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用;
  • 字符串是一个类似数组的对象,也原生具有Iterator接口,所以也可被for of迭代。

迭代器模式

  迭代器模式提供了一种方法顺序访问一个聚合对象中的各个元素,而又无需暴露该对象的内部实现,这样既可以做到不暴露集合的内部结构,又可让外部代码透明地访问集合内部的数据。迭代器模式为遍历不同的集合结构提供了一个统一的接口,从而支持同样的算法在不同的集合结构上进行操作。

  不难发现,Symbol.iterator实现的就是一种迭代器模式。集合对象内部实现了Symbol.iterator接口,供外部调用,而我们无需过多的关注集合对象内部的结构,需要处理集合对象内部的数据时,我们通过for of调用Symbol.iterator接口即可。

  比如针对前文普通对象的Symbol.iterator接口实现一节的代码,如果我们对obj里面的数据结构进行了如下调整,那么,我们只需对应的修改供外部迭代使用的Symbol.iterator接口,即可不影响外部迭代调用:

const obj = {
  // 数据结构调整
  data: ['value1', 'value2'],
  [Symbol.iterator]() {
    let index = 0;
    return {
      next: () => {
        if (index < this.data.length) {
          // 迭代结果 未结束
          return {
            value: this.data[index++],
            done: false
          };
        } else {
          // 迭代结果 结束
          return { value: undefined, done: true };
        }
      }
    };
  }
}
// 外部调用
for (const value of obj) {
  console.log(value); // value1 value2
}

  实际使用时,我们可以把上面的Symbol.iterator提出来进行单独封装,这样就可以对一类数据结构进行迭代操作了。当然,下面的代码只是最简单的示例,你可以在此基础上探究更多实用的技巧。

const obj1 = {
  data: ['value1', 'value2']
}
const obj2 = {
  data: [1, 2]
}
// 遍历方法
consoleEachData = (obj) => {
  obj[Symbol.iterator] = () => {
    let index = 0;
    return {
      next: () => {
        if (index < obj.data.length) {
          return {
            value: obj.data[index++],
            done: false
          };
        } else {
          return { value: undefined, done: true };
        }
      }
    };
  }
  for (const value of obj) {
    console.log(value);
  }
}
consoleEachData(obj1); // value1 value2
consoleEachData(obj2); // 1  2

一点补充

  在写这篇文章时,有个问题给我带来了困扰:原生object对象默认没有部署Iterator接口,即object不是一个可迭代对象。对象的扩展运算符...等同于使用Object.assign()方法,这个比较好理解。那么,原生object对象的解构赋值又是怎样一种机制呢?

let aClone = { ...a };
// 等同于
let aClone = Object.assign({}, a);

  有一种说法是:ES6提供了Map数据结构,实际上原生object对象被解构时,会被当作Map进行解构。关于这点,大家有什么不同的观点吗?欢迎评论区一起探讨。

同时,ECMAScript后面又引入了异步迭代器for await...of 语句,该语句创建一个循环,该循环遍历异步可迭代对象以及同步可迭代对象,详情可查看MDN:for-await...of

参考资料

本文首发于个人博客,欢迎指正和star

查看原文

赞 23 收藏 12 评论 3

胡小喵 赞了文章 · 2020-11-23

前端装逼技巧 108 式(一)—— 打工人

你在拼多多到处找人砍价,他在滴滴打车求人助力,我在电子厂拧螺丝拧到凌晨,我们都有光明的未来!早安,打工人!

系列文章汇总:

楔子

  作为一名拥有钢铁般意志的前端打工人,装逼是不可能的,这辈子都不可能装逼。如果真要装逼,那就大家一起装逼,毕竟前端要讲武德嘛,要耗子尾汁。遂决定写下前端装逼技巧108式,供诸君茶余饭后一乐,时不时秀个骚操作,为打工的生活增添一抹亮色。

  因作为打工人,时间、精力有限,目前大纲只有约50式,还请诸君有好的装逼要点私信或者在评论区留言,也可在我的博客页面扫码添加微信,大家共同探讨装逼大计、共同迎接打工人的光明未来!

系列文章力求通过一行代码或者一个小的切入点去理解一个知识点,文章风格所限,引用资料部分,将在对应小节末尾标出。

第一式:子曰,公欲装逼好,工具少不了

  • 代码太丑陋,carbon来相救:把你的代码转换为精美图片进行分享(点击图片跳转)

carbon

  本文为便于代码复制,将奉行不首先装逼原则,尽量减少此装逼利器的使用。

第二式:console调试万金油,学会开车更上头

  console.log()在前端调试中的地位自不必赘述,其实一代车神也对其五体投地,不信诸君细看(如真有不解其意者,建议发扬不耻下问的求知精神,问问你旁边的同事):

image

  是的,以上图片是由console.log()完成的,我没有骗你,贴出代码以证清白,为便于诸君控制台开车,此处我们忘掉第一式:

// 在此提醒,为免于生成丑陋的锯齿背景图片,请注意空格的个数,并保证console面板的宽度。
console.log(`%c                                                                            
                                                                            
                                                                            
                               %c FBI WARNING %c                                
                                                                            
                                                                            
%c        Federal Law provides severe civil and criminal penalties for        
        the unauthorized reproduction,distribution, or exhibition of        
         copyrighted motion pictures (Title 17, United States Code,         
        Sections 501 and 508). The Federal Bureau of Investigation          
         investigates allegations of criminal copyright infringement        
                 (Title 17, United States Code, Section 506).               
                                                                            
                                                                            
                                                                            
`,
'background: #000; font-size: 18px; font-family: monospace',
'background: #f33; font-size: 18px; font-family: monospace; color: #eee; text-shadow:0 0 1px #fff',
'background: #000; font-size: 18px; font-family: monospace',
'background: #000; font-size: 18px; font-family: monospace; color: #ddd; text-shadow:0 0 2px #fff'
)

  为什么会这样呢?想必你还记得其他语言中的print()。占位符是print()的专属吗?不,他们在console.log()中同样适用:

  • %s:字符串
  • %d:整数
  • %i:整数
  • %f:浮点数
  • %o:obj对象(DOM)
  • %O:obj对象
  • %c:CSS样式

  console.log()可以通过以上这些特有的占位符进行信息的加工输出。是的,你可能已经明白,上面代码的玄机就在四个%c,第一个创建神秘而性感的纯黑背景;第二个给“FBI WARNING”加上红色的背景;第三个恢复纯黑的性感;第四个配上白色的文字,如此,大事已成。

  明白了以上原理,诸君就可以自由发挥,展示你们强大的css实力了,甚至还可以输出gif背景图,在装逼的路上更上几层楼。不装了,我是css渣渣。

console.log(
  '%c孤篷',
  'text-shadow: 0 1px 0 #ccc,0 2px 0 #c9c9c9,0 3px 0 #bbb,0 4px 0 #b9b9b9,0 5px 0 #aaa,0 6px 1px rgba(0,0,0,.1),0 0 5px rgba(0,0,0,.1),0 1px 3px rgba(0,0,0,.3),0 3px 5px rgba(0,0,0,.2),0 5px 10px rgba(0,0,0,.25),0 10px 10px rgba(0,0,0,.2),0 20px 20px rgba(0,0,0,.15);font-size:5em'
  )

孤蓬

  那么,我们是否可以超越度娘,在自家公司官网控制台完成精美的招聘文案投送呢?更高级一些的console.log插图用法可以参考这里

  拓展:console对象都有哪些方法?

console

参考资料:小蝌蚪日记:通过console.log高仿FBI Warning | Using the F12 Tools Console to View Errors and Status

第三式:芙蓉面,杨柳腰,无物比妖娆 —— 让你看清UI的轮廓

  • UI轮廓哪里寻,outline属性来帮您。

    html * {
       outline: 1px solid red
    }

    UCloud

  • 解析与思考

    • 这里没有使用 border 的原因是 border 会增加元素的大小但是 outline 不会;
    • 通过这个技巧不仅能帮助我们在开发中迅速了解元素所在的位置,还能帮助我们方便地查看任意网站的布局;
    • 所有浏览器都支持 outline 属性;outline (轮廓)是绘制于元素周围的一条线,位于边框边缘的外围,可起到突出元素的作用;
    • 轮廓线不会占据空间,也不一定是矩形(比如2D转换等)。
    • 去掉Chrome浏览器中输入框以及其它表单控件获得焦点时的带颜色边框

      input {
        outline: none;
      }
  • 通过一个开关实现任意网页开启关闭outline

    • Chrome右上角三个点⇒书签⇒书签管理器⇒右上角三个点⇒「添加新书签」;
    • 名称随意,粘贴以下代码到网址中;
    • 然后我们就可以在任意网站上点击刚才创建的书签,内部会判断是否存在调试的 style。存在的话就删除,不存在的话就添加,通过这种方式我们就能很方便的通过这个技巧查看任意网页的布局了。

         javascript: (function() {
            var elements = document.body.getElementsByTagName('*');
            var items = [];
            for (var i = 0; i < elements.length; i++) {
               if (elements[i].innerHTML.indexOf('html * { outline: 1px solid red }') != -1) {
                  items.push(elements[i]);
               }
            }
            if (items.length > 0) {
               for (var i = 0; i < items.length; i++) {
                  items[i].innerHTML = '';
               }
            } else {
               document.body.innerHTML +=
                  '<style>html * { outline: 1px solid red }</style>';
            }
       })();
参考资料:很好用的 UI 调试技巧

第四式:角声寒,夜阑珊,又改需求。难,难,难!—— 类型转换助你不带脏字的骂产品、优雅的夸自己

  • (!(~+[])+{})[--[~+""][+[]]*[~+[]]+~~!+[]]+({}+[])[[~!+[]*~+[]]]:sb
  • ([][[]]+[])[+!![]]+([]+{})[!+[]+!![]]:nb
  • (+!![]*([]+{})+[]+{})[+[]]+([]+{})[!+[]+!![]]:Nb

图解:取类型转换得到的字符串里的字母进行拼凑(看懂了原理,其实我们完全可以尝试写的更简练一些)

nb

插件:zhuangbility,一个可以逆向操作,输入文字,返回操作符的npm插件

第五式:a == 1 && a == 2 && a == 3,那你可以实现a === 1 && a === 2 && a === 3吗?

  • a == 1 && a == 2 && a == 3

      // 当然,你也可以把count作为属性放在a对象上
      let count = 1
      let a = {
        valueOf: function(){return count++}
      }
      console.log(a==1 && a==2 && a==3) // true
    • 对象在转换基本类型时,会调用该对象上 valueOftoString 这两个方法,该方法的返回值是转换为基本类型的结果
    • 具体调用以上哪个方法取决于内置的 toPrimitive 调用结果
  • a === 1 && a === 2 && a === 3
let count = 1;
Object.defineProperty(window, 'a', {
    get: function() {
        return count ++;
    }
});

console.log(a===1 && a===2 && a===3) // true
Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。同时,该API也是Vue 2.x数据绑定实现的核心,Vue 在 3.x 版本之后改用 Proxy 进行实现,本系列文章后续会进行简单讨论。

原理可参考:译:在JS中,如何让(a===1 && a===2 && a === 3)(严格相等)的值为true? | 深入浅出Object.defineProperty() | ECMAScript7规范中的ToPrimitive抽象操作

第六式:最近有点儿火的 Web Components 可能并不是小鲜肉

Web Components原理:

  • html很宽松,浏览器也可以识别不规则、不合法标签(元素)(如<custom-label>Web Components</custom-label> 会展示"Web Components"。);
  • 自定义继承自HTMLElement的类,称为自定义元素的类;
  • 经过window.customElements.defineAPI定义和注册自定义元素,使得不合法标签(自定义元素)与自定义元素的类关联,实现合法化
  • 通过模板标签<template>简化类的定义过程并添加样式;
  • 通过自定义元素的attachShadow()方法开启 Shadow DOM(这部分 DOM 默认与外部 DOM 隔离,内部任何代码都无法影响外部),隐藏自定义元素的内部实现;
  • 添加事件监听、进行组件化封装等。

Web Components的好处:

  • 可以通过 shadow DOM 创建子 DOM 树,不会被页面上的 CSS 样式和 javascript 脚本所影响;
  • 便于复用/重用;
  • 相比于Vue、React、Angular等的组件化,Web Components是原生的、框架无关的。
参考资料:Web Components 入门实例教程-阮一峰 | Window.customElements | Web Components

第七式:Windows环境变量设置其实可以很简单

  使用Windows系统电脑进行开发的小伙伴也许经常会碰到需要手动设置环境变量的情况,其实设置环境变量也可以很简单的通过命令行完成:


# 查看当前所有可用的环境变量
set
# 查看某个环境变量:查看path变量的值
set path
# 修改环境变量(注意:这里是覆盖)
set 变量名=变量内容
# 设置为空
set 变量名=
# 给变量追加内容(%变量名%;代表以前的值)
set 变量名=%变量名%;变量内容
# 将C:\Go\bin\添加到path中
set path=%path%;C:\Go\bin\
参考资料:Windows使用cmd命令行查看、修改、删除与添加环境变量

第八式:1.toFixed()1.0.toFixed()1..toFixed(),究竟哪个写法是对的?

  在数字字面量中,1.xxxxx这样的语法是浮点数表示法。所以1.toFixed()这样的语法在 JavaScript 中会报错,这个错误来自于浮点数的字面量解析过程,而不是“.作为存取运算符”的处理过程。在 JavaScript 中,浮点数的小位数是可以为空的,因此“1.”和“1.0”将作为相同的浮点数被解析出来。所以会出现:

1. === 1; // true
1. === 1.0; // true
1 === 1.0; // true
1.; // 1
1.0; // 1

  既然“1.”表示的是浮点数,那么“1..toFixed”表示的就是该浮点数字面量的“.toFixed”属性。当是数字字面量时,可通过类似Number(1).toFixed() 创建基本包装类型(显示装箱),然后就可以进行属性和方法的添加、读取(或者可借助小括号把字面量括起来,告诉浏览器引擎这是一个整体)。

  • 装箱:将基本数据类型转换为对应的引用类型的操作(装箱又分为隐式装箱和显式装箱);
  • 拆箱:把引用类型转换成基本数据类型。

  基本类型不能有属性和方法,当给它们添加属性的时候系统会自动进行包装类并销毁:

var num = 1;
num.len = 2;
//上一行代码会产生如下过程:
// new Number(1).len =2; 
// delete len;
// 也就是会先添加len属性,当前语句执行结束后即销毁,所以下一行打印num还是1,没有len属性。
console.log(num, num.len);//1 undefined
var num = new Number(1);
num.len = 2;
console.log(num); // Number {1, len: 2}
参考拓展:谈谈JavaScript中装箱和拆箱

第九式:typeof不靠谱,我们又该如何判断类型?

  • typeof之殇:我们应该都知道,使用 typeof 可以准确判断除 null 以外的基本类型,以及 functionsymbol 类型;null 会被 typeof 判断为 object

    • 在 JavaScript 最初的实现中,JavaScript 中的值是由一个表示类型的标签和实际数据值表示的。对象的类型标签是 0。由于 null 代表的是空指针(大多数平台下值为 0x00),因此,null 的类型标签是 0,typeof null 也因此返回 "object";
    • 在 ES 6 之前,typeof 总能保证对任何所给的操作数返回一个字符串。即便是没有声明的标识符,typeof 也能返回 'undefined'。使用 typeof 永远不会抛出错误。但在加入了块级作用域的 let 和 const 之后,在其被声明之前对块中的 let 和 const 变量使用 typeof 会抛出一个 ReferenceError。块作用域变量在块的头部处于“暂存死区”,直至其被初始化,在这期间,访问变量将会引发错误。
  • 以前经常拿来判断数组的instanceof是怎么实现的:使用 a instanceof B 判断的是a 是否为 B 的实例,即 a 的原型链上是否存在 B 构造函数(ES6之后可以通过Array.isArray()来判断是否是数组)。

      // L 表示左表达式,R 表示右表达式
      const customInstanceof = (L, R) => {
        if (typeof L !== 'object') return false
        while (true) { 
          // 已经遍历到了最顶端
          if (L === null) return false
          // 利用原型链进行判断
          if (R.prototype === L.__proto__) return true
          L = L.__proto__
        } 
      };
      customInstanceof([], Array) // true
  • constructor为什么不是我们的选择?

    • constructor属性是可以被修改的,会导致检测出的结果不正确;
    • 除了undefinednull,其他类型的变量均能使用constructor判断出类型。

        let bool=true;
        bool.constructor==Boolean  //true
        let num1=1;
        num1.constructor==Number  //true
        let num2=new Number();
        num2.constructor==Number   //true
        // constructor属性是可以被修改的
        num2.constructor = Object
        num2.constructor==Number   //false
        let str='hello world';
        str.constructor==String     //true
  • Object.prototype.toString竟如此万能?

      Object.prototype.toString.call(123)
      //"[object Number]"
      Object.prototype.toString.call('str')
      //"[object String]"
      Object.prototype.toString.call(true)
      //"[object Boolean]"
      Object.prototype.toString.call({})
      //"[object Object]"
      Object.prototype.toString.call([])
      //"[object Array]"
      // 定义getType方法,用来判断类型
      getType = (obj) => {
        return Object.prototype.toString.call(obj).slice(8, -1)
      }
      getType(12n) // BigInt
      getType(Symbol()) // Symbol
      getType(() => {}) // Function
      getType() // Undefined
      getType(null) // Null
      getType(NaN) // Number
资料参考:typeof | The history of “typeof null”

第十式:十进制二进制互转,真的不用那么麻烦

  • 使用NumberObject.toString(radix)十进制转二进制:

    // 如有补齐位数的需求,可通过判断返回值的长度在前面加0
    let num = 10;
    console.log(num.toString(2)); // 1010
  • 使用parseInt(string, radix);二进制转十进制:

    let num = 1010101;
    console.log(parseInt(num,2)); // 85
  • Tips:由于以上代码都使用let定义了num变量,除了刷新页面外,该如何在控制台分别执行呢?只需把代码放在一对花括号之间即可(块级作用域)。

第十一式:没有加减乘除,如何比较正整数字符串的大小?

  在接手的部分项目中,存在需要前端拼接Elasticsearch查询语句的情况,好不容易会了点Elasticsearch,却发现问题并没有那么简单:金额数量区间查询你告诉我存储的是字符串?那岂不是会出现1<3000<5的情况?天啦噜,不要逗我好吗?

  那么,在不改动ES的情况下,如何通过正则表达式查询来实现正整数字符串大小的比较呢?直接说思路:数位更多或者从高位开始比,数值更大即是更大的数【一时间没想到更好的解法,有更好的解法欢迎留言或者私信】。

// 通过正则表达式从字符串数组中筛选出大于某个数值的字符串类型数据
const filterStrNumberByRegExp = (num, arr) => {
  const strBaseNumber = num.toString();
  const arrBaseNumber = strBaseNumber.split('');
  const len = strBaseNumber.length;
  // 生成正则:数位更多或者从高位开始比,数值更大
  let strRegExp = `\\d{${len+1}}`;
  arrBaseNumber.map((item, index) => {
    // 这里因为有位数限制,'^'和'$'不是必须的,可以去除
    strRegExp += `|${strBaseNumber.substring(index,-1) || '^'}[${+item + 1}-9]\\d{${len - index - 1}}$`
  });
  // 丢给ES进行查询时,貌似不可使用\d(可用[0-9]替代)、开头、结尾匹配等字符,上面四行可用下面注释内容替换
  //let strRegExp = `[0-9]{${len+1}}`;
  //arrBaseNumber.map((item, index) => {
  //  strRegExp += `|${strBaseNumber.substring(index,-1) || ''}[${+item + 1}-9][0-9]{${len - index - 1}}`
  //});
  const regExp = new RegExp(strRegExp);
  // 丢给ES进行正则查询时使用strRegExp结果
  console.log(regExp, strRegExp);
  return arr.filter(item => {
    // 小于等于判断的话,这里取反或者自行修改正则
    if(regExp.test(item)) return true;
  });
};
filterStrNumberByRegExp(386, ['12', '334', '556', '1122', '5546','234','388','387','1234','386','385']); // ["556", "1122", "5546", "388", "387", "1234"]

  详细Elasticsearch列表页搜索公共方法实现可以查看我的这篇笔记。

第十二式:前端太寂寞?如何让页面和你说话? —— TTS(Text To Speah)

  在项目中需要对ajax请求返回的消息进行语音播报,str 为需要播报的信息(适应于错误信息语音提示等场景):

//语音播报
function voiceAnnouncements(str){
    // 百度语音合成:或者使用新版地址https://tsn.baidu.com/text2audio
    var url = "http://tts.baidu.com/text2audio?lan=zh&ie=UTF-8&spd=5&text=" + encodeURI(str);
    var n = new Audio(url);
    n.src = url;
    n.play();
};
voiceAnnouncements(`
秋名山上路人稀,常有车手较高低;
如今车道依旧在,不见当年老司机。
司机车技今尚好,前端群里不寂寥;
向天再借五百年,誓言各行领风骚。
`);
// 尝试了一些换女声的方式,但是,都失败了。。。
voiceAnnouncements(`
哇,代码写的真棒,你可真秀哇!
`);
  • 参数解释:

    • lan:固定值zh。语言选择,目前只有中英文混合模式,填写固定值zh
    • ie:编码方式
    • spd:语速,取值0-9,默认为5中语速
    • text:合成的文本,使用UTF-8编码。小于512个中文字或者英文数字。(文本在百度服务器内转换为GBK后,长度必须小于1024字节)
  • React Native Text-To-Speech library for Android and iOS
  • 用语音控制自己的网站 annyang:A tiny JavaScript Speech Recognition library that lets your users control your site with voice commands.annyang has no dependencies, weighs just 2 KB, and is free to use and modify under the MIT license。

第十三式:失焦事件与点击事件冲突怎么办?

  • 场景:

    • 下拉框中blur与click冲突;
    • 输入框blur与下方可点击浮沉click冲突:输入值时下方出现浮层,输入框失去焦点时,浮层隐藏;点击浮层条目触发搜索并隐藏浮层;
    • 问题:点击浮层时,由于失焦事件先触发,浮层隐藏逻辑执行,导致浮层的onClick事件逻辑无法执行

    失焦事件与点击事件冲突

// 点击弹窗条目进行搜索
handleSearch = (activeSearch) => {
  console.log(activeSearch);
  this.setState({ visible: false });
}

// 获得焦点,有值时展示弹窗
onFocus = () => {
  if (this.state.keyword) {
    this.setState({ visible: true });
  }
}

// 输入且有值时展示弹窗
onChange = (e) => {
  this.setState({
    keyword: e.target.value,
    visible: !!e.target.value
  })
}

// 失去焦点隐藏弹窗
onBlur = () => {
  if (this.state.keyword) {
    this.setState({ visible: false });
  }
}

render() {
  const { keyword, visible } = this.state;
  return (
    <div>
      <Input
        allowClear
        addonBefore={<Icon type="user" />}
        placeholder="支持ID、名称、主邮箱、客户经理、专属账户、客户ID、GroupID搜索"
        style={ { width: 460 } }
        onFocus={this.onFocus}
        onChange={this.onChange}
        onBlur={this.onBlur}
      />
      {
        // 展示弹窗(点击条目完成搜索)
        visible && keyword && <div className={styles.SearchSelect}>
          {
            showOptions.map(item => (
              <div
                onClick={() => this.handleSearch(item)}
                className={styles.item}
                key={item.key}
              >
                <div>
                  {item.label}:{keyword}
                </div>
              </div>
            ))
          }
        </div>
      }
    </div>
  );
}
  • 解决:

    • 方法一:给失焦事件设置延迟触发

        onBlur = () => {
          if (this.state.keyword) {
            setTimeout(() => {
              this.setState({ visible: false });
            }, 300);
          }
        }
    • 方法二:使用onMouseDown替代onClick

      • mousedown事件:当鼠标指针移动到元素上方,并按下鼠标按键时,会发生mousedown事件,所以它会先于失焦事件执行。
      • mouseup事件:当在元素上放松鼠标按钮时,会发生mouseup事件。

第十四式:不用加减乘除如何做加法——位运算让你的代码更高效

  • JavaScript 位运算符

    JavaScript 位运算符

  位运算是基于二进制的,如何快速获得二进制可参考第十式。

  • 不用加减乘除做加法

    function add(a,b) {
        let sum;
        let add1;
        while(b!=0) {
            // 异或
            sum = a^b;
            // 与 左移
            add1 = (a&b)<<1;
            a = sum;
            b = add1;
        }
        return a
    };
    add(1,2); // 3
  • JS按位运算符的妙用:

    • 使用&运算符判断一个数的奇偶(只需记住0和1与1进行&运算的结果即可):

      • 偶数 & 1 = 0
      • 奇数 & 1 = 1
    • 使用~~,>>,<<,>>>,|来取整:

      • ~~Math.PI:3(按位取反再取反)
      • Math.PI>>0Math.PI<<0Math.PI>>>0:3(按位左移或者右移0位,>>>不可用于负数)
      • Math.PI|0:3,按位异或
    • 使用<<,>>来计算乘除:

      • 整数左移n位相当于乘2的n次方;
      • 右移相当于除以2的n次方,再向下取整
    • 利用^来完成比较两个数是否相等:!(a ^ b)
    • 使用^来完成值交换:参考第十五式
    • 使用&,>>,|来完成rgb值和16进制颜色值之间的转换

      • 16进制颜色值转RGB:

        function hexToRGB(hex){
          hex = hex.replace("#","0x");
          let r = hex >> 16;
          let g = hex >> 8 & 0xff;
          let b = hex & 0xff;
          return "rgb("+r+","+g+","+b+")";
        };
        hexToRGB('#cccccc'); // rgb(204,204,204)
      • RGB转16进制颜色值:

        function RGBToHex(rgb){
          let rgbArr = rgb.split(/[^\d]+/);
          let color = rgbArr[1]<<16 | rgbArr[2]<<8 | rgbArr[3];
          return "#"+color.toString(16);
        };
        RGBToHex('rgb(204,204,204)'); // #cccccc
参考资料:JavaScript 位运算符

第十五式:无聊的脑筋急转弯,不借助第三个变量交换a,b两个变量值的N种方法

  • 方法一:加减

      a = a + b;
      b = a - b;
      a= a - b;
  • 方法二:位运算

      a ^= b;
      b ^= a;
      a ^= b;
  • 方法三:对象或者数组

      a = {a, b};
      b = a.a;
      a = a.b;
      // a = [a, b];
      // b = a[0];
      // a = a[1];
  • 方法四:ES 6 解构赋值

      [a, b] = [b, a]
  • 方案五:运算符优先级

      a = [b, b=a][0];
    参考资料: 不借助第三个变量交换a,b两个变量值

第十六式:如何在浏览器当前页面打开并操作另一个tab页

  if (window.customeWindow) {
    window.customeWindow.close()
  }
  window.customeWindow = window.open()
  window.customeWindow.document.write('<p style="color:red">写点什么呢?<p>')
  window.customeWindow.document.write('<p style="color:#cccccc">想写什么就写什么。<p>')
  window.customeWindow.document.write('再追加点别的。')
  window.customeWindow.document.close() // 连续追加输入结束
  window.customeWindow.document.write('哈哈,现在页面上就只有我了!')
  window.customeWindow.document.write('<p style="color:red">不,还有我!<p>')
参考资料:BRAFT EDITOR富文本编辑器预览

第十七式:产品说要按照中文拼音顺序排序?

  • 使用 stringObject.localeCompare(target) 方法实现中文按照拼音顺序排序
var array = ["上海", "北京", "杭州", "广东", "深圳", "西安"];
// localeCompare() 方法返回一个数字来指示一个参考字符串是否在排序顺序前面或之后或与给定字符串相同。
array = array.sort((item1, item2) => item1.localeCompare(item2));
// ["北京", "广东", "杭州", "上海", "深圳", "西安"]
参考资料:String.prototype.localeCompare()
  • 一个对象数组按照另一个数组排序
sortFunc = (propName, referArr) => (prev, next) => 
referArr.indexOf(prev[propName]) - referArr.indexOf(next[propName])
// 按照年龄age的顺序给person排序
const age = [33, 11, 55, 22, 66];
const person = [
 {age: 55, weight: 50},
 {age: 22, weight: 42},
 {age: 11, weight: 15},
 {age: 66, weight: 56},
 {age: 33, weight: 68}]
person.sort(sortFunc('age', age));
// 结果:
// [
//  {"age": 33,"weight": 68},
//  {"age": 11,"weight": 15},
//  {"age": 55,"weight": 50},
//  {"age": 22,"weight": 42},
//  {"age": 66,"weight": 56}
// ]

第十八式:这段代码为什么会报错,说好的分号可以省略呢?

  console.log(123)
  [12,2].filter(item => item > 3)
  // Uncaught TypeError: Cannot read property '2' of undefined
  // at <anonymous>:2:1
  • 分号推断:编译原理里的分号推断,作用是在编程的时候,让程序员省略掉不必要的分号;
  • JavaScript有着自动分号插入的机制(Automatic Semicolon Insertion),简称ASI(ASI 只是表示编译器正确理解了程序员的意图,并没有真的插入分号);
  • 浏览器引擎的 Parser(负责将JS 源码转换为 AST)总是优先将换行符前后的符号流当作一条语句解析(带换行的多行注释与换行符是等效的);
  • 所以在 Parser 眼里,以上代码是这样的:

    • console.log(123)[12,2].filter(item => item > 3)console.log(123)没有返回值,既undefined
    • [12,2]中的方括号被视为读取console.log(123)返回值中的属性2,类似于根据下标取数组中的元素;
    • 为什么是取属性2呢,因为12,2是个逗号表达式,表达式的值是最右边的“2”,如此以来,上面的报错信息就很好理解了。
  • 不能省略的分号:

    • for 循环头部的分号
    • 作为空语句存在的分号
    • [、(、+、-、和/五个字符开头的语句之前的分号
    资料参考:备胎的自我修养——趣谈 JavaScript 中的 ASI (Automatic Semicolon Insertion)

本文首发于个人博客,欢迎指正和star

查看原文

赞 158 收藏 108 评论 23

胡小喵 赞了文章 · 2020-10-12

浏览器是如何工作的:Chrome V8让你更懂JavaScript

Chrome V8
  V8 是由 Google 开发的开源 JavaScript 引擎,也被称为虚拟机,模拟实际计算机各种功能来实现代码的编译和执行

上图清晰版

记得那年花下,深夜,初识谢娘时

为什么需要 JavaScript 引擎

  我们写的 JavaScript 代码直接交给浏览器或者 Node 执行时,底层的 CPU 是不认识的,也没法执行。CPU 只认识自己的指令集,指令集对应的是汇编代码。写汇编代码是一件很痛苦的事情。并且不同类型的 CPU 的指令集是不一样的,那就意味着需要给每一种 CPU 重写汇编代码
  JavaScirpt 引擎可以将 JS 代码编译为不同 CPU(Intel, ARM 以及 MIPS 等)对应的汇编代码,这样我们就不需要去翻阅每个 CPU 的指令集手册来编写汇编代码了。当然,JavaScript 引擎的工作也不只是编译代码,它还要负责执行代码、分配内存以及垃圾回收

# 将一个寄存器中的数据移动到另外一个寄存器中
1000100111011000  #机器指令
mov ax,bx         #汇编指令
资料拓展: 汇编语言入门教程【阮一峰】

热门 JavaScript 引擎

  • V8 (Google),用 C++编写,开放源代码,由 Google 丹麦开发,是 Google Chrome 的一部分,也用于 Node.js。
  • JavaScriptCore (Apple),开放源代码,用于 webkit 型浏览器,如 Safari ,2008 年实现了编译器和字节码解释器,升级为了 SquirrelFish。苹果内部代号为“Nitro”的 JavaScript 引擎也是基于 JavaScriptCore 引擎的。
  • Rhino,由 Mozilla 基金会管理,开放源代码,完全以 Java 编写,用于 HTMLUnit
  • SpiderMonkey (Mozilla),第一款 JavaScript 引擎,早期用于 Netscape Navigator,现时用于 Mozilla Firefox。
  • Chakra (JScript 引擎),用于 Internet Explorer。
  • Chakra (JavaScript 引擎),用于 Microsoft Edge。
  • KJS,KDE 的 ECMAScript/JavaScript 引擎,最初由哈里·波顿开发,用于 KDE 项目的 Konqueror 网页浏览器中。
  • JerryScript — 三星推出的适用于嵌入式设备的小型 JavaScript 引擎。
  • 其他:Nashorn、QuickJSHermes

V8

  Google V8 引擎是用 C ++编写的开源高性能 JavaScript 和 WebAssembly 引擎,它已被用于 Chrome 和 Node.js 等。可以运行在 Windows 7+,macOS 10.12+和使用 x64,IA-32,ARM 或 MIPS 处理器的 Linux 系统上。 V8 最早被开发用以嵌入到 Google 的开源浏览器 Chrome 中,第一个版本随着第一版Chrome于 2008 年 9 月 2 日发布。但是 V8 是一个可以独立运行的模块,完全可以嵌入到任何 C ++应用程序中。著名的 Node.js( 一个异步的服务器框架,可以在服务端使用 JavaScript 写出高效的网络服务器 ) 就是基于 V8 引擎的,Couchbase, MongoDB 也使用了 V8 引擎。

  和其他 JavaScript 引擎一样,V8 会编译 / 执行 JavaScript 代码,管理内存,负责垃圾回收,与宿主语言的交互等。通过暴露宿主对象 ( 变量,函数等 ) 到 JavaScript,JavaScript 可以访问宿主环境中的对象,并在脚本中完成对宿主对象的操作

how-v8-works

资料拓展:v8 logo | V8 (JavaScript engine)) | 《V8、JavaScript+的现在与未来》 | 几张图让你看懂 WebAssembly

V8一词最早见于“V-8 engine”,即V8发动机,一般使用在中高端车辆上。8个气缸分成两组,每组4个,成V型排列。是高层次汽车运动中最常见的发动机结构,尤其在美国,IRL,ChampCar和NASCAR都要求使用V8发动机。

与君初相识,犹如故人归

什么是 D8

  d8 是一个非常有用的调试工具,你可以把它看成是 debug for V8 的缩写。我们可以使用 d8 来查看 V8 在执行 JavaScript 过程中的各种中间数据,比如作用域、AST、字节码、优化的二进制代码、垃圾回收的状态,还可以使用 d8 提供的私有 API 查看一些内部信息

V8源码编译出来的可执行程序名为d8。d8作为V8引擎在命令行中可以使用的交互shell存在。Google官方已经不记得d8这个名字的由来,但是做为"delveloper shell"的缩写,用首字母d和8结合,恰到好处。
还有一种说法是d8最初叫developer shell,因为d后面有8个字符,因此简写为d8,类似于i18n(internationalization)这样的简写。
参考:Using d8

安装 D8

  • 方法一:自行下载编译

  • 方法二:使用编译好的 d8 工具

    // 解压文件,点击d8打开(mac安全策略限制的话,按住control,再点击,弹出菜单中选择打开)
      V8 version 8.4.109
      d8> 1 + 2
        3
      d8> 2 + '4'
        "24"
      d8> console.log(23)
        23
        undefined
      d8> var a = 1
        undefined
      d8> a + 2
        3
      d8> this
        [object global]
      d8>

    本文后续用于 demo 演示时的文件目录结构:

     V8:
        # d8可执行文件
        d8
        icudtl.dat
        libc++.dylib
        libchrome_zlib.dylib
        libicui18n.dylib
        libicuuc.dylib
        libv8.dylib
        libv8_debug_helper.dylib
        libv8_for_testing.dylib
        libv8_libbase.dylib
        libv8_libplatform.dylib
        obj
        snapshot_blob.bin
        v8_build_config.json
        # 新建的js示例文件
        test.js
    • 方法三:mac

        # 如果已有HomeBrew,忽略第一条命令
        ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
        brew install v8
    • 方法四:使用 node 代替,比如可以用node --print-bytecode ./test.js,打印出 Ignition(解释器)生成的 Bytecode(字节码)。

都有哪些 d8 命令可供使用?

  • 查看 d8 命令

      # 如果不想使用./d8这种方式进行调试,可将d8加入环境变量,之后就可以直接`d8 --help`了
      ./d8 --help
  • 过滤特定的命令

      # 如果是 Windows 系统,可能缺少 grep 程序,请自行下载安装并添加环境变量
      ./d8 --help |grep print

    如:

    • print-bytecode 查看生成的字节码
    • print-opt-code 查看优化后的代码
    • print-ast 查看中间生成的 AST
    • print-scopes 查看中间生成的作用域
    • trace-gc 查看这段代码的内存回收状态
    • trace-opt 查看哪些代码被优化了
    • trace-deopt 查看哪些代码被反优化了
    • turbofan-stats 打印优化编译器的一些统计数据

使用 d8 进行调试

// test.js
function sum(a) {
  var b = 6;
  return a + 6;
}
console.log(sum(3));
  # d8 后面跟上文件名和要执行的命令,如执行下面这行命令,就会打印出 test.js 文件所生成的字节码。
  ./d8 ./test.js --print-bytecode
  # 执行以下命令,输出9
  ./d8 ./test.js

内部方法

  你还可以使用 V8 所提供的一些内部方法,只需要在启动 V8 时传入 --allow-natives-syntax 命令,你就可以在 test.js 中使用诸如HasFastProperties(检查一个对象是否拥有快属性)的内部方法(索引属性、常规属性、快属性等下文会介绍)。

function Foo(property_num, element_num) {
  //添加可索引属性
  for (let i = 0; i < element_num; i++) {
    this[i] = `element${i}`;
  }
  //添加常规属性
  for (let i = 0; i < property_num; i++) {
    let ppt = `property${i}`;
    this[ppt] = ppt;
  }
}
var bar = new Foo(10, 10);
// 检查一个对象是否拥有快属性
console.log(%HasFastProperties(bar));
delete bar.property2;
console.log(%HasFastProperties(bar));
  ./d8 --allow-natives-syntax ./test.js
  # 依次打印:true false

心似双丝网,中有千千结

V8 引擎的内部结构

  V8 是一个非常复杂的项目,有超过 100 万行 C++代码。它由许多子模块构成,其中这 4 个模块是最重要的:

  • Parser:负责将 JavaScript 源码转换为 Abstract Syntax Tree (AST)

    确切的说,在“Parser”将 JavaScript 源码转换为 AST前,还有一个叫”Scanner“的过程,具体流程如下:
    Scanner
  • Ignition:interpreter,即解释器,负责将 AST 转换为 Bytecode,解释执行 Bytecode;同时收集 TurboFan 优化编译所需的信息,比如函数参数的类型;解释器执行时主要有四个模块,内存中的字节码、寄存器、栈、堆。

    通常有两种类型的解释器,基于栈 (Stack-based)和基于寄存器 (Register-based),基于栈的解释器使用栈来保存函数参数、中间运算结果、变量等;基于寄存器的虚拟机则支持寄存器的指令操作,使用寄存器来保存参数、中间计算结果。通常,基于栈的虚拟机也定义了少量的寄存器,基于寄存器的虚拟机也有堆栈,其区别体现在它们提供的指令集体系大多数解释器都是基于栈的,比如 Java 虚拟机,.Net 虚拟机,还有早期的 V8 虚拟机。基于堆栈的虚拟机在处理函数调用、解决递归问题和切换上下文时简单明快。而现在的 V8 虚拟机则采用了基于寄存器的设计,它将一些中间数据保存到寄存器中。
    基于寄存器的解释器架构基于寄存器的解释器架构
    资料参考:解释器是如何解释执行字节码的?
  • TurboFan:compiler,即编译器,利用 Ignition 所收集的类型信息,将 Bytecode 转换为优化的汇编代码;
  • Orinoco:garbage collector,垃圾回收模块,负责将程序不再需要的内存空间回收。

  其中,Parser,Ignition 以及 TurboFan 可以将 JS 源码编译为汇编代码,其流程图如下:
V8流程
  简单地说,Parser 将 JS 源码转换为 AST,然后 Ignition 将 AST 转换为 Bytecode,最后 TurboFan 将 Bytecode 转换为经过优化的 Machine Code(实际上是汇编代码)

  • 如果函数没有被调用,则 V8 不会去编译它。
  • 如果函数只被调用 1 次,则 Ignition 将其编译 Bytecode 就直接解释执行了。TurboFan 不会进行优化编译,因为它需要 Ignition 收集函数执行时的类型信息。这就要求函数至少需要执行 1 次,TurboFan 才有可能进行优化编译。
  • 如果函数被调用多次,则它有可能会被识别为热点函数,且 Ignition 收集的类型信息证明可以进行优化编译的话,这时 TurboFan 则会将 Bytecode 编译为 Optimized Machine Code(已优化的机器码),以提高代码的执行性能。

  图片中的红色虚线是逆向的,也就是说Optimized Machine Code 会被还原为 Bytecode,这个过程叫做 Deoptimization。这是因为 Ignition 收集的信息可能是错误的,比如 add 函数的参数之前是整数,后来又变成了字符串。生成的 Optimized Machine Code 已经假定 add 函数的参数是整数,那当然是错误的,于是需要进行 Deoptimization。

function add(x, y) {
  return x + y;
}

add(3, 5);
add('3', '5');

  在运行 C、C++以及 Java 等程序之前,需要进行编译,不能直接执行源码;但对于 JavaScript 来说,我们可以直接执行源码(比如:node test.js),它是在运行的时候先编译再执行,这种方式被称为即时编译(Just-in-time compilation),简称为 JIT。因此,V8 也属于 JIT 编译器

资料拓展参考:V8 引擎是如何工作的?

V8 是怎么执行一段 JavaScript 代码的

  • V8 出现之前,所有的 JavaScript 虚拟机所采用的都是解释执行的方式,这是 JavaScript 执行速度过慢的一个主要原因。而 V8 率先引入了即时编译(JIT)双轮驱动的设计(混合使用编译器和解释器的技术),这是一种权衡策略,混合编译执行和解释执行这两种手段,给 JavaScript 的执行速度带来了极大的提升。V8 出现之后,各大厂商也都在自己的 JavaScript 虚拟机中引入了 JIT 机制,所以目前市面上 JavaScript 虚拟机都有着类似的架构。另外,V8 也是早于其他虚拟机引入了惰性编译、内联缓存、隐藏类等机制,进一步优化了 JavaScript 代码的编译执行效率
  • V8 执行一段 JavaScript 的流程图:
    V8执行一段JavaScript流程图

    资料拓展:V8 是如何执行一段 JavaScript 代码的?
  • V8 本质上是一个虚拟机,因为计算机只能识别二进制指令,所以要让计算机执行一段高级语言通常有两种手段:

    • 第一种是将高级代码转换为二进制代码,再让计算机去执行;
    • 另外一种方式是在计算机安装一个解释器,并由解释器来解释执行。
  • 解释执行和编译执行都有各自的优缺点,解释执行启动速度快,但是执行时速度慢,而编译执行启动速度慢,但是执行速度快。为了充分地利用解释执行和编译执行的优点,规避其缺点,V8 采用了一种权衡策略,在启动过程中采用了解释执行的策略,但是如果某段代码的执行频率超过一个值,那么 V8 就会采用优化编译器将其编译成执行效率更加高效的机器代码
  • 总结:

    V8 执行一段 JavaScript 代码所经历的主要流程包括:

    • 初始化基础环境;
    • 解析源码生成 AST 和作用域;
    • 依据 AST 和作用域生成字节码;
    • 解释执行字节码;
    • 监听热点代码;
    • 优化热点代码为二进制的机器代码;
    • 反优化生成的二进制机器代码。

一等公民与闭包

一等公民的定义

  • 在编程语言中,一等公民可以作为函数参数,可以作为函数返回值,也可以赋值给变量。
  • 如果某个编程语言的函数,可以和这个语言的数据类型做一样的事情,我们就把这个语言中的函数称为一等公民。例如,字符串在几乎所有编程语言中都是一等公民,字符串可以做为函数参数,字符串可以作为函数返回值,字符串也可以赋值给变量。对于各种编程语言来说,函数就不一定是一等公民了,比如 Java 8 之前的版本。
  • 对于 JavaScript 来说,函数可以赋值给变量,也可以作为函数参数,还可以作为函数返回值,因此 JavaScript 中函数是一等公民

动态作用域与静态作用域

  • 如果一门语言的作用域是静态作用域,那么符号之间的引用关系能够根据程序代码在编译时就确定清楚,在运行时不会变。某个函数是在哪声明的,就具有它所在位置的作用域。它能够访问哪些变量,那么就跟这些变量绑定了,在运行时就一直能访问这些变量。即静态作用域可以由程序代码决定,在编译时就能完全确定。大多数语言都是静态作用域的。
  • 动态作用域(Dynamic Scope)。也就是说,变量引用跟变量声明不是在编译时就绑定死了的。在运行时,它是在运行环境中动态地找一个相同名称的变量。在 macOS 或 Linux 中用的 bash 脚本语言,就是动态作用域的。

闭包的三个基础特性

  • JavaScript 语言允许在函数内部定义新的函数
  • 可以在内部函数中访问父函数中定义的变量
  • 因为 JavaScript 中的函数是一等公民,所以函数可以作为另外一个函数的返回值
// 闭包(静态作用域,一等公民,调用栈的矛盾体)
function foo() {
  var d = 20;
  return function inner(a, b) {
    const c = a + b + d;
    return c;
  };
}
const f = foo();

  关于闭包,可参考我以前的一篇文章,在此不再赘述,在此主要谈下闭包给 Chrome V8 带来的问题及其解决策略。

惰性解析

  所谓惰性解析是指解析器在解析的过程中,如果遇到函数声明,那么会跳过函数内部的代码,并不会为其生成 AST 和字节码,而仅仅生成顶层代码的 AST 和字节码。

  • 在编译 JavaScript 代码的过程中,V8 并不会一次性将所有的 JavaScript 解析为中间代码,这主要是基于以下两点:

    • 首先,如果一次解析和编译所有的 JavaScript 代码,过多的代码会增加编译时间,这会严重影响到首次执行 JavaScript 代码的速度,让用户感觉到卡顿。因为有时候一个页面的 JavaScript 代码很大,如果要将所有的代码一次性解析编译完成,那么会大大增加用户的等待时间;
    • 其次,解析完成的字节码和编译之后的机器代码都会存放在内存中,如果一次性解析和编译所有 JavaScript 代码,那么这些中间代码和机器代码将会一直占用内存
  • 基于以上的原因,所有主流的 JavaScript 虚拟机都实现了惰性解析。
  • 闭包给惰性解析带来的问题:上文的 d 不能随着 foo 函数的执行上下文被销毁掉。

预解析器

  V8 引入预解析器,比如当解析顶层代码的时候,遇到了一个函数,那么预解析器并不会直接跳过该函数,而是对该函数做一次快速的预解析。

  • 判断当前函数是不是存在一些语法上的错误,发现了语法错误,那么就会向 V8 抛出语法错误;
  • 检查函数内部是否引用了外部变量,如果引用了外部的变量,预解析器会将栈中的变量复制到堆中,在下次执行到该函数的时候,直接使用堆中的引用,这样就解决了闭包所带来的问题

V8 内部是如何存储对象的:快属性和慢属性

下面的代码会输出什么:

// test.js
function Foo() {
  this[200] = 'test-200';
  this[1] = 'test-1';
  this[100] = 'test-100';
  this['B'] = 'bar-B';
  this[50] = 'test-50';
  this[9] = 'test-9';
  this[8] = 'test-8';
  this[3] = 'test-3';
  this[5] = 'test-5';
  this['D'] = 'bar-D';
  this['C'] = 'bar-C';
}
var bar = new Foo();

for (key in bar) {
  console.log(`index:${key}  value:${bar[key]}`);
}
//输出:
// index:1  value:test-1
// index:3  value:test-3
// index:5  value:test-5
// index:8  value:test-8
// index:9  value:test-9
// index:50  value:test-50
// index:100  value:test-100
// index:200  value:test-200
// index:B  value:bar-B
// index:D  value:bar-D
// index:C  value:bar-C

  在ECMAScript 规范中定义了数字属性应该按照索引值大小升序排列,字符串属性根据创建时的顺序升序排列。在这里我们把对象中的数字属性称为排序属性,在 V8 中被称为 elements,字符串属性就被称为常规属性,在 V8 中被称为 properties。在 V8 内部,为了有效地提升存储和访问这两种属性的性能,分别使用了两个线性数据结构来分别保存排序属性和常规属性。同时 v8 将部分常规属性直接存储到对象本身,我们把这称为对象内属性 (in-object properties),不过对象内属性的数量是固定的,默认是 10 个。

function Foo(property_num, element_num) {
  //添加可索引属性
  for (let i = 0; i < element_num; i++) {
    this[i] = `element${i}`;
  }
  //添加常规属性
  for (let i = 0; i < property_num; i++) {
    let ppt = `property${i}`;
    this[ppt] = ppt;
  }
}
var bar = new Foo(10, 10);

  可以通过 Chrome 开发者工具的 Memory 标签,捕获查看当前的内存快照。通过增大第一个参数来查看存储变化。(Console面板运行以上代码,打开Memory面板,通过点击Take heap snapshot记录内存快照,点击快照,筛选出Foo进行查看。可参考使用 chrome-devtools Memory 面板了解Memory面板。)

  我们将保存在线性数据结构中的属性称之为“快属性”,因为线性数据结构中只需要通过索引即可以访问到属性,虽然访问线性结构的速度快,但是如果从线性结构中添加或者删除大量的属性时,则执行效率会非常低,这主要因为会产生大量时间和内存开销。因此,如果一个对象的属性过多时,V8 就会采取另外一种存储策略,那就是“慢属性”策略,但慢属性的对象内部会有独立的非线性数据结构 (字典) 作为属性存储容器。所有的属性元信息不再是线性存储的,而是直接保存在属性字典中。

v8 属性存储:
v8属性存储

总结:

  因为 JavaScript 中的对象是由一组组属性和值组成的,所以最简单的方式是使用一个字典来保存属性和值,但是由于字典是非线性结构,所以如果使用字典,读取效率会大大降低。为了提升查找效率,V8 在对象中添加了两个隐藏属性,排序属性和常规属性,element 属性指向了 elements 对象,在 elements 对象中,会按照顺序存放排序属性。properties 属性则指向了 properties 对象,在 properties 对象中,会按照创建时的顺序保存常规属性。

  通过引入这两个属性,加速了 V8 查找属性的速度,为了更加进一步提升查找效率,V8 还实现了内置内属性的策略,当常规属性少于一定数量时,V8 就会将这些常规属性直接写进对象中,这样又节省了一个中间步骤。

  但是如果对象中的属性过多时,或者存在反复添加或者删除属性的操作,那么 V8 就会将线性的存储模式降级为非线性的字典存储模式,这样虽然降低了查找速度,但是却提升了修改对象的属性的速度

资料拓展:快属性和慢属性:V8 是怎样提升对象属性访问速度的?

堆空间和栈空间

栈空间

  • 现代语言都是基于函数的,每个函数在执行过程中,都有自己的生命周期和作用域,当函数执行结束时,其作用域也会被销毁,因此,我们会使用栈这种数据结构来管理函数的调用过程,我们也把管理函数调用过程的栈结构称之为调用栈
  • 栈空间主要是用来管理 JavaScript 函数调用的,栈是内存中连续的一块空间,同时栈结构是“先进后出”的策略。在函数调用过程中,涉及到上下文相关的内容都会存放在栈上,比如原生类型、引用到的对象的地址、函数的执行状态、this 值等都会存在在栈上。当一个函数执行结束,那么该函数的执行上下文便会被销毁掉。
  • 栈空间的最大的特点是空间连续,所以在栈中每个元素的地址都是固定的,因此栈空间的查找效率非常高,但是通常在内存中,很难分配到一块很大的连续空间,因此,V8 对栈空间的大小做了限制,如果函数调用层过深,那么 V8 就有可能抛出栈溢出的错误。
  • 栈的优势和缺点:

    • 栈的结构非常适合函数调用过程。
    • 在栈上分配资源和销毁资源的速度非常快,这主要归结于栈空间是连续的,分配空间和销毁空间只需要移动下指针就可以了。
    • 虽然操作速度非常快,但是栈也是有缺点的,其中最大的缺点也是它的优点所造成的,那就是栈是连续的,所以要想在内存中分配一块连续的大空间是非常难的,因此栈空间是有限的

      // 栈溢出
      function factorial(n) {
        if (n === 1) {
          return 1;
        }
        return n * factorial(n - 1);
      }
      console.log(factorial(50000));

堆空间

  • 堆空间是一种树形的存储结构,用来存储对象类型的离散的数据,JavaScript 中除了原生类型的数据,其他的都是对象类型,诸如函数、数组,在浏览器中还有 window 对象、document 对象等,这些都是存在堆空间的。
  • 宿主在启动 V8 的过程中,会同时创建堆空间和栈空间,再继续往下执行,产生的新数据都会存放在这两个空间中。

继承

  继承就是一个对象可以访问另外一个对象中的属性和方法,在 JavaScript 中,我们通过原型和原型链的方式来实现了继承特性

  JavaScript 的每个对象都包含了一个隐藏属性 __proto__ ,我们就把该隐藏属性 __proto__ 称之为该对象的原型 (prototype),__proto__ 指向了内存中的另外一个对象,我们就把 __proto__ 指向的对象称为该对象的原型对象,那么该对象就可以直接访问其原型对象的方法或者属性。

  JavaScript 中的继承非常简洁,就是每个对象都有一个原型属性,该属性指向了原型对象,查找属性的时候,JavaScript 虚拟机会沿着原型一层一层向上查找,直至找到正确的属性。

隐藏属性__proto__

var animal = {
  type: 'Default',
  color: 'Default',
  getInfo: function () {
    return `Type is: ${this.type},color is ${this.color}.`;
  },
};
var dog = {
  type: 'Dog',
  color: 'Black',
};

利用__proto__实现继承:

dog.__proto__ = animal;
dog.getInfo();

  通常隐藏属性是不能使用 JavaScript 来直接与之交互的。虽然现代浏览器都开了一个口子,让 JavaScript 可以访问隐藏属性 __proto__,但是在实际项目中,我们不应该直接通过 __proto__ 来访问或者修改该属性,其主要原因有两个:

  • 首先,这是隐藏属性,并不是标准定义的;
  • 其次,使用该属性会造成严重的性能问题。因为 JavaScript 通过隐藏类优化了很多原有的对象结构,所以通过直接修改__proto__会直接破坏现有已经优化的结构,触发 V8 重构该对象的隐藏类!

构造函数是怎么创建对象的?

  在 JavaScript 中,使用 new 加上构造函数的这种组合来创建对象和实现对象的继承。不过使用这种方式隐含的语义过于隐晦。其实是 JavaScript 为了吸引 Java 程序员、在语法层面去蹭 Java 热点,所以就被硬生生地强制加入了非常不协调的关键字 new。

function DogFactory(type, color) {
  this.type = type;
  this.color = color;
}
var dog = new DogFactory('Dog', 'Black');

  其实当 V8 执行上面这段代码时,V8 在背后悄悄地做了以下几件事情:

var dog = {};
dog.__proto__ = DogFactory.prototype;
DogFactory.call(dog, 'Dog', 'Black');

机器码、字节码

V8 为什么要引入字节码

  • 早期的 V8 为了提升代码的执行速度,直接将 JavaScript 源代码编译成了没有优化的二进制机器代码,如果某一段二进制代码执行频率过高,那么 V8 会将其标记为热点代码,热点代码会被优化编译器优化,优化后的机器代码执行效率更高。
  • 随着移动设备的普及,V8 团队逐渐发现将 JavaScript 源码直接编译成二进制代码存在两个致命的问题:

    • 时间问题:编译时间过久,影响代码启动速度;
    • 空间问题:缓存编译后的二进制代码占用更多的内存。
  • 这两个问题无疑会阻碍 V8 在移动设备上的普及,于是 V8 团队大规模重构代码,引入了中间的字节码。字节码的优势有如下三点:

    • 解决启动问题:生成字节码的时间很短;
    • 解决空间问题:字节码虽然占用的空间比原始的 JavaScript 多,但是相较于机器代码,字节码还是小了太多,缓存字节码会大大降低内存的使用。
    • 代码架构清晰:采用字节码,可以简化程序的复杂度,使得 V8 移植到不同的 CPU 架构平台更加容易。
  • Bytecode 某种程度上就是汇编语言,只是它没有对应特定的 CPU,或者说它对应的是虚拟的 CPU。这样的话,生成 Bytecode 时简单很多,无需为不同的 CPU 生产不同的代码。要知道,V8 支持 9 种不同的 CPU,引入一个中间层 Bytecode,可以简化 V8 的编译流程,提高可扩展性。
  • 如果我们在不同硬件上去生成 Bytecode,会发现生成代码的指令是一样的。

如何查看字节码

// test.js
function add(x, y) {
  var z = x + y;
  return z;
}
console.log(add(1, 2));

运行./d8 ./test.js --print-bytecode:

[generated bytecode for function: add (0x01000824fe59 <SharedFunctionInfo add>)]
Parameter count 3 #三个参数,包括了显式地传入的 x 和 y,还有一个隐式地传入的 this
Register count 1
Frame size 8
         0x10008250026 @    0 : 25 02             Ldar a1 #将a1寄存器中的值加载到累加器中,LoaD Accumulator from Register
         0x10008250028 @    2 : 34 03 00          Add a0, [0]
         0x1000825002b @    5 : 26 fb             Star r0 #Store Accumulator to Register,把累加器中的值保存到r0寄存器中
         0x1000825002d @    7 : aa                Return  #结束当前函数的执行,并将控制权传回给调用方
Constant pool (size = 0)
Handler Table (size = 0)
Source Position Table (size = 0)
3

常用字节码指令

  • Ldar:表示将寄存器中的值加载到累加器中,你可以把它理解为 LoaD Accumulator from Register,就是把某个寄存器中的值,加载到累加器中。
  • Star:表示 Store Accumulator Register, 你可以把它理解为 Store Accumulator to Register,就是把累加器中的值保存到某个寄存器中
  • Add:Add a0, [0]是从 a0 寄存器加载值并将其与累加器中的值相加,然后将结果再次放入累加器。

    add a0 后面的[0]称之为 feedback vector slot,又叫反馈向量槽,它是一个数组,解释器将解释执行过程中的一些数据类型的分析信息都保存在这个反馈向量槽中了,目的是为了给 TurboFan 优化编译器提供优化信息,很多字节码都会为反馈向量槽提供运行时信息。
  • LdaSmi:将小整数(Smi)加载到累加器寄存器中
  • Return:结束当前函数的执行,并将控制权传回给调用方。返回的值是累加器中的值。

bytecode-ignition

V8 中的字节码指令集 | 理解 V8 的字节码「译」

隐藏类和内联缓存

  JavaScript 是一门动态语言,其执行效率要低于静态语言,V8 为了提升 JavaScript 的执行速度,借鉴了很多静态语言的特性,比如实现了 JIT 机制,为了提升对象的属性访问速度而引入了隐藏类,为了加速运算而引入了内联缓存

为什么静态语言的效率更高?

  静态语言中,如 C++ 在声明一个对象之前需要定义该对象的结构,代码在执行之前需要先被编译,编译的时候,每个对象的形状都是固定的,也就是说,在代码的执行过程中是无法被改变的。可以直接通过偏移量查询来查询对象的属性值,这也就是静态语言的执行效率高的一个原因。

  JavaScript 在运行时,对象的属性是可以被修改的,所以当 V8 使用了一个对象时,比如使用了 obj.x 的时候,它并不知道该对象中是否有 x,也不知道 x 相对于对象的偏移量是多少,也就是说 V8 并不知道该对象的具体的形状。那么,当在 JavaScript 中要查询对象 obj 中的 x 属性时,V8 会按照具体的规则一步一步来查询,这个过程非常的慢且耗时。

将静态的特性引入到 V8

  • V8 采用的一个思路就是将 JavaScript 中的对象静态化,也就是 V8 在运行 JavaScript 的过程中,会假设 JavaScript 中的对象是静态的。
  • 具体地讲,V8 对每个对象做如下两点假设:

    • 对象创建好了之后就不会添加新的属性;
    • 对象创建好了之后也不会删除属性。
  • 符合这两个假设之后,V8 就可以对 JavaScript 中的对象做深度优化了。V8 会为每个对象创建一个隐藏类,对象的隐藏类中记录了该对象一些基础的布局信息,包括以下两点:

    • 对象中所包含的所有的属性;
    • 每个属性相对于对象的偏移量。
  • 有了隐藏类之后,那么当 V8 访问某个对象中的某个属性时,就会先去隐藏类中查找该属性相对于它的对象的偏移量,有了偏移量和属性类型,V8 就可以直接去内存中取出对应的属性值,而不需要经历一系列的查找过程,那么这就大大提升了 V8 查找对象的效率。
  • 在 V8 中,把隐藏类又称为 map,每个对象都有一个 map 属性,其值指向内存中的隐藏类;
  • map 描述了对象的内存布局,比如对象都包括了哪些属性,这些数据对应于对象的偏移量是多少。

通过 d8 查看隐藏类

// test.js
let point1 = { x: 100, y: 200 };
let point2 = { x: 200, y: 300 };
let point3 = { x: 100 };
%DebugPrint(point1);
%DebugPrint(point2);
%DebugPrint(point3);
 ./d8 --allow-natives-syntax ./test.js
# ===============
DebugPrint: 0x1ea3080c5bc5: [JS_OBJECT_TYPE]
# V8 为 point1 对象创建的隐藏类
 - map: 0x1ea308284ce9 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1>
 - elements: 0x1ea3080406e9 <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x1ea3080406e9 <FixedArray[0]> {
    #x: 100 (const data field 0)
    #y: 200 (const data field 1)
 }
0x1ea308284ce9: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 20
 - inobject properties: 2
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - back pointer: 0x1ea308284cc1 <Map(HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x1ea3081c0451 <Cell value= 1>
 - instance descriptors (own) #2: 0x1ea3080c5bf5 <DescriptorArray[2]>
 - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1>
 - constructor: 0x1ea3082413b1 <JSFunction Object (sfi = 0x1ea3081c557d)>
 - dependent code: 0x1ea3080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

# ===============
DebugPrint: 0x1ea3080c5c1d: [JS_OBJECT_TYPE]
# V8 为 point2 对象创建的隐藏类
 - map: 0x1ea308284ce9 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1>
 - elements: 0x1ea3080406e9 <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x1ea3080406e9 <FixedArray[0]> {
    #x: 200 (const data field 0)
    #y: 300 (const data field 1)
 }
0x1ea308284ce9: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 20
 - inobject properties: 2
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - back pointer: 0x1ea308284cc1 <Map(HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x1ea3081c0451 <Cell value= 1>
 - instance descriptors (own) #2: 0x1ea3080c5bf5 <DescriptorArray[2]>
 - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1>
 - constructor: 0x1ea3082413b1 <JSFunction Object (sfi = 0x1ea3081c557d)>
 - dependent code: 0x1ea3080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

# ===============
DebugPrint: 0x1ea3080c5c31: [JS_OBJECT_TYPE]
# V8 为 point3 对象创建的隐藏类
 - map: 0x1ea308284d39 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1>
 - elements: 0x1ea3080406e9 <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x1ea3080406e9 <FixedArray[0]> {
    #x: 100 (const data field 0)
 }
0x1ea308284d39: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 16
 - inobject properties: 1
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - back pointer: 0x1ea308284d11 <Map(HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x1ea3081c0451 <Cell value= 1>
 - instance descriptors (own) #1: 0x1ea3080c5c41 <DescriptorArray[1]>
 - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1>
 - constructor: 0x1ea3082413b1 <JSFunction Object (sfi = 0x1ea3081c557d)>
 - dependent code: 0x1ea3080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

多个对象共用一个隐藏类

  • 在 V8 中,每个对象都有一个 map 属性,该属性值指向该对象的隐藏类。不过如果两个对象的形状是相同的,V8 就会为其复用同一个隐藏类,这样有两个好处:

    • 减少隐藏类的创建次数,也间接加速了代码的执行速度;
    • 减少了隐藏类的存储空间。
  • 那么,什么情况下两个对象的形状是相同的,要满足以下两点:

    • 相同的属性名称;
    • 相等的属性个数。

重新构建隐藏类

  • 给一个对象添加新的属性,删除新的属性,或者改变某个属性的数据类型都会改变这个对象的形状,那么势必也就会触发 V8 为改变形状后的对象重建新的隐藏类。
// test.js
let point = {};
%DebugPrint(point);
point.x = 100;
%DebugPrint(point);
point.y = 200;
%DebugPrint(point);
# ./d8 --allow-natives-syntax ./test.js
DebugPrint: 0x32c7080c5b2d: [JS_OBJECT_TYPE]
 - map: 0x32c7082802d9 <Map(HOLEY_ELEMENTS)> [FastProperties]
 ...

DebugPrint: 0x32c7080c5b2d: [JS_OBJECT_TYPE]
 - map: 0x32c708284cc1 <Map(HOLEY_ELEMENTS)> [FastProperties]
 ...

DebugPrint: 0x32c7080c5b2d: [JS_OBJECT_TYPE]
 - map: 0x32c708284ce9 <Map(HOLEY_ELEMENTS)> [FastProperties]
 ...
  • 每次给对象添加了一个新属性之后,该对象的隐藏类的地址都会改变,这也就意味着隐藏类也随着改变了;如果删除对象的某个属性,那么对象的形状也就随着发生了改变,这时 V8 也会重建该对象的隐藏类;
  • 最佳实践

    • 使用字面量初始化对象时,要保证属性的顺序是一致的;
    • 尽量使用字面量一次性初始化完整对象属性;
    • 尽量避免使用 delete 方法。

通过内联缓存来提升函数执行效率

  虽然隐藏类能够加速查找对象的速度,但是在 V8 查找对象属性值的过程中,依然有查找对象的隐藏类和根据隐藏类来查找对象属性值的过程。如果一个函数中利用了对象的属性,并且这个函数会被多次执行:

function loadX(obj) {
  return obj.x;
}
var obj = { x: 1, y: 3 };
var obj1 = { x: 3, y: 6 };
var obj2 = { x: 3, y: 6, z: 8 };
for (var i = 0; i < 100; i++) {
  // 对比时间差异
  console.time(`---${i}----`)
  loadX(obj);
  console.timeEnd(`---${i}----`)
  loadX(obj1);
  // 产生多态
  loadX(obj2);
}

通常 V8 获取 obj.x 的流程

  • 找对象 obj 的隐藏类;
  • 再通过隐藏类查找 x 属性偏移量;
  • 然后根据偏移量获取属性值,在这段代码中 loadX 函数会被反复执行,那么获取 obj.x 的流程也需要反复被执行;

内联缓存及其原理

  • 函数 loadX 在一个 for 循环里面被重复执行了很多次,因此 V8 会想尽一切办法来压缩这个查找过程,以提升对象的查找效率。这个加速函数执行的策略就是内联缓存 (Inline Cache),简称为 IC;
  • IC 的原理:在 V8 执行函数的过程中,会观察函数中一些调用点 (CallSite) 上的关键中间数据,然后将这些数据缓存起来,当下次再次执行该函数的时候,V8 就可以直接利用这些中间数据,节省了再次获取这些数据的过程,因此 V8 利用 IC,可以有效提升一些重复代码的执行效率。
  • IC 会为每个函数维护一个反馈向量 (FeedBack Vector),反馈向量记录了函数在执行过程中的一些关键的中间数据。
  • 反馈向量其实就是一个表结构,它由很多项组成的,每一项称为一个插槽 (Slot),V8 会依次将执行 loadX 函数的中间数据写入到反馈向量的插槽中。
  • 当 V8 再次调用 loadX 函数时,比如执行到 loadX 函数中的 return obj.x 语句时,它就会在对应的插槽中查找 x 属性的偏移量,之后 V8 就能直接去内存中获取 obj.x 的属性值了。这样就大大提升了 V8 的执行效率。

单态、多态和超态

  • 如果一个插槽中只包含 1 个隐藏类,那么我们称这种状态为单态 (monomorphic);
  • 如果一个插槽中包含了 2 ~ 4 个隐藏类,那我们称这种状态为多态 (polymorphic);
  • 如果一个插槽中超过 4 个隐藏类,那我们称这种状态为超态 (magamorphic)。
  • 单态的性能优于多态和超态,所以我们需要稍微避免多态和超态的情况。要避免多态和超态,那么就尽量默认所有的对象属性是不变的,比如你写了一个 loadX(obj) 的函数,那么当传递参数时,尽量不要使用多个不同形状的 obj 对象。

总结:
  V8 引入了内联缓存(IC),IC 会监听每个函数的执行过程,并在一些关键的地方埋下监听点,这些包括了加载对象属性 (Load)、给对象属性赋值 (Store)、还有函数调用 (Call),V8 会将监听到的数据写入一个称为反馈向量 (FeedBack Vector) 的结构中,同时 V8 会为每个执行的函数维护一个反馈向量。有了反馈向量缓存的临时数据,V8 就可以缩短对象属性的查找路径,从而提升执行效率。但是针对函数中的同一段代码,如果对象的隐藏类是不同的,那么反馈向量也会记录这些不同的隐藏类,这就出现了多态和超态的情况。我们在实际项目中,要尽量避免出现多态或者超态的情况

异步编程与消息队列

V8 是如何执行回调函数的

  回调函数有两种类型:同步回调和异步回调,同步回调函数是在执行函数内部被执行的,而异步回调函数是在执行函数外部被执行的。
  通用 UI 线程宏观架构:
通用UI线程架构
  UI 线程提供一个消息队列,并将待执行的事件添加到消息队列中,然后 UI 线程会不断循环地从消息队列中取出事件、执行事件。关于异步回调,这里也有两种不同的类型,其典型代表是 setTimeout 和 XMLHttpRequest:

  • setTimeout 的执行流程其实是比较简单的,在 setTimeout 函数内部封装回调消息,并将回调消息添加进消息队列,然后主线程从消息队列中取出回调事件,并执行回调函数。
  • XMLHttpRequest 稍微复杂一点,因为下载过程需要放到单独的一个线程中去执行,所以执行 XMLHttpRequest.send 的时候,宿主会将实际请求转发给网络线程,然后 send 函数退出,主线程继续执行下面的任务。网络线程在执行下载的过程中,会将一些中间信息和回调函数封装成新的消息,并将其添加进消息队列中,然后主线程从消息队列中取出回调事件,并执行回调函数。

宏任务和微任务

  • 调用栈:调用栈是一种数据结构,用来管理在主线程上执行的函数的调用关系。主线程在执行任务的过程中,如果函数的调用层次过深,可能造成栈溢出的错误,我们可以使用 setTimeout 来解决栈溢出的问题。setTimeout 的本质是将同步函数调用改成异步函数调用,这里的异步调用是将回调函数封装成宏任务,并将其添加进消息队列中,然后主线程再按照一定规则循环地从消息队列中读取下一个宏任务。
  • 宏任务:就是指消息队列中的等待被主线程执行的事件。每个宏任务在执行时,V8 都会重新创建栈,然后随着宏任务中函数调用,栈也随之变化,最终,当该宏任务执行结束时,整个栈又会被清空,接着主线程继续执行下一个宏任务。
  • 微任务:你可以把微任务看成是一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。
  • JavaScript 中之所以要引入微任务,主要是由于主线程执行消息队列中宏任务的时间颗粒度太粗了,无法胜任一些对精度和实时性要求较高的场景,微任务可以在实时性和效率之间做一个有效的权衡。另外使用微任务,可以改变我们现在的异步编程模型,使得我们可以使用同步形式的代码来编写异步调用。
  • 微任务是基于消息队列、事件循环、UI 主线程还有堆栈而来的,然后基于微任务,又可以延伸出协程、Promise、Generator、await/async 等现代前端经常使用的一些技术。

    微任务技术栈

    // 不会使浏览器卡死
    function foo() {
      setTimeout(foo, 0);
    }
    foo();

    调用栈、主线程、消息队列
    微任务:

// 浏览器console控制台可使浏览器卡死(无法响应鼠标事件等)
function foo() {
  return Promise.resolve().then(foo);
}
foo();
  • 如果当前的任务中产生了一个微任务,通过 Promise.resolve() 或者 Promise.reject() 都会触发微任务,触发的微任务不会在当前的函数中被执行,所以执行微任务时,不会导致栈的无限扩张
  • 和异步调用不同,微任务依然会在当前任务执行结束之前被执行,这也就意味着在当前微任务执行结束之前,消息队列中的其他任务是不可能被执行的。因此在函数内部触发的微任务,一定比在函数内部触发的宏任务要优先执行。
  • 微任务依然是在当前的任务中执行的,所以如果在微任务中循环触发新的微任务,那么将导致消息队列中的其他任务没有机会被执行。

前端异步编程方案史

前端异步编程方案史

  • Callback 模式的异步编程模型需要实现大量的回调函数,大量的回调函数会打乱代码的正常逻辑,使得代码变得不线性、不易阅读,这就是我们所说的回调地狱问题
  • Promise 能很好地解决回调地狱的问题,我们可以按照线性的思路来编写代码,这个过程是线性的,非常符合人的直觉。
  • 但是这种方式充满了 Promise 的 then() 方法,如果处理流程比较复杂的话,那么整段代码将充斥着大量的 then,语义化不明显,代码不能很好地表示执行流程。我们想要通过线性的方式来编写异步代码,要实现这个理想,最关键的是要能实现函数暂停和恢复执行的功能。而生成器就可以实现函数暂停和恢复,我们可以在生成器中使用同步代码的逻辑来异步代码 (实现该逻辑的核心是协程)。
  • 但是在生成器之外,我们还需要一个触发器来驱动生成器的执行。前端的最终方案就是 async/await,async 是一个可以暂停和恢复执行的函数,在 async 函数内部使用 await 来暂停 async 函数的执行,await 等待的是一个 Promise 对象,如果 Promise 的状态变成 resolve 或者 reject,那么 async 函数会恢复执行。因此,使用 async/await 可以实现以同步的方式编写异步代码这一目标。和生成器函数一样,使用了 async 声明的函数在执行时,也是一个单独的协程,我们可以使用 await 来暂停该协程,由于 await 等待的是一个 Promise 对象,我们可以 resolve 来恢复该协程。
协程是一种比线程更加轻量级的存在。你可以把协程看成是跑在线程上的任务,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程。比如,当前执行的是 A 协程,要启动 B 协程,那么 A 协程就需要将主线程的控制权交给 B 协程,这就体现在 A 协程暂停执行,B 协程恢复执行;同样,也可以从 B 协程中启动 A 协程。通常,如果从 A 协程启动 B 协程,我们就把 A 协程称为 B 协程的父协程。

正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。每一时刻,该线程只能执行其中某一个协程。最重要的是,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。

资料拓展:co 函数库的含义和用法

垃圾回收

垃圾数据

  从“GC Roots”对象出发,遍历 GC Root 中的所有对象,如果通过 GC Roots 没有遍历到的对象,则这些对象便是垃圾数据。V8 会有专门的垃圾回收器来回收这些垃圾数据。

垃圾回收算法

垃圾回收大致可以分为以下几个步骤:

  • 第一步,通过 GC Root 标记空间中活动对象和非活动对象。目前 V8 采用的可访问性(reachability)算法来判断堆中的对象是否是活动对象。具体地讲,这个算法是将一些 GC Root 作为初始存活的对象的集合,从 GC Roots 对象出发,遍历 GC Root 中的所有对象:

    • 通过 GC Root 遍历到的对象,我们就认为该对象是可访问的(reachable),那么必须保证这些对象应该在内存中保留,我们也称可访问的对象为活动对象
    • 通过 GC Roots 没有遍历到的对象,则是不可访问的(unreachable),那么这些不可访问的对象就可能被回收,我们称不可访问的对象为非活动对象
    • 浏览器环境中,GC Root 有很多,通常包括了以下几种 (但是不止于这几种):

      • 全局的 window 对象(位于每个 iframe 中);
      • 文档 DOM 树,由可以通过遍历文档到达的所有原生 DOM 节点组成;
      • 存放栈上变量。
  • 第二步,回收非活动对象所占据的内存。其实就是在所有的标记完成之后,统一清理内存中所有被标记为可回收的对象。
  • 第三步,做内存整理。一般来说,频繁回收对象后,内存中就会存在大量不连续空间,我们把这些不连续的内存空间称为内存碎片。当内存中出现了大量的内存碎片之后,如果需要分配较大的连续内存时,就有可能出现内存不足的情况,所以最后一步需要整理这些内存碎片。但这步其实是可选的,因为有的垃圾回收器不会产生内存碎片(比如副垃圾回收器)

垃圾回收

  • V8 依据代际假说,将堆内存划分为新生代和老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放生存时间久的对象。代际假说有两个特点:

    • 第一个是大部分对象都是“朝生夕死”的,也就是说大部分对象在内存中存活的时间很短,比如函数内部声明的变量,或者块级作用域中的变量,当函数或者代码块执行结束时,作用域中定义的变量就会被销毁。因此这一类对象一经分配内存,很快就变得不可访问;
    • 第二个是不死的对象,会活得更久,比如全局的 window、DOM、Web API 等对象。
  • 为了提升垃圾回收的效率,V8 设置了两个垃圾回收器,主垃圾回收器和副垃圾回收器。

    • 主垃圾回收器负责收集老生代中的垃圾数据,副垃圾回收器负责收集新生代中的垃圾数据。
    • 副垃圾回收器采用了 Scavenge 算法,是把新生代空间对半划分为两个区域(有些地方也称作From和To空间),一半是对象区域,一半是空闲区域。新的数据都分配在对象区域,等待对象区域快分配满的时候,垃圾回收器便执行垃圾回收操作,之后将存活的对象从对象区域拷贝到空闲区域,并将两个区域互换。

      • 这种角色翻转的操作还能让新生代中的这两块区域无限重复使用下去。
      • 副垃圾回收器每次执行清理操作时,都需要将存活的对象从对象区域复制到空闲区域,复制操作需要时间成本,如果新生区空间设置得太大了,那么每次清理的时间就会过久,所以为了执行效率,一般新生区的空间会被设置得比较小
      • 副垃圾回收器还会采用对象晋升策略,也就是移动那些经过两次垃圾回收依然还存活的对象到老生代中。
    • 主垃圾回收器回收器主要负责老生代中的垃圾数据的回收操作,会经历标记、清除和整理过程

      • 主垃圾回收器主要负责老生代中的垃圾回收。除了新生代中晋升的对象,一些大的对象会直接被分配到老生代里。
      • 老生代中的对象有两个特点:一个是对象占用空间大;另一个是对象存活时间长。

Stop-The-World

  由于 JavaScript 是运行在主线程之上的,因此,一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。我们把这种行为叫做全停顿(Stop-The-World)

  • V8 最开始的垃圾回收器有两个特点:

    • 第一个是垃圾回收在主线程上执行,
    • 第二个特点是一次执行一个完整的垃圾回收流程。
  • 由于这两个原因,很容易造成主线程卡顿,所以 V8 采用了很多优化执行效率的方案。

    • 第一个方案是并行回收,在执行一个完整的垃圾回收过程中,垃圾回收器会使用多个辅助线程来并行执行垃圾回收。
    • 第二个方案是增量式垃圾回收,垃圾回收器将标记工作分解为更小的块,并且穿插在主线程不同的任务之间执行。采用增量垃圾回收时,垃圾回收器没有必要一次执行完整的垃圾回收过程,每次执行的只是整个垃圾回收过程中的一小部分工作。
    • 第三个方案是并发回收,回收线程在执行 JavaScript 的过程,辅助线程能够在后台完成的执行垃圾回收的操作。

      资料参考:深入解读 V8 引擎的「并发标记」技术
    • 主垃圾回收器就综合采用了所有的方案(并发标记,增量标记,辅助清理),副垃圾回收器也采用了部分方案。

似此星辰非昨夜,为谁风露立中宵

Breaking the JavaScript Speed Limit with V8

  Daniel Clifford 在 Google I/O 2012 上做了一个精彩的演讲“Breaking the JavaScript Speed Limit with V8”。在演讲中,他深入解释了 13 个简单的代码优化方法,可以让你的JavaScript代码在 Chrome V8 引擎编译/运行时更加快速。在演讲中,他介绍了怎么优化,并解释了原因。下面简明的列出了13 个 JavaScript 性能提升技巧

  1. 在构造函数里初始化所有对象的成员(所以这些实例之后不会改变其隐藏类);
  2. 总是以相同的次序初始化对象成员;
  3. 尽量使用可以用 31 位有符号整数表示的数;
  4. 为数组使用从 0 开始的连续的主键;
  5. 别预分配大数组(比如大于 64K 个元素)到其最大尺寸,令其尺寸顺其自然发展就好;
  6. 别删除数组里的元素,尤其是数字数组;
  7. 别加载未初始化或已删除的元素;
  8. 对于固定大小的数组,使用”array literals“初始化(初始化小额定长数组时,用字面量进行初始化);
  9. 小数组(小于 64k)在使用之前先预分配正确的尺寸;
  10. 请勿在数字数组中存放非数字的值(对象);
  11. 尽量使用单一类型(monomorphic)而不是多类型(polymorphic)(如果通过非字面量进行初始化小数组时,切勿触发类型的重新转换);
  12. 不要使用 try{} catch{}(如果存在 try/catch 代码快,则将性能敏感的代码放到一个嵌套的函数中);
  13. 在优化后避免在方法中修改隐藏类。
演讲资料参考: Performance Tips for JavaScript in V8 | JavaScript V8性能小贴士【译】 | 内网视频 | YouTube

在 V8 引擎里 5 个优化代码的技巧

  1. 对象属性的顺序: 在实例化你的对象属性的时候一定要使用相同的顺序,这样隐藏类和随后的优化代码才能共享;
  2. 动态属性: 在对象实例化之后再添加属性会强制使得隐藏类变化,并且会减慢为旧隐藏类所优化的代码的执行。所以,要在对象的构造函数中完成所有属性的分配;
  3. 方法: 重复执行相同的方法会运行的比不同的方法只执行一次要快 (因为内联缓存);
  4. 数组: 避免使用 keys 不是递增的数字的稀疏数组,这种 key 值不是递增数字的稀疏数组其实是一个 hash 表。在这种数组中每一个元素的获取都是昂贵的代价。同时,要避免提前申请大数组。最好的做法是随着你的需要慢慢的增大数组。最后,不要删除数组中的元素,因为这会使得 keys 变得稀疏;
  5. 标记值 (Tagged values): V8 用 32 位来表示对象和数字。它使用一位来区分它是对象 (flag = 1) 还是一个整型 (flag = 0),也被叫做小整型(SMI),因为它只有 31 位。然后,如果一个数值大于 31 位,V8 将会对其进行 box 操作,然后将其转换成 double 型,并且创建一个新的对象来装这个数。所以,为了避免代价很高的 box 操作,尽量使用 31 位的有符号数。
资料参考:How JavaScript works: inside the V8 engine + 5 tips on how to write optimized code | 译文
box 操作参考:JavaScript类型:关于类型,有哪些你不知道的细节? | JavaScript 的装箱和拆箱 | 谈谈JavaScript中装箱和拆箱

JavaScript 启动性能瓶颈分析与解决方案

资料参考: JavaScript Start-up Performance | JavaScript 启动性能瓶颈分析与解决方案

天长地久有时尽,V8 绵绵无绝期

资料参考

番外篇

  • Chrome插件Console Importer推荐:Easily import JS and CSS resources from Chrome console. (可以在浏览器控制台安装 loadsh、moment、jQuery 等库,在控制台直接验证、使用这些库。)
    效果图:
    BCv8tP.gif

本文首发于个人博客,欢迎指正和star
本文同步发布并于掘金社区

查看原文

赞 84 收藏 56 评论 31

认证与成就

  • 获得 0 次点赞
  • 获得 0 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 0 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2020-09-14
个人主页被 61 人浏览