一、前言
前端的模块化规范包括 commonJS、AMD、CMD 和 ES6。其中 AMD 和 CMD 可以说是过渡期的产物,目前较为常见的是commonJS 和 ES6。在 TS 中这两种模块化方案的混用,往往会出现一些意想不到的问题。
二、import * as
考虑到兼容性,我们一般会将代码编译为 es5 标准,于是 tsconfig.json 会有以下配置:
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
}
}
代码编译后最终会以 commonJS 的形式输出。
使用 React 的时候,这种写法 import React from "react" 会收到一个莫名其妙的报错:
Module "react" has no default export
这时候你只能把代码改成这样:import * as React from "react"。
究其原因,React 是以 commonJS 的规范导出的,而 import React from "react" 这种写法会去找 React 模块中的 exports.default,而 React 并没有导出这个属性,于是就报了如上错误。而 import * as React 的写法会取 module.exports 中的值,这样使用起来就不会有任何问题。我们来看看 React 模块导出的代码到底是怎样的(精简过):
...
var React = {
Children: {
map: mapChildren,
forEach: forEachChildren,
count: countChildren,
toArray: toArray,
only: onlyChild
},
createRef: createRef,
Component: Component,
PureComponent: PureComponent,
...
}
module.exports = React;
可以看到,React 导出的是一个对象,自然也不会有 default 属性。
二、esModuleInterop
为了兼容这种这种情况,TS 提供了配置项 esModuleInterop 和 allowSyntheticDefaultImports,加上后就不会有报错了:
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true
}
}
其中 allowSyntheticDefaultImports 这个字段的作用只是在静态类型检查时,把 import 没有 exports.default 的报错忽略掉。
而 esModuleInterop 会真正的在编译的过程中生成兼容代码,使模块能正确的导入。还是开始的代码:
import React from "react";
现在 TS 编译后是这样的:
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
var react_1 = __importDefault(require("react"));
编译器帮我们生成了一个新的对象,将模块赋值给它的 default 属性,运行时就不会报错了。
三、Tree Shaking
如果把 TS 按照 ES6 规范编译,就不需要加上 esModuleInterop,只需要 allowSyntheticDefaultImports,防止静态类型检查时报错。
{
"compilerOptions": {
"module": "es6",
"target": "es6",
"allowSyntheticDefaultImports": true
}
}
什么情况下我们会考虑导出成 ES6 规范呢?多数情况是为了使用 webpack 的 tree shaking 特性,因为它只对 ES6 的代码生效。
顺便再发散一下,讲讲 babel-plugin-component。
import { Button, Select } from 'element-ui'
上面的代码经过编译后,是下面这样的:
var a = require('element-ui');
var Button = a.Button;
var Select = a.Select;
var a = require('element-ui') 会引入整个组件库,即使只用了其中的 2 个组件。
babel-plugin-component 的作用是将代码做如下转换:
// 转换前
import { Button, Select } from 'element-ui'
// 转换后
import Button from 'element-ui/lib/button'
import Select from 'element-ui/lib/select'
最终编译出来是这个样子,只会加载用到的组件:
var Button = require('element-ui/lib/button');
var Select = require('element-ui/lib/select');
四、总结
本文讲解了 TypeScript 是如何导入不同模块标准打包的代码的。无论你导入的是 commonJS 还是 ES6 的代码,万无一失的方式是把 esModuleInterop 和 allowSyntheticDefaultImports 都配置上。
初始化
使用 https://github.com/XYShaoKang... 作为基础模板
gatsby new gatsby-project-config https://github.com/XYShaoKang/gatsby-hello-world
Prettier 配置
安装 VSCode 扩展
按 Ctrl + P (MAC 下: Cmd + P) 输入以下命令,按回车安装
ext install esbenp.prettier-vscode
安装依赖
yarn add -D prettier
Prettier 配置文件.prettierrc.js
// .prettierrc.js
module.exports = {
trailingComma: 'es5',
tabWidth: 2,
semi: false,
singleQuote: true,
endOfLine: 'lf',
printWidth: 50,
arrowParens: 'avoid',
}
ESLint 配置
安装 VSCode 扩展
按 Ctrl + P (MAC 下: Cmd + P) 输入以下命令,按回车安装
ext install dbaeumer.vscode-eslint
安装 ESLint 依赖
yarn add -D eslint babel-eslint eslint-config-google eslint-plugin-react eslint-plugin-filenames
ESLint 配置文件.eslintrc.js
使用官方仓库的配置,之后在根据需要修改
// https://github.com/gatsbyjs/gatsby/blob/master/.eslintrc.js
// .eslintrc.js
module.exports = {
parser: 'babel-eslint',
extends: [
'google',
'eslint:recommended',
'plugin:react/recommended',
],
plugins: ['react', 'filenames'],
parserOptions: {
ecmaVersion: 2016,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
env: {
browser: true,
es6: true,
node: true,
jest: true,
},
globals: {
before: true,
after: true,
spyOn: true,
__PATH_PREFIX__: true,
__BASE_PATH__: true,
__ASSET_PREFIX__: true,
},
rules: {
'arrow-body-style': [
'error',
'as-needed',
{ requireReturnForObjectLiteral: true },
],
'no-unused-expressions': [
'error',
{
allowTaggedTemplates: true,
},
],
'consistent-return': ['error'],
'filenames/match-regex': [
'error',
'^[a-z-\\d\\.]+$',
true,
],
'no-console': 'off',
'no-inner-declarations': 'off',
quotes: ['error', 'backtick'],
'react/display-name': 'off',
'react/jsx-key': 'warn',
'react/no-unescaped-entities': 'off',
'react/prop-types': 'off',
'require-jsdoc': 'off',
'valid-jsdoc': 'off',
},
settings: {
react: {
version: '16.4.2',
},
},
}
解决 Prettier ESLint 规则冲突
推荐配置
安装依赖
yarn add -D eslint-config-prettier eslint-plugin-prettier
在.eslintrc.js中的extends添加'plugin:prettier/recommended'
module.exports = {
extends: ['plugin:prettier/recommended'],
}
VSCode 中 Prettier 和 ESLint 协作
方式一:使用 ESLint 扩展来格式化代码
配置.vscode/settings.json
// .vscode/settings.json
{
"eslint.format.enable": true,
"[javascript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"[javascriptreact]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
}
}
ESLint 扩展会默认忽略.开头的文件,比如.eslintrc.js
如果需要格式化.开头的文件,可以在.eslintignore中添加一个否定忽略来启用对应文件的格式化功能.
!.eslintrc.js
或者直接使用!.*,这样可以开启所有点文件的格式化功能
方式二:使用 Prettier 扩展来格式化代码
在版prettier-vscode@v5.0.0中已经删除了直接对linter的集成,所以版没法像之前那样,通过prettier-eslint来集成ESLint的修复了(一定要这样用的话,可以通过降级到prettier-vscode@4来使用了).如果要使用Prettier来格式化的话,就只能按照官方指南中的说的集成方法,让Prettier来处理格式,通过配置在保存时使用ESlint自动修复代码.只是这样必须要保存文件时,才能触发ESLint的修复了.
配置 VSCode 使用 Prettier 来格式化 js 和 jsx 文件
在项目中新建文件.vscode/settings.json
// .vscode/settings.json
{
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}
说实话这个体验很糟糕,之前直接一键格式化代码并且修复 ESLint 错误,可以对比格式化之前和格式化之后的代码,如果感觉不对可以直接撤销更改就好了.现在必须要通过保存,才能触发修复 ESlint 错误.而在开发过程中,通过监听文件改变来触发热加载或者重新编译是很常见的操作.这样之后每次想要去修复 ESLint 错误,还是只是想看看修复错误之后的样子,都必须要去触发热加载或重新编译,每次操作的成本就太高了.
我更推荐第一种方式使用 ESLint 扩展来对代码进行格式化.
调试 Gatsby 配置
调试构建过程
添加配置文件.vscode/launch.json
// .vscode/launch.json
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Gatsby develop",
"type": "node",
"request": "launch",
"protocol": "inspector",
"program": "${workspaceRoot}/node_modules/gatsby/dist/bin/gatsby",
"args": ["develop"],
"stopOnEntry": false,
"runtimeArgs": ["--nolazy"],
"sourceMaps": false,
"outputCapture": "std"
}
]
}
的gatsby@2.22.*版本中调试不能进到断点,解决办法是降级到2.21.*,yarn add gatsby@2.21.40,等待官方修复再使用版本的
调试客户端
需要安装 Debugger for Chrome 扩展
ext install msjsdiag.debugger-for-chrome
添加配置文件.vscode/launch.json
// .vscode/launch.json
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Gatsby Client Debug",
"url": "http://localhost:8000",
"webRoot": "${workspaceFolder}"
}
]
}
先启动 Gatsby,yarn develop,然后按 F5 开始调试.
收集了一些工作中常用的工具。
如果你有好用的工具或者有意思的工具网站,要留言哦!
React是Facebook开发的一款JS库,那么Facebook为什么要建造React呢,主要为了解决什么问题,通过这个又是如何解决的?
从这几个问题出发我就在网上搜查了一下,有这样的解释。
Facebook认为MVC无法满足他们的扩展需求,由于他们非常巨大的代码库和庞大的组织,使得MVC很快变得非常复复杂,每当需要添加一项新的功能或特性时,系统的复杂度就成级数增长,致使代码变得脆弱和不可预测,结果导致他们的MVC正在土崩瓦解。认为MVC不适合大规模应用,当系统中有很多的模型和相应的视图时,其复杂度就会迅速扩大,非常难以理解和调试,特别是模型和视图间可能存在的双向数据流动。
解决这个问题需要“以某种方式组织代码,使其更加可预测”,这通过他们(Facebook)提出的Flux和React已经完成。
Flux
是一个系统架构,用于推进应用中的数据单向流动。React
是一个JavaScript框架,用于构建“可预期的”和“声明式的”Web用户界面,它已经使Facebook更快地开发Web应用
对于Flux,目前还没怎么研究,不怎么懂,这里就先把Flux的图放上来,有兴趣或者了解的可以再分享下,这里主要说下React。
那么React是解决什么问题的,在官网可以找到这样一句话:
We built React to solve one problem: building large applications with data that changes over time.
构建那些数据会随时间改变的大型应用,做这些,React有两个主要的特点:
另外在React官网上,通过《Why did we build React?》为什么我们要建造React的文档中还可以了解到以下四点:
Virtual DOM 虚拟DOM
传统的web应用,操作DOM一般是直接更新操作的,但是我们知道DOM更新通常是比较昂贵的。而React为了尽可能减少对DOM的操作,提供了一种不同的而又强大的方式来更新DOM,代替直接的DOM操作。就是Virtual DOM
,一个轻量级的虚拟的DOM,就是React抽象出来的一个对象,描述dom应该什么样子的,应该如何呈现。通过这个Virtual DOM去更新真实的DOM,由这个Virtual DOM管理真实DOM的更新。
为什么通过这多一层的Virtual DOM操作就能更快呢? 这是因为React有个diff算法,更新Virtual DOM并不保证马上影响真实的DOM,React会等到事件循环结束,然后利用这个diff算法,通过当前新的dom表述与之前的作比较,计算出最小的步骤更新真实的DOM。
component 的使用在 React 里极为重要, 因为 components 的存在让计算 DOM diff 更。
State 和 Render
React是如何呈现真实的DOM,如何渲染组件,什么时候渲染,怎么同步更新的,这就需要简单了解下State和Render了。state属性包含定义组件所需要的一些数据,当数据发生变化时,将会调用Render重现渲染,这里只能通过提供的setState方法更新数据。
好了,说了这么多,下面看写代码吧,先看一个官网上提供的Hello World
的示例:
<!DOCTYPE html> <html> <head> <script src="http://fb.me/react-0.12.1.js"></script> <script src="http://fb.me/JSXTransformer-0.12.1.js"></script> </head> <body> <div id="example"></div> <script type="text/jsx"> React.render( <h1>Hello, world!</h1>,
document.getElementById('example')
); </script> </body> </html>
这个很简单,浏览器访问,可以看到Hello, world!
字样。JSXTransformer.js
是支持解析JSX语法的,JSX是可以在Javascript中写html代码的一种语法。如果不喜欢,React也提供原生Javascript的方法。
再来看下另外一个例子:
<html> <head> <title>Hello React</title> <script src="http://fb.me/react-0.12.1.js"></script> <script src="http://fb.me/JSXTransformer-0.12.1.js"></script> <script src="http://code.jquery.com/jquery-1.10.0.min.js"></script> <script src="http://cdnjs.cloudflare.com/ajax/libs/showdown/0.3.1/showdown.min.js"></script> <style> #content{ width: 800px; margin: 0 auto; padding: 5px 10px; background-color:#eee; } .commentBox h1{ background-color: #bbb; } .commentList{ border: 1px solid yellow; padding:10px; } .commentList .comment{ border: 1px solid #bbb; padding-left: 10px; margin-bottom:10px; } .commentList .commentAuthor{ font-size: 20px; } .commentForm{ margin-top: 20px; border: 1px solid red; padding:10px; } .commentForm textarea{ width:100%; height:50px; margin:10px 0 10px 2px; } </style> </head> <body> <div id="content"></div> <script type="text/jsx"> var staticData = [ {author: "张飞", text: "我在写一条评论~!"}, {author: "关羽", text: "2货,都知道你在写的是一条评论。。"}, {author: "刘备", text: "哎,咋跟这俩逗逼结拜了!"} ]; var converter = new Showdown.converter();//markdown /** 组件结构: <CommentBox> <CommentList> <Comment /> </CommentList> <CommentForm /> </CommentBox> */ //评论内容组件 var Comment = React.createClass({ render: function (){ var rawMarkup = converter.makeHtml(this.props.children.toString()); return ( <div className="comment"> <h2 className="commentAuthor"> {this.props.author}: </h2> <span dangerouslySetInnerHTML={{__html: rawMarkup}} /> </div> ); } }); //评论列表组件 var CommentList = React.createClass({ render: function (){ var commentNodes = this.props.data.map(function (comment){ return ( <Comment author={comment.author}> {comment.text} </Comment> ); }); return ( <div className="commentList"> {commentNodes} </div> ); } }); //评论表单组件 var CommentForm = React.createClass({ handleSubmit: function (e){ e.preventDefault(); var author = this.refs.author.getDOMNode().value.trim(); var text = this.refs.text.getDOMNode().value.trim(); if(!author || !text){ return; } this.props.onCommentSubmit({author: author, text: text}); this.refs.author.getDOMNode().value = ''; this.refs.text.getDOMNode().value = ''; return; }, render: function (){ return ( <form className="commentForm" onSubmit={this.handleSubmit}> <input type="text" placeholder="Your name" ref="author" /><br/> <textarea type="text" placeholder="Say something..." ref="text" ></textarea><br/> <input type="submit" value="Post" /> </form> ); } }); //评论块组件 var CommentBox = React.createClass({ loadCommentsFromServer: function (){ this.setState({data: staticData}); /* 方便起见,这里就不走服务端了,可以自己尝试 $.ajax({ url: this.props.url + "?_t=" + new Date().valueOf(), dataType: 'json', success: function (data){ this.setState({data: data}); }.bind(this), error: function (xhr, status, err){ console.error(this.props.url, status, err.toString()); }.bind(this) }); */ }, handleCommentSubmit: function (comment){ //TODO: submit to the server and refresh the list var comments = this.state.data; var newComments = comments.concat([comment]); //这里也不向后端提交了 staticData = newComments; this.setState({data: newComments}); }, //初始化 相当于构造函数 getInitialState: function (){ return {data: []}; }, //组件添加的时候运行 componentDidMount: function (){ this.loadCommentsFromServer(); this.interval = setInterval(this.loadCommentsFromServer, this.props.pollInterval); }, //组件删除的时候运行 componentWillUnmount: function() { clearInterval(this.interval); }, //调用setState或者父级组件重新渲染不同的props时才会重新调用 render: function (){ return ( <div className="commentBox"> <h1>Comments</h1> <CommentList data={this.state.data}/> <CommentForm onCommentSubmit={this.handleCommentSubmit} /> </div> ); } }); //当前目录需要有comments.json文件 //这里定义属性,如url、pollInterval,包含在props属性中 React.render( <CommentBox url="comments.json" pollInterval="2000" />, document.getElementById("content") ); </script> </body> </html>
乍一看挺多,主要看脚本部分就可以了。方便起见,这里都没有走后端。定义了一个全局的变量staticData
,可权当是走服务端,通过浏览器的控制台改变staticData
的值,查看下效果,提交一条评论,查看下staticData的值的变化。
国外应用的较多,facebook、Yahoo、Reddit等。在github可以看到一个列表Sites-Using-React,国内的话,查了查,貌似比较少,目前知道的有一个杭州大搜车。大多技术要在国内应用起来一般是较慢的,不过React确实感觉比较特殊,特别是UI的组件化和Virtual DOM的思想,我个人比较看好,有兴趣继续研究研究。
和其他一些js框架相比,React怎样,比如Backbone、Angular等。
与传统PC桌面不同,手机屏幕的尺寸更加小巧操作,方式也已触控为主,APP界面设计不但要保证APP功能的完整性和合理性,又要保证APP的功能性和实用性,在保证其拥有流畅的操作感受的同时,满足人们的审美需求。
接下来为大家介绍几款手机appui界面设计
--手机appUI设计--
--手机appUI设计--
--手机appUI设计--
--手机appUI设计--
--手机appUI设计--
--手机appUI设计--
--手机appUI设计--
--手机appUI设计--
--手机appUI设计--
--手机appUI设计--
--手机appUI设计--
--手机appUI设计--
--手机appUI设计--
--手机appUI设计--
--手机appUI设计--
--手机appUI设计--
--手机appUI设计--
--手机appUI设计--
(以上图片均来源于网络)
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务
更多精彩文章:
闭包是一个让初级JavaScript
使用者既熟悉又陌生的一个概念。因为闭包在我们书写JavaScript
代码时,随处可见,但是我们又不知道哪里用了闭包。
关于闭包的定义,网上(书上)的解释总是千奇百怪,我们也只能“取其精华去其糟粕”去总结一下。
ECMAScript中,闭包指的是:
从实践角度:一下才算是闭包:
闭包跟词法作用域,作用域链,执行上下文这几个JavaScript
中重要的概念都有关系,因此要想真的理解闭包,至少要对那几个概念不陌生。
闭包的优点:
闭包的缺点:
我们来一步一步引出闭包。
自执行函数也叫立即调用函数(IIFE),是一个在定义时就执行的函数。
var a=1;
(function() { console.log(a)
})()
上述代码是一个最简单的自执行函数。
在ES6之前,是没有块级作用域的,只有全局作用域和函数作用域,因此自执行函数还能在ES6之前实现块级作用域。
// ES6 块级作用域 var a = 1; if(true) { let a=111; console.log(a); // 111 } console.log(a); // 1
这里 if{} 中用let声明了一个 a。这个 a 就具有块级作用域,在这个 {} 中访问 a ,永远访问的都是 let 声明的a,跟全局作用域中的a没有关系。如果我们把 let 换成 var ,就会污染全局变量 a 。
如果用自执行函数来实现:
var a = 1;
(function() { if(true) { var a=111; console.log(a); // 111 }
})() console.log(a); // 1
为什么要在这里要引入自执行函数的概念呢?因为通常我们会用自执行函数来创建闭包,实现一定的效果。
来看一个基本上面试提问题:
for(var i=0;i<5;i++) {
setTimeout(function() { console.log(i);
},1000)
}
在理想状态下我们期望输出的是 0 ,1 ,2 ,3 ,4。但是实际上输出的是5 ,5 ,5 ,5 ,5。为什么是这样呢?其实这里不仅仅涉及到作用域,作用域链还涉及到Event Loop、微任务、宏任务。但是在这里不讲这些。
下面我们先解释它为什么会输出 5个5,然后再用自执行函数来修改它,以达到我们预期的结果。
提示:for 循环中,每一次的都声明一个同名变量,下一个变量的值为上一次循环执行完同名变量的值。
首先用var声明变量 for 是不会产生块级作用域的,所以在 () 中声明的 i 为全局变量。相当于:
// 伪代码 var i; for(i=0;i<5;i++) {
setTimeout(function() { console.log(i);
},1000)
}
setTimeout中的第一个参数为一个全局的匿名函数。相当于:
// 伪代码 var i; var f = function() { console.log(i);
} for(i=0;i<5;i++) {
setTimeout(f,1000)
}
由于setTimeout是在1秒之后执行的,这个时候for循环已经执行完毕,此时的全局变量 i 已经变成了 5 。1秒后5个setTimeout中的匿名函数会同时执行,也就是5个 f 函数执行。这个时候 f 函数使用的变量 i 根据作用域链的查找规则找到了全局作用域中的 i 。因此会输出 5 个5。
那我们怎样来修改它呢?
for(var i=0;i<5;i++) {
(function (){ setTimeout(function() { console.log(i);
},1000)
})();
}
上述例子会输出我们期望的值吗?答案是否。为什么呢?我们虽然把 setTimeout 包裹在一个匿名函数中了,但是当setTimeout中匿名函数执行时,首先去匿名函数中查找 i 的值,找不到还是会找到全局作用域中,最终 i 的值仍然是全局变量中的 i ,仍然为 5个5.
那我们把外层的匿名函数中声明一个变量 j 让setTimeout中的匿名函数访问这个 j 不就找不到全局变量中的变量了吗。
for(var i=0;i<5;i++) {
(function (){ var j = i;
setTimeout(function() { console.log(j);
},1000)
})();
}
这个时候才达到了我们预期的结果:0 1 2 3 4。
我们来优化一下:
for(var i=0;i<5;i++) {
(function (i){ setTimeout(function() { console.log(i);
},1000)
})(i);
}
*思路2:用 let 声明变量,产生块级作用域。
for(let i=0;i<5;i++) {
setTimeout(function() { console.log(i);
},1000)
}
这时for循环5次,产生 5 个块级作用域,也会声明 5 个具有块级作用域的变量 i ,因此setTimeout中的匿名函数每次执行时,访问的 i 都是当前块级作用域中的变量 i 。
什么是理论中的闭包?就是看似像闭包,其实并不是闭包。它只是类似于闭包。
function foo() { var a=2; function bar() { console.log(a); // 2 }
bar();
}
foo();
上述代码根据最上面我们对闭包的定义,它并不完全是闭包,虽然是一个函数可以访问另一个函数中的变量,但是被嵌套的函数是在当前词法作用域中被调用的。
我们怎样把上述代码foo 函数中的bar函数,在它所在的词法作用域外执行呢?
下面的代码就清晰的展示了闭包:
function foo() { var a=2; function bar() { console.log(a);
} return bar;
} var baz=foo();
baz(); // 2 —— 朋友,这就是闭包的效果。
上述代码中 bar 被当做 foo函数返回值。foo函数执行后把返回值也就是 bar函数 赋值给了全局变量 baz。当 baz 执行时,实际上也就是 bar 函数的执行。我们知道 foo 函数在执行后,foo 的内部作用域会被销毁,因为引擎有垃圾回收期来释放不再使用的内存空间。所以在bar函数执行时,实际上foo函数内部的作用域已经不存在了,理应来说 bar函数 内部再访问 a 变量时是找不到的。但是闭包的神奇之处就在这里。由于 bar 是在 foo 作用域中被声明的,所以 bar函数 会一直保存着对 foo 作用域的引用。这时就形成了闭包。
我们先看个例子:
var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope;
} return f;
} var foo = checkscope();
foo();
我们用伪代码来解释JavaScript
引擎在执行上述代码时的步骤:
JavaScript
引擎遇到可执行代码时,就会进入一个执行上下文(环境)
但是我们想一个问题,checkscope函数执行完毕,它的执行上下文从栈中弹出,也就是销毁了不存在了,f 函数还能访问包裹函数的作用域中的变量(scope)吗?答案是可以。
理由是在第6步,我们说过当checkscope 执行函数执行完毕时,它的执行上下文会从栈中弹出,此时活动对象也会被回收,按理说当 f 在访问checkscope的活动对象时是访问不到的。
其实这里还有个概念,叫做作用域链:当 checkscope 函数被创建时,会创建对应的作用域链,里面值存放着包裹它的作用域对应执行上下文的变量对象,在这里只是全局执行上下文的变量对象,当checkscope执行时,此时的作用域链变化了 ,里面存放的是变量对象(活动对象)的集合,最顶端是当前函数的执行上下文的活动对象。端是全局执行上下文的变量对象。类似于:
checkscope.scopeChain = [
checkscope.AO
global.VO
]
当checkscope执行碰到了 f 函数的创建,因此 f 函数也会创建对应的作用域链,默认以包裹它的函数执行时对应的作用域链为基础。因此此时 f 函数创建时的作用域链如下:
checkscope.scopeChain = [
checkscope.AO
global.VO
]
当 f 函数执行时,此时的作用域链变化如下:
checkscope.scopeChain = [
f.AO
checkscope.AO
global.VO
]
当checkscope函数执行完毕,内部作用域会被回收,但是 f函数 的作用域链还是存在的,里面存放着 checkscope函数的活动对象,因此在f函数执行时会从作用域链中查找内部使用的 scope 标识符,从而在作用域链的第二位找到了,也就是在 checkscope.AO 找到了变量scope的值。
正是因为JavaScript
做到了这一点,因此才会有闭包的概念。还有人说闭包并不是为了拥有它采取设计它的,而是设计作用域链时的副作用产物。
闭包是JavaScript
中最难的点,也是平常面试中常问的问题,我们必须要真正的去理解它,如果只靠死记硬背是经不起考验的。
你知道吗?视力,听力和行动能力完全健康的人,可以轻松地读写,可以有效执行多任务,并且始终可以正常工作的人约占总人口的50%?其余的人都是戴着眼镜或有色盲,手腕或耳朵受伤,生活在嘈杂的环境中或网络信号质量差,忙碌或忙碌中,阅读障碍或有注意力障碍等。
这意味着大约一半的用户可能很难使用我们的产品或浏览我们的网站。因此,我们可能错过了提高用户满意度并扩大受众范围的机会。
不过在设计阶段实施一些简单的原则就可以改善交互和整体用户体验,极限设计可以为所有人带来价值,我们称之为“包容性设计”。
什么是包容性设计?包容性设计考虑了尽可能多的人的需求和能力,而不仅仅是针对残疾人。它认识到我们的需求会随着时间和环境的变化而变化,因此它会预测错误,挣扎和不同的交互方式。它的目的是在问题发生之前解决问题,提高标准并改变良好产品设计的标准。
包容的用户界面是善解人意,有意识且可访问的。年龄,性别,教育程度,财富和能力等不同特征,在不同环境中生活或工作,获得技术水平不同的不同人群可以舒适地使用它。
我们将使用POUR作为在用户与界面之间创建简单,轻松,快速交互的参考。
POUR代表
可以理解:数字内容可以轻松地以不同方式进行解释或处理吗?
可操作:数字产品能否轻松自如地进行功能和控制?
可以理解:用户可以理解界面的功能和内部信息吗?
健壮性:数字产品是否与不同的辅助技术和设备兼容?
设计师如何提供帮助
作为设计师,我们当然不能控制以上所有要求都能做到。但是我们应该承认,人们遇到的许多可访问性问题是由设计阶段未做过的决定引起的。因此,设计师有很多机会可以有所作为。仅通过做出更明智的设计决策,我们就可以影响(改进或协助)四种经验。
视觉体验:这包括形状,颜色,对比,文本样式-产品界面的所有图形元素。
听觉体验:这是指与产品互动时产生的声音,音量和清晰度。
认知经验:这描述了用户花费在解释界面上的时间,以及使用界面需要多少注意力和精力。
运动体验:这包括执行任务或与产品交互所需的所有动作和动作。
通常,可访问性被认为是对创造力的挑战;但是,如果我们认为这是一个创造性的挑战,那么我们会开辟全新的可能性领域。真正好的可访问性的诀窍不是在功能或功能上进行折衷,也不是在美学上取舍,而是使功能和创意体验也可以访问。
改善视觉体验
1.颜色
对比度对比度是亮度或颜色的差异,使物体从周围环境中脱颖而出,并可能对清晰度产生显着影响。高对比度使视觉元素从背景中脱颖而出,更加引人注目。
专家提示:纯粹的#000000黑白色会给眼睛带来强烈的对比度,甚至会影响阅读障碍者。这就是为什么我们倾向于避免使用它,而是选择深灰色的原因。
亮度
亮度描述从光源发出的照明水平或从表面反射的光量。明亮的颜色反射更多的光线,并会干扰我们阅读和处理信息的能力。
避免在背景或较大表面上使用鲜艳的颜色。请勿在文本上或文本附近使用鲜艳的颜色,以免干扰文本。如果品牌要求特定的高亮度颜色,请尝试建议使用饱和或较深的颜色。如果你绝对必须使用明亮的颜色,则应将其用于突出显示动作的方法最小化,并将其与较深的色相搭配以达到平衡和高对比度。
专家提示:任何含有超过50%黄色的颜色都会自然反射更多的光。这意味着黄色,橙色,绿色和蓝绿色是高风险颜色,应谨慎使用。
色盲
色盲是无法区分特定颜色(通常是红色和绿色,偶尔是蓝色)的一种,它比你想象的要常见。
专家提示:不要仅仅依靠颜色;颜色不应该是传达重要信息的唯一方法。您可以执行以下操作:
使用下划线表示链接和斜体,或使用粗体突出显示文本
将图标与文本一起使用可传达成功或失败的信息
使用纹理或图案作为图表
为按钮或明显的通知使用清晰的视觉样式,针对焦点或活动状态使用不同的字体样式
2.版式
字体选择
通信是每个数字产品的首要目标,可以借助印刷术及其正确应用来实现。内容应清晰易读,这意味着易于识别和解释,轻松阅读和处理。
简洁明了对于快速阅读和解释至关重要,请避免使用复杂的字体,因为它们只会增加视觉干扰。选择正确的字体家族,针对那些具有清晰定义和独特形状的字符,因为视力障碍或阅读障碍的人可能会因某些字符或其组合而感到困惑。
字体样式
字体样式还会影响弱视或阅读障碍者的阅读性能。我们应该注意并谨慎使用字体样式(如斜体,下划线和大写)的频率和位置。
根据“英国阅读障碍协会”的规定,应避免使用斜体,特别是对于较大的副本块或较小的字体。这是因为它们使字母倾斜,显得更加尖锐,因此更难以阅读。
正文也应避免使用带下划线的字体样式。给长的段落加下划线会增加视觉噪音,从而降低阅读性能,而给短的句子或单词加下划线会与活动链接相关联,并可能引起混乱。粗体是添加对比度和强调的更好选择。
尽管没有确凿的研究,但有一些证据支持也应避免主要针对正文使用大写字母。似乎所有大写字母的统一外观会降低单词形状的对比度,从而使扫描变得不那么容易。此外,大写看起来有点紧张,可能感觉好像有人在向您大喊大叫。
专家提示:平衡是关键。谨慎使用每个样式并赋予其含义甚至可以提高可读性。
字体大小
您知道绝大多数人戴眼镜或隐形眼镜吗?实际上,十分之六以上!此外,约有62%的人通过手机访问互联网,这还不包括应用程序的使用情况。当视力不佳的人在旅途中在小屏幕上使用技术时,可能会出什么问题?
使用较大的字体。通常,16px被认为是最具有包容性的,但是请注意,字体可以以不同的比例站立,并且字体的大小可以相差很大。切勿低于14px,事实上,大多数现代网站的正文都使用18px字体,而标签,标题或工具提示仅使用14px或16px。
专家提示:此外,避免使用薄而轻的字体,因为对于较小的字体或在明亮的光线下可能难以阅读。
段落格式
帮助人们轻松浏览内容应该是我们的首要目标,因为只有20%的人可以阅读内容,其中55%的人可以快速浏览内容。我们的工作是通过使用舒适的段落格式来尽可能地支持人们。
研究表明,用于支持可读性的平均在线行长(包括空格)约为70个字符。标题,字幕和项目符号点将有助于扫描,而左段对齐将使文本更易于阅读。
较长的文字墙使人们参与的机会大大减少。成功的段落长度不超过5到6个句子。
空格将帮助患有认知和注意力障碍的人,保持阅读重点。对于其余的内容,它只会使阅读更加愉快和流畅。根据WCAG,最佳做法是将行高(行之间的间距)设置为相对于该类型大小的1.5相对值。段落之间的间距也至少应比行间距大1.5倍,因此必须明确定义。
提示:行距不应超过2.0,因为它可能产生相反的效果并分散读者注意力。
复制版面
作为设计师,我们经常陷入过度设计布局的陷阱,以使它们看起来引人注目或独特,从而将可用性放在一边。这就是为什么我们看到诸如文本的一部分之类的趋势在彩色或带纹理的背景上重叠图像或文本的趋势。只要我们知道如何以及何时使用它们,我们仍然可以享受其中的一些趋势。
当在彩色或带纹理的背景上使用文本时,我们需要确保它们之间的色彩对比度足够高,同时在整个重叠区域都保持一致-意味着在副本下没有较浅和较暗的区域,也没有过多的细节干扰。较大的字体大小和较重的字体粗细也会提高对比度。
专家提示:一如既往地“了解您的用户”。时髦的布局并不适合所有人。
改善听觉体验
您可能在想,视觉设计如何影响听觉体验?因此,想象一下您正在与一个俱乐部的朋友交谈。我敢打赌,您只能听见她说的话的一半,但是您可以通过看着她的嘴唇移动,肢体语言和面部表情来保持对话的进行。由于视觉效果的支持增强了模棱两可的声音,因此您最终可以理解它们。
在用户界面中,声音对于不同的人可能意味着各种各样的事情。它们也很容易在嘈杂的背景中丢失,因此最好以视觉提示来支持它们。
我们的目标应该是提供听觉和视觉提示的反馈,支持错误,通知以及与相关和邻近图形元素的重大交互。我们还必须确保视觉线索保持足够长的活动时间,以使人们能够看到和阅读,同时又不隐藏任何重要的内容。
一个好的做法-不限于支持声音辅助技术,是在UI元素中添加描述性标签,并在图像中添加标题,以便于在屏幕阅读器中轻松导航。为视频使用字幕是改善听力体验的另一种方法,对非母语人士也有帮助。
最后,我们不应该忽略声音是问题的情况,这就是为什么我们需要视觉替代的原因。有些人可能对特定的声音敏感,或者处于声音可能引起干扰的情况下。然后,这是一个好习惯,让人们可以选择关闭声音而不必调低扬声器音量,从而使此功能清晰可见。
专家提示:避免使用不必要的自动播放声音和音乐,因为它们会打扰甚至惊吓别人。
改善认知体验
1.知觉
视觉清晰度
清晰度是用户界面中的第一个也是最重要的属性。成功的用户界面使用户能够识别和解释他们所看到的内容,了解产品的价值和所要采取的行动,预测使用产品时会发生什么以及与产品成功交互。
形式跟随功能是一项原则,指出对象应反映其预期的功能或目的。为了在用户界面中实现此目的,我们使用了附加功能,附加到UI的视觉提示/属性,以显示用户与其交互的可能方式。
支付能力取决于用户的身体能力,目标,过去的经验,当然还取决于他们认为可能的情况。按钮应该看起来像按钮,就像链接,菜单标签,表单等一样。使用清晰的符号/功能可以帮助用户识别或解释界面,并轻松进行交互。
在用户界面中使用熟悉的和已建立的设计解决方案将帮助用户预测结果并自信地采取行动。因此,使用设计模式来解决常见问题是一个好习惯,该设计模式是经过测试,优化和可重用的解决方案。
设计模式建立在过去的经验和可能性的基础上,并附加到特定的目标上。为避免眼前的问题,选择正确的设计模式应该是我们避免混淆或压力大的交互的第一要务。
建立一致的视觉语言是获得更全面界面的关键。具有相同功能和/或重要性的重复交互式UI组件应始终以相同的方式外观和操作。因此,导航,按钮,链接,标签,错误等元素应在整个产品中具有一致的样式,颜色和动画。
值得注意的是,一致的视觉语言不仅可以通过附加含义和减少视觉噪音来帮助互动,而且还可以增强产品的个性,提升品牌知名度,建立情感联系和信任。
层次结构
视觉层次结构是指图形元素的视觉重量及其排列方式,使用户可以轻松地探索和发现内容。通过为页面元素分配不同的视觉权重,我们可以对内容进行分组并影响人们感知信息和浏览产品的顺序。
颜色是第一大关注焦点。彩色元素将脱颖而出,因此在层次结构中位于较高位置。明亮的颜色会更加突出,因此,考虑到这一点,我们应该仔细安排和分配颜色,以将眼睛引导至正确的位置。
视觉元素的大小(例如印刷,按钮,图标和图像)在确定重要性方面几乎与颜色一样强大。较大的图形吸引了用户的注意,并且显得很重要。对于排版,明显不同的尺寸缩放比例可以帮助建立内容层次结构,并使内容扫描变得轻松而轻松。
辅助视觉层次结构的另一种方法是通过设计一致性和例外。一致对齐,外观相似,重复或相邻的元素给人的印象是它们是相关且同等重要的,而偏离元素以及不寻常的形状和有趣的纹理或样式将更加显着。太多的设计例外会引起人们的关注,并会增加复杂性,因此,谨慎使用它们是一个好习惯。
专家提示:研究格式塔原理及其在UI设计中的应用将有助于我们理解视觉感知和分组以改善视觉层次。
色彩应用
颜色不应该是传达信息或增加意义的唯一方法,但它仍然有用且很有影响力,因此不应将其视为装饰性元素。颜色具有含义,尽管没有硬性规定,但是太多的颜色会导致信息疲劳,并且不一致地使用颜色会导致混乱。
避免使用太多颜色。通常,三种颜色足以描述页面的所有重要视觉元素。60–30–10规则可以帮助我们建立完美的和谐。其中60%的彩色项目由原色组成,以创建统一的产品主题,具有30%的辅助颜色增强含义和/或创建引人注目的效果,以及10%的强调色,以补充和辅助主颜色和辅助颜色。
此外,我们需要确保为消息使用正确的色调。除了美学,颜色还可以创造情感和无意识的联系。特定阴影的含义会因我们所处的文化和环境而异,并且颜色通常具有不同的含义-在西方世界,错误是红色,成功是绿色,信息是蓝色等。
专家提示:可以将我们自己的含义分配给颜色,只要它们不与既定规范重叠,并且我们在整个产品中使它们保持一致。
符号学
符号学是对符号/图标及其含义的研究。它着重于人们如何形成和解释这些含义,这取决于人们所看到的上下文。在用户界面中,图标是可视语言的一部分,用于表示功能,功能或内容。符号学可以帮助我们设计立即被识别和理解的图像。
尽管这些年来,我们已经开发出具有大多数人接受和理解的含义的图标。用户还习惯于使用特定于平台的图标,并且可以轻松地进行解释。在可能的情况下,最好遵循这些既定的解决方案,以获得熟悉和流畅的体验。
当然,在某些情况下,我们需要设计具有特定功能的自定义产品特定图标。这些图标必须尽可能简单明了,以确保清晰度。它们还应该具有一致的视觉样式,以传达其功能或与其他非功能性元素区分开。
最后,我们不应该仅仅依靠视觉隐喻来传达含义,因为某些关联可能并不那么明显。如果图标需要标题来描述其含义,则可能不合适。如果不确定,请与实际用户一起测试我们的设计会有所帮助。
专家提示:图标不仅易于解释,而且还可以具有多种含义。因此,将标记与功能图标结合使用是一种很好的做法。
2.互动
记忆
许多心理学实验表明,健康个体的处理能力非常有限。在我们的短期记忆中,我们大多数人平均可以保留7项,具体取决于个人。我们的大脑并未针对数字产品所需的抽象思维和数据记忆进行优化,因此良好的设计会有所作为。
减少页面上可用选项和信息的数量,以及使用清晰的标题,面包屑和“后退”选项来访问以前的内容,将帮助用户记住或提醒自己他们在哪里,打算做什么或要做什么。是必需的。
交互元素上或附近的清晰可见副本将帮助用户在整个交互过程中保持知情和自信。例如,表单标签应始终可见,动作不应隐藏在悬停后面,按钮应提供目标位置的上下文,并且各节的标题应明确。
专家提示:通过称为“块”的过程可以增加我们的短期记忆和处理能力。这是我们在视觉上将项目分组以形成更容易记住的较大项目的地方。
改善运动体验
菲茨法
菲茨法则为人类的运动和互动提供了一个模型。它指出,将指针(光标或手指)快速移动到目标区域所需的时间是其距目标的距离除以目标大小的函数。意味着较小的目标会增加互动时间。
根据Fitts法则,我们旨在减小用户与目标之间的距离,同时增加其尺寸。该法律主要适用于导航和按钮。菜单和子菜单元素应在附近,而按钮,链接和分页应在较大区域上单击,以实现更快更准确的交互。
专家提示:根据可用性最佳实践,按钮/链接的最小尺寸为42x42像素(重击尺寸)。
奖励:提高绩效
到目前为止,我们已经建立了包容性设计,旨在让尽可能多的人访问并实现他们的目标或解决他们的问题,尽管他们有自己的情况。我们可能很幸运,可以使用进的设备或超高速互联网,但是当我们的信号不太好时,我们会感到挣扎。对于大多数人来说,老式设备和糟糕的互联网已成为常态,因此,为获得最佳性能而设计是一件大事。
极简主义是关键。如果我们打算创造一种可以被尽可能多的人使用的产品,那么我们就应该摆脱不必要的一切。图形,图像或动画是有价值的,还是增加了视觉噪音和加载时间?如果是的话,那就必须走了。
图像优化是帮助提高数字产品性能的另一个标准。通过将图像调整为合适的大小,然后通过诸如ImageOptim和TinyPNG之类的工具运行它们,可以节省宝贵的千字节和实际的加载时间。
开发人员通常使用的一种提高性能的技术是“延迟加载”模式,其中图像的加载是异步的,并延迟到需要时才加载。例如,如果您快速滚动到页面底部,则在网站完全加载之前,您可能会看到类似网站线框的内容。“渐进图像加载”的一种替代方法是“渐进图像加载”,其中我们显示一个空的占位符框<div>,然后用小的低质量模糊图像填充它,最后用所需的高质量图像替换它。
在每个数字产品中都遵循上述最佳实践,这是高可访问性标准的良好起点。但是总会有改进的余地,并且更好地了解我们的用户可以揭示提高无障碍标准的新方法。因此,有必要花费一些时间和金钱来更多地了解我们的不同类型的用户,因为他们可以教会我们很多有关使包容性体验成为现实的知识。
了解我们的用户将帮助我们练习同理心。“赋权”不是偶然的设计思维过程的第一步。在移情阶段,我们的目标是加深对我们正在设计的人员及其独特视角的了解,因此我们可以在进行任何设计决策时与他们认同并代表他们。
zhuanz
this
this是我们在书写代码时最常用的关键词之一,即使如此,它也是JavaScript最容易被最头疼的关键词。那么this到底是什么呢?
如果你了解执行上下文,那么你就会知道,其实this是执行上下文对象的一个属性:
executionContext = {
scopeChain:[ ... ],
VO:{
...
},
this: ?
}
执行上下文中有三个重要的属性,作用域链(scopeChain)、变量对象(VO)和this。
this是在进入执行上下文时确定的,也就是在函数执行时才确定,并且在运行期间不允许修改并且是永久不变的
在全局代码中的this
在全局代码中this 是不变的,this始终是全局对象本身。
var a = 10;
this.b = 20;
window.c = 30;
console.log(this.a);
console.log(b);
console.log(this.c);
console.log(this === window) // true
// 由于this就是全局对象window,所以上述 a ,b ,c 都相当于在全局对象上添加相应的属性
如果我们在代码运行期尝试修改this的值,就会抛出错误:
this = { a : 1 } ; // Uncaught SyntaxError: Invalid left-hand side in assignment
console.log(this === window) // true
函数代码中的this
在函数代码中使用this,才是令我们最容易困惑的,这里我们主要是对函数代码中的this进行分析。
我们在上面说过this的值是,进入当前执行上下文时确定的,也就是在函数执行时并且是执行前确定的。但是同一个函数,作用域中的this指向可能完全不同,但是不管怎样,函数在运行时的this的指向是不变的,而且不能被赋值。
function foo() {
console.log(this);
}
foo(); // window
var obj={
a: 1,
bar: foo,
}
obj.bar(); // obj
函数中this的指向丰富的多,它可以是全局对象、当前对象、或者是任意对象,当然这取决于函数的调用方式。在JavaScript中函数的调用方式有一下几种方式:作为函数调用、作为对象属性调用、作为构造函数调用、使用apply或call调用。下面我们将按照这几种调用方式一一讨论this的含义。
作为函数调用
什么是作为函数调用:就是独立的函数调用,不加任何修饰符。
function foo(){
console.log(this === window); // true
this.a = 1;
console.log(b); // 2
}
var b = 2;
foo();
console.log(a); // 1
上述代码中this绑定到了全局对象window。this.a相当于在全局对象上添加一个属性 a 。
在严格模式下,独立函数调用,this的绑定不再是window,而是undefined。
function foo() {
"use strict";
console.log(this===window); // false
console.log(this===undefined); // true
}
foo();
这里要注意,如果函数调用在严格模式下,而内部代码执行在非严格模式下,this 还是会默认绑定为 window。
function foo() {
console.log(this===window); // true
}
(function() {
"use strict";
foo();
})()
对于在函数内部的函数独立调用 this 又指向了谁呢?
function foo() {
function bar() {
this.a=1;
console.log(this===window); // true
}
bar()
}
foo();
console.log(a); // 1
上述代码中,在函数内部的函数独立调用,此时this还是被绑定到了window。
总结:当函数作为独立函数被调用时,内部this被默认绑定为(指向)全局对象window,但是在严格模式下会有区别,在严格模式下this被绑定为undefined。
作为对象属性调用
var a=1;
var obj={
a: 2,
foo: function() {
console.log(this===obj); // true
console.log(this.a); // 2
}
}
obj.foo();
上述代码中 foo属性的值为一个函数。这里称 foo 为 对象obj 的方法。foo的调用方式为 对象 . 方法 调用。此时 this 被绑定到当前调用方法的对象。在这里为 obj 对象。
再看一个例子:
var a=1;
var obj={
a: 2,
bar: {
a: 3,
foo: function() {
console.log(this===bar); // true
console.log(this.a); // 3
}
}
}
obj.bar.foo();
遵循上面说的规则 对象 . 属性 。这里的对象为 obj.bar 。此时 foo 内部this被绑定到了 obj.bar 。 因此 this.a 即为 obj.bar.a 。
再来看一个例子:
var a=1;
var obj={
a: 2,
foo: function() {
console.log(this===obj); // false
console.log(this===window); // true
console.log(this.a); // 1
}
}
var baz=obj.foo;
baz();
这里 foo 函数虽然作为对象obj 的方法。但是它被赋值给变量 baz 。当baz调用时,相当于 foo 函数独立调用,因此内部 this被绑定到 window。
使用apply或call调用
apply和call为函数原型上的方法。它可以更改函数内部this的指向。
var a=1;
function foo() {
console.log(this.a);
}
var obj1={
a: 2
}
var obj2={
a: 3
}
var obj3={
a: 4
}
var bar=foo.bind(obj1);
bar();// 2 this => obj1
foo(); // 1 this => window
foo.call(obj2); // 3 this => obj2
foo.call(obj3); // 4 this => obj3
当函数foo 作为独立函数调用时,this被绑定到了全局对象window,当使用bind、call或者apply方法调用时,this 被分别绑定到了不同的对象。
作为构造函数调用
var a=1;
function Person() {
this.a=2; // this => p;
}
var p=new Person();
console.log(p.a); // 2
上述代码中,构造函数 Person 内部的 this 被绑定为 Person的一个实例。
总结:
当我们要判断当前函数内部的this绑定,可以依照下面的原则:
函数是否在是通过 new 操作符调用?如果是,this 绑定为新创建的对象
var bar = new foo(); // this => bar;
函数是否通过call或者apply调用?如果是,this 绑定为指定的对象
foo.call(obj1); // this => obj1;
foo.apply(obj2); // this => obj2;
函数是否通过 对象 . 方法调用?如果是,this 绑定为当前对象
obj.foo(); // this => obj;
函数是否独立调用?如果是,this 绑定为全局对象。
foo(); // this => window
DOM事件处理函数中的this
1). 事件绑定
<button id="btn">点击我</button>
// 事件绑定
function handleClick(e) {
console.log(this); // <button id="btn">点击我</button>
}
document.getElementById('btn').addEventListener('click',handleClick,false); // <button id="btn">点击我</button>
document.getElementById('btn').onclick= handleClick; // <button id="btn">点击我</button>
根据上述代码我们可以得出:当通过事件绑定来给DOM元素添加事件,事件将被绑定为当前DOM对象。
2).内联事件
<button onclick="handleClick()" id="btn1">点击我</button>
<button onclick="console.log(this)" id="btn2">点击我</button>
function handleClick(e) {
console.log(this); // window
}
//第二个 button 打印的是 <button id="btn">点击我</button>
我认为内联事件可以这样理解:
//伪代码
<button onclick=function(){ handleClick() } id="btn1">点击我</button>
<button onclick=function() { console.log(this) } id="btn2">点击我</button>
这样我们就能理解上述代码中为什么内联事件一个指向window,一个指向当前DOM元素。(当然浏览器处理内联事件时并不是这样的)
定时器中的this
定时器中的 this 指向哪里呢?
function foo() {
setTimeout(function() {
console.log(this); // window
},1000)
}
foo();
再来看一个例子
var name="chen";
var obj={
name: "erdong",
foo: function() {
console.log(this.name); // erdong
setTimeout(function() {
console.log(this.name); // chen
},1000)
}
}
obj.foo();
到这里我们可以看到,函数 foo 内部this指向为调用它的对象,即:obj 。定时器中的this指向为 window。那么有什么办法让定时器中的this跟包裹它的函数绑定为同一个对象呢?
1). 利用闭包:
var name="chen";
var obj={
name: "erdong",
foo: function() {
console.log(this.name) // erdong
var that=this;
setTimeout(function() {
// that => obj
console.log(that.name); // erdong
},1000)
}
}
obj.foo();
利用闭包的特性,函数内部的函数可以访问含义访问当前词法作用域中的变量,此时定时器中的 that 即为包裹它的函数中的 this 绑定的对象。在下面我们会介绍利用 ES6的箭头函数实现这一功能。
当然这里也可以适用bind来实现:
var name="chen";
var obj={
name: "erdong",
foo: function() {
console.log(this.name); // erdong
setTimeout(function() {
// this => obj
console.log(this.name); // erdong
}.bind(this),1000)
}
}
obj.foo();
被忽略的this
如果你把 null 或者 undefined 作为 this 的绑定对象传入 call 、apply或者bind,这些值在调用时会被忽略,实例 this 被绑定为对应上述规则。
var a=1;
function foo() {
console.log(this.a); // 1 this => window
}
var obj={
a: 2
}
foo.call(null);
var a=1;
function foo() {
console.log(this.a); // 1 this => window
}
var obj={
a: 2
}
foo.apply(null);
var a=1;
function foo() {
console.log(this.a); // 1 this => window
}
var obj={
a: 2
}
var bar = foo.bind(null);
bar();
bind 也可以实现函数柯里化:
function foo(a,b) {
console.log(a,b); // 2 3
}
var bar=foo.bind(null,2);
bar(3);
更复杂的例子:
var foo={
bar: function() {
console.log(this);
}
};
foo.bar(); // foo
(foo.bar)(); // foo
(foo.bar=foo.bar)(); // window
(false||foo.bar)(); // window
(foo.bar,foo.bar)(); // window
上述代码中:
foo.bar()为对象的方法调用,因此 this 绑定为 foo 对象。
(foo.bar)() 前一个() 中的内容不计算,因此还是 foo.bar()
(foo.bar=foo.bar)() 前一个 () 中的内容计算后为 function() { console.log(this); } 所以这里为匿名函数自执行,因此 this 绑定为 全局对象 window
后面两个实例同上。
这样理解会比较好:
(foo.bar=foo.bar) 括号中的表达式执行为 先计算,再赋值,再返回值。
(false||foo.bar)() 括号中的表达式执行为 判断前者是否为 true ,若为true,不计算后者,若为false,计算后者并返回后者的值。
(foo.bar,foo.bar) 括号中的表达式之行为分别计算 “,” 操作符两边,然后返回 “,” 操作符后面的值。
箭头函数中的this
箭头函数时ES6新增的语法。
有两个作用:
更简洁的函数
本身不绑定this
代码格式为:
// 普通函数
function foo(a){
// ......
}
//箭头函数
var foo = a => {
// ......
}
//如果没有参数或者参数为多个
var foo = (a,b,c,d) => {
// ......
}
我们在使用普通函数之前对于函数的this绑定,需要根据这个函数如何被调用来确定其内部this的绑定对象。而且常常因为调用链的数量或者是找不到其真正的调用者对 this 的指向模糊不清。在箭头函数出现后其内部的 this 指向不需要再依靠调用的方式来确定。
箭头函数有几个特点(与普通函数的区别)
箭头函数不绑定 this 。它只会从作用域链的上一层继承 this。
箭头函数不绑定arguments,使用reset参数来获取实参的数量。
箭头函数是匿名函数,不能作为构造函数。
箭头函数没有prototype属性。
不能使用 yield 关键字,因此箭头函数不能作为函数生成器。
这里我们只讨论箭头函数中的this绑定。
用一个例子来对比普通函数与箭头函数中的this绑定:
var obj={
foo: function() {
console.log(this); // obj
},
bar: () => {
console.log(this); // window
}
}
obj.foo();
obj.bar();
上述代码中,同样是通过对象 . 方法调用一个函数,但是函数内部this绑定确是不同,只因一个数普通函数一个是箭头函数。
用一句话来总结箭头函数中的this绑定:
个人上面说的它会从作用域链的上一层继承 this ,说法并不是很正确。作用域中存放的是这个函数当前执行上下文与所有父级执行上下文的变量对象的集合。因此在作用域链中并不存在 this 。应该说是作用域链上一层对应的执行上下文中继承 this 。
箭头函数中的this继承于作用域链上一层对应的执行上下文中的this
var obj={
foo: function() {
console.log(this); // obj
},
bar: () => {
console.log(this); // window
}
}
obj.bar();
上述代码中obj.bar执行时的作用域链为:
scopeChain = [
obj.bar.AO,
global.VO
]
根据上面的规则,此时bar函数中的this指向为全局执行上下文中的this,即:window。
再来看一个例子:
var obj={
foo: function() {
console.log(this); // obj
var bar=() => {
console.log(this); // obj
}
bar();
}
}
obj.foo();
在普通函数中,bar 执行时内部this被绑定为全局对象,因为它是作为独立函数调用。但是在箭头函数中呢,它却绑定为 obj 。跟父级函数中的 this 绑定为同一对象。
此时它的作用域链为:
scopeChain = [
bar.AO,
obj.foo.AO,
global.VO
]
这个时候我们就差不多知道了箭头函数中的this绑定。
继续看例子:
var obj={
foo: () => {
console.log(this); // window
var bar=() => {
console.log(this); // window
}
bar();
}
}
obj.foo();
这个时候怎么又指向了window了呢?
我们还看当 bar 执行时的作用域链:
scopeChain = [
bar.AO,
obj.foo.AO,
global.VO
]
当我们找bar函数中的this绑定时,就会去找foo函数中的this绑定。因为它是继承于它的。这时 foo 函数也是箭头函数,此时foo中的this绑定为window而不是调用它的obj对象。因此 bar函数中的this绑定也为全局对象window。
我们在回头看上面关于定时器中的this的例子:
var name="chen";
var obj={
name: "erdong",
foo: function() {
console.log(this.name); // erdong
setTimeout(function() {
console.log(this); // chen
},1000)
}
}
obj.foo();
这时我们就可以很简单的让定时器中的this与foo中的this绑定为同一对象:
var name="chen";
var obj={
name: "erdong",
foo: function() {
// this => obj
console.log(this.name); // erdong
setTimeout(() => {
// this => foo中的this => obj
console.log(this.name); // erdong
},1000)
}
}
obj.foo();
5G以其更快的速度、连接和云访问,将大数据引进车内
在MWC上,华为、小米、三星等通讯企业纷纷发布了5G手机,而吉利也在2月26日的MWC上,宣布了与高通和高新兴合作发布吉利全球首批支持5G和C-V2X的量产车型计划。可见5G时代对于各大车企来说有着巨大的影响,尤其是车联网产业。那么,在5G环境下,汽车行业究竟有什么变化呢?
5G是一代的移动网络,凭借高带宽和低延迟,提供了更快的速度、连接和云访问。5G的最大速度可达到每秒20GB,比4G要快100倍。它可以应用于手机、无人驾驶、VR、电影、充电桩、医疗、农业等多个领域。而基于5G通讯技术推出的C-V2X,是实现无人驾驶和车内技术的重要前提。
万物互联
万物互联是自动驾驶汽车发展的关键,基于5G通讯技术推出的C-V2X能让联网车辆与交通基础设施进行通信。通过5G可以实现自动驾驶汽车彼此之间所有数据的沟通与互联。并与交通灯、道路、传感器、停车场等基础设施之间的信息互联,最终实现车路协同、万物互联。
车车互联提升驾驶辅助
在5G环境下的汽车,可以通过云计算来计算车与车之间的距离、车辆的下一步动作、隐藏车辆可视化、零误差高清导航等信息。同时,也可以与其他车辆实现共享数据,提升ADAS和AEB等驾驶辅助功能,来避免车辆之间发生碰撞。
智慧交通引领场景化设计
5G环境下,大量的数据将被引入车内,来提供更准确的数据信息。如,高速收费、红绿灯、RTTI、实时车位情况、消费支付、行人检测等情况。众多的交互有助于车辆更好的了解环境信息,并作出反馈,从而提供更好的场景化设计。如,自动超车、协作式避让、自动播报前方道路拥挤程度并重新规划路线功能等场景化设计。
再比如,哈曼正在研发的交通信号灯速度优化建议技术,帮助司机根据红绿灯信息调整时速;西亚特测试了在交通信号灯中安装热像仪,以检测行人的动作并将数据反馈给汽车。
车、商业、家居互联改善驾驶体验
当车辆与酒店、商场、影院、餐厅、健身房、加油站、家居、充电桩等场所相连接,就需要以5g结合C-V2X技术的部署为基准。从而根据车主的需求,更快地预定房间、订餐、定电影票、充电桩、商场优惠等活动,实现终端之间更的通信。
比如,我们要去看电影,那么车辆会根据实时路况(是否拥堵、有几个红绿灯、是否有车祸)为您选择最佳的路线方案,并通知停车场到达时间,以方便确定是否有符合您时间的停车位,从而得到及时的反馈。
车载娱乐
可以说,如今我们所说的无人驾驶、车载触屏、全息投影、AR(增强现实)、VR游戏、AR-HUD、实时电影、车辆之间影片共享、移动办公、多模态交互等车载信息娱乐,都是需要在5G环境下来完成的。在未来,你可以在车内利用几秒的时间下载一部电影,也可以在车内与其他车辆之间建立网络游戏通信,实现虚拟内容与车辆运动的实时对接。
奔驰与哈曼合作开发的MBUX信息娱乐平台也引入了增强现实(AR)的车载导航系统。
MBUX信息AR车载导航系统
BMW Vision Next 100 AR-HUD
奥迪发布的“沉浸式车内娱乐系统”,让乘客在车内佩戴VR眼镜,然后车辆会根据行驶路线路况,实时匹配逼真的电竞类影片。
奥迪发布的“沉浸式车内娱乐系统”
手势交互
车载机器人
全息投影
移动办公
安全驾驶
未来的汽车将变得更加安全和,因为5G凭借其更高的带宽、最小的延迟和零误差高清导航,能够预防事故和观察到车身周围的各个角落。再结合C-V2X技术将极大地促进车辆之间、车辆与行人、道路基础设施之间的信息流动和监控车辆异常情况。从而做到提前预知危险,并迅速作出响应来提高道路安全性。另外,5G对于阻止黑客攻击和数据拦截能够得到更快响应,从而保障通讯安全。
想象一下,如果你的车可以在1毫秒内做出反应并将反馈传达给数百人,那么,危险系数就会降低很多。5G时代,端到端的时长为1毫秒,同时1平方公里内可同时连接100万个网络,足以满足智能交通和自动驾驶的要求。
无人驾驶
5G是实现无人驾驶最为关键的因素,如今,5G的成功研发有助于车辆之间大量的数据传输和存储,实现车联网的同时,也保障了车辆行驶的安全性;另一方面,5G可实现数据更快速的云访问,从而更有效地减少传感器技术的成本,最终实现无人驾驶。
总结
5G以其更快的速度、连接和云访问,将大数据引进车内。从而,在提升驾驶体验的同时,实现了汽车与万物的互联,保障了汽车驾驶中的安全性,从而为无人驾驶汽车提供了技术支撑和更便捷和的信息娱乐系统。
5G将成为未来十年全球主导的移动通信标准;
基于5G的C-V2X技术,会加快无人驾驶技术的落地;
5G环境下的车载信息娱乐系统将改善用户驾驶体验,同时催生大量的市场新机会;
共享平台下,5G将根据实时路况作出更准确的判断,从而减少时间成本和停车成本;
5G远不止应用于汽车、手机,它将应用于医疗、机器人、农作业、航空等更多方面;
5G技术为无人驾驶的实现提供了技术支撑和安全保障,同时也降低了安装传感器技术的成本;
5G实现了车与车、道路、家居、商业之间真正意义上的“车联网”,为交通提供更准确的交通信息和通信信息;
5G将会提供更多的场景,从而加快市场社会变革和新的商机,但需要大量的资金投入,能否将技术化为盈利是一大问题;
5G以毫秒的速度,提供更精细的地图数据和更高级的驾驶辅助,并作出快速的反馈,提高了驾驶的安全性,对于减少交通拥堵,提高交通效率和道路安全有着积极的作用。
转自-站酷
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务。
课程介绍
近些年,浏览器的功能越来越强大,渐渐得成为了复杂应用和图形的平台。同时,现有大多数浏览器实现了对 WebGL 的支持,但要直接使用 WebGL 相关接口进行开发,则需要学习复杂的着色器语言,且开发周期长,不利于项目的快速开发。
面对这种情况,Three.js 应运而生,它不但对 WebGL 进行了封装,将复杂的接口简单化,而且基于面向对象思维,将数据结构对象化,非常方便我们开发。Three.js 的发展十分迅速,然而配套的学习材料却比较匮乏,于是便有了当前的这个课程。
本课程作为入门课程,不会深入做源码解析,主要协助初学者了解 Three.js 的数据结构,基础 API 以及相关辅助插件的使用。帮助初学者达到快速入门的目的。
本课程共包含四大部分。
第一部分(第01-02课),入门前概述,带你初步认识 Three.js、框架选择标准、开发工具,源码获取,实现一个“Hello World”辅助工具。
第二部分(第03-08课),基础功能篇,主要包括 Object3D、Scene、Mesh、Group、Geometry、Materials、Lights、Cameras、粒子等相关功能的介绍。
第三部分(第09-15课),进阶篇,主要包括 Controls、Loaders、Animation、Tween、核心对象,与场景之间的交互以及性能优化介绍。
第四部分(第16课),实战篇,带大家利用所学知识实现一个 3D 小案例。
郑世强,现就职于上海某网络公司担任前端工程师,CSDN 博客作者,长期活跃于各大论坛,擅长前端开发、WEBGL 开发。
WebGL(Web 图形库)是一种 JavaScript API,用于在任何兼容的 Web 浏览器中呈现交互式 3D 和 2D 图形,而无需使用插件。WebGL 通过引入一个与 OpenGL ES 2.0 紧密相符合的 API,可以在 HTML5 <canvas> 元素中使用(简介引自 MDN)。
以我的理解,WebGL 给我们提供了一系列的图形接口,能够让我们通过 JavaScript 去使用 GPU 来进行浏览器图形渲染的工具。
Three.js 是一款 webGL 框架,由于其易用性被广泛应用。Three.js 在 WebGL 的 API 接口基础上,又进行的一层封装。它是由居住在西班牙巴塞罗那的程序员 Ricardo Cabbello Miguel 所开发,他更为人知的网名是 Mr.doob。
Three.js 以简单、直观的方式封装了 3D 图形编程中常用的对象。Three.js 在开发中使用了很多图形引擎的高级技巧,极大地提高了性能。另外,由于内置了很多常用对象和极易上手的工具,Three.js 的功能也非常强大。最后,Three.js 还是完全开源的,你可以在 GitHub 上找到它的源代码,并且有很多人贡献代码,帮助 Mr.doob 一起维护这个框架。
WebGL 原生 API 是一种非常低级的接口,而且还需要一些数学和图形学的相关技术。对于没有相关基础的人来说,入门真的很难,Three.js 将入门的门槛降低了一大截,对 WebGL 进行封装,简化我们创建三维动画场景的过程。只要你有一定的 JavaScript 基础,有一定的前端经验,我坚信,用不了多长时间,三维制作会变得很简单。
用最简单的一句话概括:WebGL 和 Three.js 的关系,相当于 JavaScript 和 jQuery 的关系。
Three.js 作为 WebGL 框架中的佼佼者,由于它的易用性和扩展性,使得它能够满足大部分的开发需求,Three.js 的具体功能如下:
Three.js 掩盖了 3D 渲染的细节:Three.js 将 WebGL 原生 API 的细节抽象化,将 3D 场景拆解为网格、材质和光源(即它内置了图形编程常用的一些对象种类)。
面向对象:开发者可以使用上层的 JavaScript 对象,而不是仅仅调用 JavaScript 函数。
功能非常丰富:Three.js 除封装了 WebGL 原始 API 之外,Three.js 还包含了许多实用的内置对象,可以方便地应用于游戏开发、动画制作、幻灯片制作、髙分辨率模型和一些特殊的视觉效果制作。
速度很快:Three.js 采用了 3D 图形最佳实践来保证在不失可用性的前提下,保持极高的性能。
支持交互:WebGL 本身并不提供拾取(Picking)功能(即是否知道鼠标正处于某个物体上)。而 Three.js 则固化了拾取支持,这就使得你可以轻松为你的应用添加交互功能。
包含数学库:Three.js 拥有一个强大易用的数学库,你可以在其中进行矩阵、投影和矢量运算。
内置文件格式支持:你可以使用流行的 3D 建模软件导出文本格式的文件,然后使用 Three.js 加载,也可以使用 Three.js 自己的 JSON 格式或二进制格式。
扩展性很强:为 Three.js 添加新的特性或进行自定义优化是很容易的事情。如果你需要某个特殊的数据结构,那么只需要封装到 Three.js 即可。
支持HTML5 Canvas:Three.js 不但支持 WebGL,而且还支持使用 Canvas2D、Css3D 和 SVG 进行渲染。在未兼容 WebGL 的环境中可以回退到其它的解决方案。
虽然 Three.js 的优势很大,但是它也有它的不足之处:
官网文档非常粗糙,对于新手极度不友好。
国内的相关资源匮乏。
Three.js 所有的资料都是以英文格式存在,对国内的朋友来说又提高了门槛。
Three.js 不是游戏引擎,一些游戏相关的功能没有封装在里面,如果需要相关的功能需要进行二次开发。
随着 WebGL 的迅速发展,相关的 WebGL 库也丰富起来,接下来介绍几个比较火的 WebGL 库。
Babylon.JS 是最好的 JavaScript 3D 游戏引擎,它能创建专业级三维游戏。主要以游戏开发和易用性为主。与 Three.js 之间的对比:
Three.js 比较全面,而 Babylon.js 专注于游戏方面。
Babylon.js 提供了对碰撞检测、场景重力、面向游戏的照相机,Three.js 本身不自带,需要依靠引入插件实现。
对于 WebGL 的封装,双方做得各有千秋,Three.js 浅一些,好处是易于扩展,易于向更底层学习;Babylon.js 深一些,好处是易用扩展难度大一些。
Three.js 的发展依靠社区推动,出来的比较早,发展比较成熟,Babylon.js 由微软公司在2013推出,文档和社区都比较健全,国内还不怎么火。
PlayCanvas 是一个基于 WebGL 游戏引擎的企业级开源 JavaScript 框架,它有许多的开发工具能帮你快速创建 3D 游戏。与 Three.js 之间的对比:
PlayCanvas 的优势在于它有云端的在线可视化编辑工具。
PlayCanvas 的扩展性不如 Three.js。
最主要是 PlayCanvas 不完全开源,还商业付费。
Cesium 是国外一个基于 JavaScript 编写的使用 WebGL 的地图引擎,支持 3D、2D、2.5D 形式的地图展示,可以自行绘制图形,高亮区域。与 Three.js 对比:
Cesium 是一个地图引擎,专注于 Gis,相关项目推荐使用它,其它项目还是算了。
至于库的扩展,其它的配套插件,以及周边的资源都不及Three.js。
通过以上信息我们发现,Three.js 在其库的扩展性,易用性以及功能方面有很好的优势。学习 Three.js 入门 3D 开发不但门槛低,而且学习曲线不会太陡,即使以后转向 WebGL 原生开发,也能通过 Three.js 学习到很多有用的知识。
现在最火的微信小游戏跳一跳也是在 Three.js 的基础上开发出来的。所以,Three.js 是我们必须要学的 WebGL 框架。
Three.js 可以使用 WebGL 在所有现代浏览器上渲染场景。对于旧版浏览器,尤其是 Internet Explorer 10 及更低版本,您可能需要回退到其他渲染器(CSS2DRenderer、CSS3DRenderer、SVGRenderer、CanvasRenderer)。
注意:如果您不需要支持这些旧版浏览器,则不推荐使用其他渲染器,因为它们速度较慢并且支持的功能比 WebGLRenderer 更少。
即可下载当前版本的代码及相关案例,文件下载解压后是这样的:
其中相关文件夹的内容是:
build:里面含有 Three.js 构建出来的 JavaScript 文件,可以直接引入使用,并有压缩版;
docs:Three.js 的官方文档;
editor:Three.js 的一个网页版的模型编辑器;
examples:Three.js 的官方案例,如果全都学会,必将成为大神;
src:这里面放置的全是编译 Three.js 的源文件;
test:一些官方测试代码,我们一般用不到;
utils:一些相关插件;
其他:开发环境搭建、开发所需要的文件,如果不对 Three.js 进行二次开发,用不到。
还有第三种,就是直接去 GitHub 上下载源码,和在官网上下载的代码一样。
<!DOCTYPE html><html><head> <meta charset=utf-8> <title>我的第一个Three.js案例</title> <style> body { margin: 0; } canvas { width: 100%; height: 100%; display: block; } </style></head><body onload="init()"><script src="https://cdn.bootcss.com/three.js/92/three.js"></script><script> //声明一些全局变量 var renderer, camera, scene, geometry, material, mesh; //初始化渲染器 function initRenderer() { renderer = new THREE.WebGLRenderer(); //实例化渲染器 renderer.setSize(window.innerWidth, window.innerHeight); //设置宽和高 document.body.appendChild(renderer.domElement); //添加到dom } //初始化场景 function initScene() { scene = new THREE.Scene(); //实例化场景 } //初始化相机 function initCamera() { camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 200); //实例化相机 camera.position.set(0, 0, 15); } //创建模型 function initMesh() { geometry = new THREE.BoxGeometry( 2, 2, 2 ); //创建几何体 material = new THREE.MeshNormalMaterial(); //创建材质 mesh = new THREE.Mesh( geometry, material ); //创建网格 scene.add( mesh ); //将网格添加到场景 } //运行动画 function animate() { requestAnimationFrame(animate); //循环调用函数 mesh.rotation.x += 0.01; //每帧网格模型的沿x轴旋转0.01弧度 mesh.rotation.y += 0.02; //每帧网格模型的沿y轴旋转0.02弧度 renderer.render( scene, camera ); //渲染界面 } //初始化函数,页面加载完成是调用 function init() { initRenderer(); initScene(); initCamera(); initMesh(); animate(); }</script></body></html>
请将上面的代码复制到 HTML 文件中,然后使用浏览器打开,我们就会发现下面的效果:
————————————————
版权声明:本文为CSDN博主「GitChat的博客」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/valada/java/article/details/80871701
蓝蓝设计的小编 http://www.lanlanwork.com