前端面试七
优化问题-----什么是长缓存?在webpack中如何做到长缓存优化?
浏览器在用户访问页面的时候,为了加快加载速度,会对用户访问的静态资源进行存储,但是每一次代码升级或者更新,都需要浏览器去下载新的代码,最方便和最简单的更新方式就是引入新的文件名称。在webpack中,可以在output给出输出的文件制定chunkhash,并且分离经常更新的代码和框架代码,通过NamedModulesPlugin或者HashedModulesIdsPlugin使再次打包文件名不变
47、谈谈对webpack的理解
WebPack的定义:
WebPack 是一个模块打包工具,可以使用WebPack管理模块之间的依赖,最后生成优化且合并后的静态资源。
它能够很好地管理、打包Web开发中所用到的HTML、JavaScript、CSS以及各种静态文件(图片、字体等),让开发过程更加高效。
对于不同类型的资源,webpack有对应的模块加载器
和其他的工具最大的不同在于他支持code-splitting(代码分割),模块化(AMD,ES6,CommonJs),全局解析
WebPack的原理:
webpack把项目当作一个整体,通过一个给定的的主文件,webpack将从这个文件开始找到你的项目的所有依赖文件,使用loaders处理它们,最后打包成一个或多个浏览器可识别的js文件
1)解析webpack配置参数,合并从shell传入和webpack.config.js文件里配置的参数,生成最后的配置结果
2)注册所有配置的插件,好让插件监听webpack构建生命周期的事件节点,以作出对应的反应
3)从配置的entrey入口文件,开始解析文件,构建AST语法树,找出每个文件所依赖的文件,递归下去
4)在解析文件递归过程,根据文件类型和loader配置,找出合适的loader来对文件进行转换
5)递归完后,得到每个文件的最终结果,根据entry配置生成代码块chunk
6)输出所有chunk到文件系统
webpack常用到哪些功能
1)entry:设置入口文件,它将是整个依赖关系的根
// 设置单个入口
var baseConfig = {
entry: './src/index.js'
}
// 设置多个入口文件,将entry写成一个对象
var baseConfig = {
entry: {
main: './src/index.js'
}
}
2)output:设置输出目录, 即使入口文件有多个,但是只有一个输出配置
// 入口文件只有一个
var path = require('path')
var baseConfig = {
entry: {
main: './src/index.js'
},
output: {
filename: 'main.js',
path: path.resolve('./build')
}
}
module.exports = baseConfig
// 入口文件有多个,需要使用占位符来确保输出文件的唯一性
output: {
filename: '[name].js',
path: path.resolve('./build')
}
3)设置loader extract-text-webpack-plugin
var baseConfig = {
entry: {
main: './src/index.js'
},
output: {
filename: '[name].js',
path: path.resolve('./build')
},
devServer: {
contentBase: './src',
historyApiFallBack: true,
inline: true
},
module: {
rules: [
{
test: /\.less$/,
use: [
{loader: 'style-loader'},
{loader: 'css-loader'},
{loader: 'less-loader'}
],
exclude: /node_modules/
}
]
}
}
4)Plugins-------plugins并不是直接操作单个文件,它直接对整个构建过程起作用
下面是一些常用的plugins
a)extract-text-webpack-plugin: 将入口中引用的css文件,都打包到独立的css文件中,而不是内嵌在js打包文件中
var ExtractTextPlugin = require('extract-text-webpack-plugin')
var lessRules = {
use: [
{loader: 'css-loader'},
{loader: 'less-loader'}
]
}
var baseConfig = {
// ...
module: {
rules: [
// ...
{test: /\.less$/, use: ExtractTextPlugin.extract(lessRules)}
]
},
plugins: [
new ExtractTextPlugin('main.css')
]
}
b)html-webpack-plugin: 依据一个简单的index.html模版,生成一个自动引用你打包后的js文件的新index.html
var HTMLWebpackPlugin = require('html-webpack-plugin')
var baseConfig = {
// ...
plugins: [
new HTMLWebpackPlugin()
]
}
c)hot-module-replacement-plugin: 它允许你在修改组件代码时,自动刷新实时预览修改后的结果。注意永远不要在生产环境中使用HMR。这儿说一下一般情况分为开发环境,测试环境,生产环境
5)将css,js代码合并
6)处理图片、文字等功能
7)解析jsx
8)解析bable
webpack的两大特色:
a) code splitting----代码分割(可以自动完成)
b) loader 可以处理各种类型的静态文件,并且支持串联操作
webpack 是以commonJS的形式来书写脚本,但对 AMD/CMD 的支持也很全面,方便旧项目进行代码迁移。
webpack具有requireJs和browserify的功能,但仍有很多自己的新特性:
1)对 CommonJS 、 AMD 、ES6的语法做了兼容
2) 对js、css、图片等资源文件都支持打包
3) 串联式模块加载器以及插件机制,让其具有更好的灵活性和扩展性,例如提供对CoffeeScript、ES6的支持
4) 有独立的配置文件webpack.config.js
5) 可以将代码切割成不同的chunk,实现按需加载,降低了初始化时间
6) 支持 SourceUrls 和 SourceMaps,易于调试
7) 具有强大的Plugin接口,大多是内部插件,使用起来比较灵活
8)webpack 使用异步 IO 并具有多级缓存。这使得 webpack 很快且在增量编译上更加快
webpack打包慢的原因以及解决方法:
模块太多。解决方法:webpack可以设置externals来将依赖的库指向全局变量,从而不再打包这个库。
什么是bundle,chunk,module
module是开发中的单个模块
chunk是指webpack在进行模块的依赖分析的时候,代码分割出来的代码块
bundle是由webpack打包出来的文件,chunk 打包后变成bundle
39、比较一下jQuery,Angular.js,React.js ,vue.js等框架的优缺点
jquery和vue.js的区别:
jquery到vue.js(MVVM模式)是一个思想的转变,由原来的直接操作DOM的思想,转变到直接操作数据。
● jQuery是JavaScript的一个库,将原生的js对象和事件进行封装
● 极大地简化了 JavaScript 编程,
● 随着浏览器厂商对HTML5规范统一遵循以及ECMA6在浏览器端的实现,jquery的使用率将会越来越低
jQuery是使用选择器($)选取DOM对象,对其进行赋值、取值、事件绑定等操作,其实和原生的js的区别只在于可以更方便的选取和操作DOM对象,而数据和界面是在一起的。比如需要获取label标签的内容:$("lable").val();
,它还是依赖DOM元素的值。
● vue是一个近年来才兴起的前端js框架
● vue是一套用于构建用户界面的渐进式框架
● vue是一款基于MVVM方式的轻量级的框架
● vue是一款基于数据驱动、组件化思想的框架
● vue被设计为可以自底向上、逐层应用的框架
从技术角度讲,Vue.js 专注于 MVVM 模型的 ViewModel 层(也就是vue的实例对象),它通过双向数据绑定把 View 层和ViewModel 层连接了起来,通过对数据的操作就可以完成对页面视图的渲染,对数据进行操作时不再需要引用相应的DOM对象 。vue以他独特的优势简单,快速,组合,紧凑,强大而迅速崛起。
举例子说明jquery与vue操作的区别
场景一:列表添加一个元素。vue只需要向数据message里面push一条数据即可完成添加一个li标签的操作,而jquery则需要获取dom元素节点,并对dom进行添加一个标签的操作,如果dom结构特别复杂,或者添加的元素非常复杂,则代码会变得非常复杂且阅读性低
//在vue中添加一个li
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
</head>
<body>
<div id="app">
<ul>
<!--根据数组数据自动渲染页面-->
<li v-for="item in message">{{item}}</li>
</ul>
<button @click="add">添加数据</button>
</div>
</body>
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script>
new Vue({
el: '#app',
data: {
message: ["第1条数据","第2条数据"],
i:2
},
methods:{
//向数组添加一条数据即可
add:function(){
this.i++
this.message.push("第"+this.i+"条数据")
}
}
})
</script>
</html>
//在jquery中添加一个li
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
</head>
<body>
<div id="app">
<ul id="list">
<li>第1条数据</li>
<li>第2条数据</li>
</ul>
<button id="add">添加数据</button>
</div>
</body>
<script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>
<script>
$(document).ready(function() {
var i=2;
$('#add').click(function() {
i++;
//通过dom操作在最后一个li元素后手动添加一个标签
$("#list").children("li").last().append("<li>第"+i+"条数据</li>")
});
});
</script>
场景二:控制按钮的显示隐藏。vue只需要控制属性isShow的值为true和false即可,而jquery则还是需要操作dom元素控制按钮的显示和隐藏
//Vue中控制按钮的显示与隐藏
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
</head>
<body>
<div id="app">
<ul>
<!--根据数组数据自动渲染页面-->
<li v-for="item in message">{{item}}</li>
</ul>
<button @click="add" v-show="isShow">添加数据</button>
<button @click="showButton">隐藏按钮</button>
</div>
</body>
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script>
new Vue({
el: '#app',
data: {
message: ["第1条数据","第2条数据"],
i:2,
isShow:true
},
methods:{
//向数组添加一条数据即可
add:function(){
this.i++
this.message.push("第"+this.i+"条数据")
},
//控制isShow的值即可
showButton:function(){
this.isShow=false;
}
}
})
</script>
</html>
//jquery控制按钮的显示与隐藏
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
</head>
<body>
<div id="app">
<ul id="list">
<li>第1条数据</li>
<li>第2条数据</li>
</ul>
<button id="add">添加数据</button>
<button id="showButton">隐藏按钮</button>
</div>
</body>
<script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>
<script>
$(document).ready(function() {
var i=2;
$('#add').click(function() {
i++;
//通过dom操作在最后一个li元素后手动添加一个标签
$("#list").children("li").last().append("<li>第"+i+"条数据</li>")
});
//需要手动隐藏dom元素
$("#showButton").click(function(){
$("#add").hide()
})
});
</script>
</html>
vue.js与react.js的比较
相同点:
在设计的时候都用到了component组件,props参数传递,组件之间的通信,state状态管理器,lifecircle声明周期等等
不同点:
个人认为vue和react.js最大的不同之处在于:它们对DOM的渲染方式不同,vue可以直接在vue文件中使用html标签,数据绑定时类似angular,可以进行条件渲染,而react.js则采用了jsx语法,运用虚拟DOM 的概念进行DOM对页面元素进行渲染,获取页面元素需要用ref来获取,似乎更加安全...
vue.js与AngularJS的区别
相同点:
● 都支持指令:内置指令和自定义指令
● 都支持过滤器:内置过滤器和自定义过滤器
● 都支持双向数据绑定
● 都不支持低端浏览器
不同点:
● AngularJS的学习成本高,比如增加了Dependency Injection特性,而Vue.js本身提供的API都比较简单、直观
● 在性能上,AngularJS依赖对数据做脏值检查,所以Watcher越多越慢。Vue.js使用观察者模式和异步队列更新,所有的数据都是独立触发的
对于庞大的应用来说,这个优化差异还是比较明显的。
vue.js与react.js的区别
相同点:
● React采用特殊的JSX语法,Vue.js在组件开发中也推崇编写.vue特殊文件格式,对文件内容都有一些约定,两者都需要编译后使用
● 都有组件思想:一切都是组件,组件实例之间可以嵌套
● 都提供合理的钩子函数,可以让开发者定制化地去处理需求
● 都不内置AJAX,Route等功能到核心包,而是以插件的方式加载
●在组件开发中都支持mixins的特性
不同点:
● React依赖Virtual DOM,而Vue.js使用的是DOM模板。React采用的Virtual DOM会对渲染出来的结果做脏检查
● Vue.js在模板中提供了指令,过滤器等,可以非常方便,快捷地操作DOM。
40、介绍下let,var,和const的区别
● let和const都是es6新版本的js语言规范出来的定义,在这以前定义一个变量只能用var,function
● let和const都是为了弥补var的一些缺陷而设计出来的。
● let和const 简单来说:
let修复了var的作用域的一些bug:
比如var中函数声明和变量声明会提升到作用域的顶部,但let不会。
var的作用域是函数作用域,而let的作用域是块级别(大括号括起来的内容)
const声明的变量只可以在声明时赋值,不可随意修改。实际上不能修改的是指向的地址,如果是数组或对象类型,里面的数据结构是可以改变的
let和const类型必须先声明后使用
let和const的区别
let声明的变量可以改变,值和类型都可以改变,没有限制
const声明的变量不得改变值
41、使用Promise而非回调(callbacks)优缺点是什么?
Promise的定义
● Promise,简单来说是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果
● 从语法上说,Promise是一个对象,从它可以获取异步操作的消息
● Promise提供统一的API,各种异步操作都可以用同样的方法进行处理
Promise的优点
● Promise 对象用于延迟(deferred) 计算和异步(asynchronous ) 计算,是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6将其写进了语言标准,统一了用法,原生提供了Promise对象
● 有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数,简化代码,使代码结构清晰明了
● 此外,Promise对象提供统一的接口,使得控制异步操作更加容易
Promise的缺点
● 无法取消Promise,一旦新建它,就会立即执行,无法中途取消
● 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部
● 当处于Pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)
Promise的基本用法
Promise对象是一个构造函数,用来生成Promise实例
// resolve--状态从“未完成”变为“成功”(即从 pending 变为 resolved),
// 在异步操作成功时调用,并将异步操作的结果,作为参数传递出去
// reject--状态从“未完成”变为“失败”(即从 pending 变为 rejected),
// 在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去
// Promise 新建后就会立即执行
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
// Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数
// 分别为resolve(param1)-必写,reject(param2)--可选
// param1--可以是普通变量也可以是Promise对象
// then(resolve(param1),reject(param2))
promise.then(function(value) {
// success
}, function(error) {
// failure
});
Promise的特点
a)对象的状态不受外界影响
Promise对象有3种状态
1)pending: 表示一个初始状态, 非 fulfilled 或 rejected
2)fulfilled: 成功的操作
3)rejected: 失败的操作
b)状态一旦改变,就不会再发生变化
状态的改变是单向的,只能由pending -> fulfilled pending -> rejected,只要这两种情况发生状态就凝固了,这时称为resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果
Promise对象的集中API
1)Promise.prototype.then():promise实例具有then方法,它的作用是为promise实例添加状态改变时的回调函数
then()方法的第一个参数是resolved状态的回调函数,第二个参数(可选)是rejected状态的回调函数。then方法返回的是一个新的promise实例,非原来的那个promise实例,因此可以采用链式写法,then方法之后还可调用一个then方法。第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。
var p=new Promise(function(resolve,eject){
resolve("ok");
});
p.then(function(value){console.log(val)},
function(err)(console.log(err))
);
getJSON("/posts.json").then(function(json) {
return json.post;
}).then(function(post) {
// ...
});
2)Promise.prototype.catch():这个方法是Promise.prototype.then(null,rejection)的别名,专门只能用来捕获错误信息,用于指定发生错误时的回调函数,then方法指定的回调函数,如果运行中抛出错误,也会被catch方法捕获
getJSON('/posts.json').then(function(posts) {
// ...
}).catch(function(error) {
// 处理 getJSON 和 前一个回调函数运行时发生的错误
console.log('发生错误!', error);
});
Promise 在resolve语句后面,再抛出错误,错误不会被捕获,等于没有抛出。因为 Promise 的状态一旦改变,就永久保持该状态,不会再变了;Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止
const promise = new Promise(function(resolve, reject) {
resolve('ok');
throw new Error('test');
});
promise.then(function(value) { console.log(value) }).catch(function(error) { console.log(error) });
// ok
3)Promise.resolve():将现有的对象转为Promise对象,进而可以执行这些方法
参数种类:
a) 参数是 Promise 实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例
b) 参数是一个thenable对象,将这个对象转为 Promise 对象,然后就立即执行thenable对象的then方法,状态为resolved
c) 参数是一个原始值,或者是一个不具有then方法的对象,则Promise.resolve方法返回一个新的 Promise 对象,状态为resolved
d) Promise.resolve方法允许调用时不带参数,直接返回一个resolved状态的 Promise 对象
4)Promise.reject():返回一个新的 Promise 实例,状态为rejected,Promise.reject()方法的参数,会原封不动地作为reject的理由,变成后续方法的参数
5)Promise.all():将多个promise实例,包装成一个新的promise实例,用于解决多个请求
// 参数为数组,数组里面每个元素都是Promise实例
// p1、p2、p3都是 Promise 实例
// 参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例
// p的状态由p1、p2、p3决定:只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled
// 此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
// 只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返
// 回值,会传递给p的回调函数
const p = Promise.all([p1, p2, p3]);
6)Promise.race():将多个promise实例,包装成一个新的promise实例
// 只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。
// 那个率先改变的 Promise 实例的返回值,就传递给p的回调函数
const p = Promise.race([p1, p2, p3]);
详细介绍的链接:https://www.cnblogs.com/huangzhilong/p/5358493.html
用js实现原生的Promise
41、ECMAScript5和6有什么区别?
ECMScript的介绍
ECMScript是一种由Ecma国际(前身为欧洲计算机制造商协会)制定和发布的脚本语言规范,javascript在它基础上经行了自己的封装。但通常来说,术语ECMAScript和javascript指的是同一个。业界所说的ECMAScript其实是指一种规范,或者说是一个标准。具体点来说,它其实就是一份文档,JS包含三个部分:ECMAScript(核心)、DOM(文档对象模型)、BOM(浏览器对象模型),ECMAScript是js语言的基础。
EMAScript5与6的区别
主要区别就是ES6改良了ES5的缺陷。
● ES6新增块级作用域:在块级作用域声明的变量,不会被外层所引用,也就是说不需要ES5通过函数(闭包)来解决全局变量污染的问题,let const
● 箭头函数代替词法作用域的this:在ES5中,"this"作用域会随着函数调用位置(全局/闭包),和函数调用方式(构造/普通)改变;ES6箭头函数中的"this"总是指向声明时的那个对象
● 处理函数的属性“arguments”: 在ES5中, arguments是一个类数组,可以使用length来遍历他,但却不能使用完全Array的api(slice/sort)
在ES6中,可以使用新特性rest参数,他的形式为 ...参数名,比如...args,rest是一个真数组可以调用Array所有api
● 类:从概念上讲,在 ES6 之前的 JS 中并没有和其他面向对象语言那样的“类”的概念。长时间里,人们把使用 new 关键字调用的函数(也叫构造器)当做“类”来使用。 通过原型链来实现继承(不方便)
● 严格模式:"use strict",在 ES5 中, 严格模式是可选项,但是在 ES6 中,许多特性要求必须使用严格模式。 因此大多数开发者和 babel 之类的工具默认添加 use strict 到 JS 文件的头部,以确保整个 JS 文件的代码都采用严格模式,这个习惯有助于我们写更好的 JavaScript
ES6新增的一些特性
1) 默认参数,形如下面:
var link = function( height=50,color='red' ){
.....
};
2) 模板对象,在es5中要将变量拼接在字符串中,在es6中可以使用${param}把他放在反引号里面,形如下面所述:
var name = `your name is ${first} ${last}`;
var url = `http://localhost:8080/api/message/${id}`;
3) 多行字符串,使用``
var name = `your name is jiang
and my name is keke. i am happy
to know you`;
4) 解构赋值:
5) 增强的字面量:
6) 箭头函数:箭头函数是函数的扩展中的一个扩展。箭头函数中的this总是指向声明时的变量对象,弥补了es5中this对象由运行时函数的调用方式和调用的位置决定的缺陷
7) promise对象
8) 块作用域,构造let和const,let声明的变量是有块作用域的,在es5中块作用域起不了任何 作用,var是函数作用域,const声明的变量在声明时要初始化并且变量指向的内存地址的值不能修改
9) 类,es6采用语法糖的形式实现类,类的主体包含构造函数、静态方法(类名调用)、原型方法(初始化对象调用)
10) 模块,在es6之前并不支持本地模块,使用AMD,CommonJS,RequireJS等解决方法,在ES6中可以使用模块import export操作
带栗子讲解的链接:http://www.css88.com/archives/6200
http://yijiebuyi.com/blog/e45098e9a0531cad19a3b98cfea1f032.html
https://blog.****.net/u012028371/article/details/52540619
1)块级作用域:let const ----- ES6新增的命令,用于声明变量,只在所在的代码块有效
ES5 只有全局作用域和函数作用域,没有块级作用域
在全局作用域下,let const不会添加为全局对象的属性,如果有同名只会遮蔽它而不是替换他
在js中声明的变量全部在作用域链[[Scopes]]数组里面
在全局作用域通过let const class声明的变量会保存在Script对象里面
而var function在全局作用域声明的变量会保存在全局对象Global里面,而在浏览器下,Global对象就指window对象
使用块级绑定的 最佳实践:默认使用const,只在确实需要改变变量的值时使用let
没有块级作用域的坏处:内层变量可能会覆盖外层变量;用来计数的循环变量泄露为全局变量
let声明的变量不存在变量提升,变量必须先声明后使用;在块级作用域内有效;暂时性死区(在同一作用域不允许使用未声明的变量);不允许在相同作用域内,重复声明同一个变量;
//let块级作用域
{
let a = 10;
var b = 1;
}
a // ReferenceError: a is not defined.
b // 1
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 10
//和上面分开的
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6
//for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// abc
// abc
// abc
//不存在变量提升
// var 的情况
console.log(foo); // 输出undefined
var foo = 2;
// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;
//暂时性死区,只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}
// 报错,不允许在相同作用域内,重复声明同一个变量
function func() {
let a = 10;
var a = 1;
}
// 报错
function func() {
let a = 10;
let a = 1;
}
// 报错,不能在函数内部重新声明参数
function func(arg) {
let arg; // 报错
}
function func(arg) {
{
let arg; // 不报错
}
}
为什么需要块级作用域
ES5 只有全局作用域和函数作用域
1、内层变量可能会覆盖外层变量,var声明的变量会提升
2、用来计数的循环变量泄露为全局变量,for
const声明一个只读的常量,一旦声明,常量的值就不能改变,并且在声明的时候就要初始化;在块级作用域内有效;const声明的变量不存在变量提升,变量必须先声明后使用;暂时性死区;在同一作用域不允许重复声明;const变量是引用类型,其内部属性值可以修改但是地址不能修改
const的本质:const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址不得改动
对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量,所以该常量的值不能改变
对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指针,const只能保证这个指针是固定的,至于它指向的数据结构是可以改变的
const foo = {};
// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123
// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only
ES6有6种声明变量的方法:var function let const import class
var、function命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let、const、class命令声明的全局变量,不属于顶层对象的属性
global对象:作为顶层对象,在所有环境下,global
都是存在的,都可以从它拿到顶层对象
ES5中的顶层对象不统一
1)浏览器里面,顶层对象是window,但 Node 和 Web Worker 没有window。
2)浏览器和 Web Worker 里面,self也指向顶层对象,但是 Node 没有self。
3)Node 里面,顶层对象是global,但其他环境都不支持
同一段代码为了能够在各种环境,都能取到顶层对象,现在一般是使用this变量,但是有局限性
1)全局环境中,this会返回顶层对象。但是,Node 模块和 ES6 模块中,this返回的是当前模块
2)函数里面的this,单纯作为函数运行,this会指向顶层对象。但是,严格模式下,这时this会返回undefined
如果函数作为对象的方法运行,则指向该对象
3)不管是严格模式,还是普通模式,new Function('return this')(),总是会返回全局对象
2)解构赋值:数组、对象、字符串、数值、布尔值、函数参数的解构赋值
解构的定义:ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值
属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值
数组的解构赋值
解构不成功,变量的值就等于undefined
let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []
不完全解构,即等号左边的模式,只匹配一部分的等号右边的数组,解构依然可以成功
let [x, y] = [1, 2, 3];
x // 1
y // 2
let [a, [b], d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4
如果等号的右边不是数组(或者严格地说,不是可遍历的结构,参见《Iterator》一章),那么将会报错
// 报错
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};
只要某种数据结构具有 Iterator 接口,都可以采用数组形式的解构赋值
let [x, y, z] = new Set(['a', 'b', 'c']);
x // "a"
解构赋值允许指定默认值解构赋值允许指定默认值,只有当一个数组成员严格等于undefined,默认值才会生效
let [foo = true] = [];
foo // true
let [x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'
默认值可以引用解构赋值的其他变量,但该变量必须已经声明
let [x = 1, y = x] = []; // x=1; y=1
let [x = 1, y = x] = [2]; // x=2; y=2
let [x = 1, y = x] = [1, 2]; // x=1; y=2
let [x = y, y = 1] = []; // ReferenceError: y is not defined,因为x用y做默认值时,y还没有声明
对象的解构赋值
对象的解构与数组有一个重要的不同,数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
let { bar, foo } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"
let { baz } = { foo: "aaa", bar: "bbb" };
baz // undefined
对象的解构赋值是下面形式的简写
let { foo: foo, bar: bar } = { foo: "aaa", bar: "bbb" };
对象的解构赋值的内部机制:先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者
let { foo: baz } = { foo: "aaa", bar: "bbb" }; //foo是匹配的模式,baz才是变量
baz // "aaa"
foo // error: foo is not defined
嵌套结构的对象:p是匹配模式,不是变量,不能赋值
let obj = {
p: [
'Hello',
{ y: 'World' }
]
};
let { p: [x, { y }] } = obj;
x // "Hello"
y // "World"
如果p也要作为变量赋值,可以写成下面这样:
let obj = {
p: [
'Hello',
{ y: 'World' }
]
};
let { p, p: [x, { y }] } = obj;
x // "Hello"
y // "World"
p // ["Hello", {y: "World"}]
对象的解构也可以指定默认值,生效的条件是,对象的属性值严格等于undefined
var {x = 3} = {x: undefined};
x // 3
var {x = 3} = {x: null};
x // null
ar {x = 3} = {};
x // 3
var {x, y = 5} = {x: 1};
x // 1
y // 5
var {x: y = 3} = {};
y // 3
var {x: y = 3} = {x: 5};
y // 5
var { message: msg = 'Something went wrong' } = {};
msg // "Something went wrong"
解构失败,变量的值等于undefined
let {foo} = {bar: 'baz'};
foo // undefined
解构模式是嵌套的对象,而且子对象所在的父属性不存在,那么将会报错
// 报错,等号左边对象的foo属性,对应一个子对象,foo这时等于undefined,再取子属性就会报错
let {foo: {bar}} = {baz: 'baz'};
如果要将一个已经声明的变量用于解构赋值,必须非常小心
// 错误的写法, JavaScript 引擎会将{x}理解成一个代码块,从而发生语法错误
let x;
{x} = {x: 1};
// SyntaxError: syntax error
// 正确的写法
let x;
({x} = {x: 1});
数组本质是特殊的对象,因此可以对数组进行对象属性的解构
let arr = [1, 2, 3];
let {0 : first, [arr.length - 1] : last} = arr; //方括号这种写法,属于“属性名表达式”
first // 1
last // 3
方括号这种写法,属于“属性名表达式”
first // 1
last // 3
字符串的解构:字符串被转换成了一个类似数组的对象
const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"
类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值
let {length : len} = 'hello';
len // 5
数值和布尔值的解构赋值:等号右边是数值和布尔值,则会先转为对象
let {toString: s} = 123;
s === Number.prototype.toString // true
let {toString: s} = true;
s === Boolean.prototype.toString // true
解构赋值的规则:只要等号右边的值不是对象或数组,就先将其转为对象。由于undefined和null无法转为对象,所以对它们进行解构赋值,都会报错
let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError
函数参数的解构赋值
function add([x, y]){
return x + y;
}
add([1, 2]); // 3
[[1, 2], [3, 4]].map(([a, b]) => a + b);
// [ 3, 7 ]
函数参数的解构也可以使用默认值,变量的默认参数有效的条件是对象的属性值严格等于undefined
function move({x = 0, y = 0} = {}) {
return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]
为函数move
的参数指定默认值,而不是为变量x
和y
指定默认值
function move({x, y} = { x: 0, y: 0 }) {
return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, undefined]
move({}); // [undefined, undefined]
move(); // [0, 0]
[1, undefined, 3].map((x = 'yes') => x);
// [ 1, 'yes', 3 ]
圆括号的问题:一个式子到底是模式,还是表达式,没有办法从一开始就知道,必须解析到(或解析不到)等号才能知道
ES6 的规则是,只要有可能导致解构的歧义,就不得使用圆括号
不得使用圆括号的情况
(1)变量声明语句
// 全部报错,都是变量声明语句,模式不能使用圆括号
let [(a)] = [1];
let {x: (c)} = {};
let ({x: c}) = {};
let {(x: c)} = {};
let {(x): c} = {};
let { o: ({ p: p }) } = { o: { p: 2 } };
(2)函数参数:也属于变量声明,因此不能带有圆括号
// 报错
function f([(z)]) { return z; }
// 报错
function f([z,(x)]) { return x; }
(3)赋值语句的模式
// 全部报错,上面代码将整个模式放在圆括号之中,导致报错
({ p: a }) = { p: 42 };
([a]) = [5];
// 报错,上面代码将一部分模式放在圆括号之中,导致报错
[({ p: a }), { x: c }] = [{}, {}];
可以使用圆括号的情况只有一种:赋值语句的非模式部分,可以使用圆括号
//都是赋值语句,圆括号都不属于模式的一部分
[(b)] = [3]; // 正确,模式是取数组的第一个成员,跟圆括号无关
({ p: (d) } = {}); // 正确,模式是p,而不是d
[(parseInt.prop)] = [3]; // 正确,模式是取数组的第一个成员,跟圆括号无关
解构赋值的作用
1)交换变量的值
let x = 1;
let y = 2;
[x, y] = [y, x];
console.log('x: '+ x); //x: 2
console.log('y: '+ y); //y: 1
2)从函数返回多个值
函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便
// 返回一个数组
function example() {
return [1, 2, 3];
}
let [a, b, c] = example(); //取出数组元素的 值
// 返回一个对象
function example() {
return {
foo: 1,
bar: 2
};
}
let { foo, bar } = example(); //取出对象的值
3)函数参数的定义
解构赋值可以方便地将一组参数与变量名对应起来
// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);
// 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});
4)快速提取 JSON 数据
let jsonData = {
id: 42,
status: "OK",
data: [867, 5309]
};
let { id, status, data: number } = jsonData;
console.log(id, status, number);
// 42, "OK", [867, 5309]
5)遍历Map结构
for...of配合变量的解构赋值,获取键名和键值就非常方便
const map = new Map();
map.set('first', 'hello');
map.set('second', 'world');
for (let [key, value] of map) {
console.log(key + " is " + value);
}
// first is hello
// second is world
6)输入模块的指定方法
加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰
const { SourceMapConsumer, SourceNode } = require("source-map");
3)增强的字面量对象:对象、数组、字符串、函数、数值、正则的扩展
对象的扩展
(1)属性的简洁表示法
ES6 允许直接写入变量和函数,作为对象的属性和方法,属性名为变量名, 属性值为变量的值
const foo = 'bar';
const baz = {foo};
baz // {foo: "bar"}
// 等同于
const baz = {foo: foo};
function f(x, y) {
return {x, y};
}
// 等同于
function f(x, y) {
return {x: x, y: y};
}
f(1, 2) // Object {x: 1, y: 2}
函数的简洁表示法
const o = {
method() {
return "Hello!";
}
};
// 等同于
const o = {
method: function() {
return "Hello!";
}
};
(2)属性名表达式
JavaScript 定义对象的属性,有两种方法
// 方法一:直接用标识符作为属性名
obj.foo = true;
// 方法二:用表达式作为属性名,这时要将表达式放在方括号之内
obj['a' + 'bc'] = 123;
如果使用字面量方式定义对象(使用大括号),在 ES5 中只能使用方法一(标识符)定义属性,ES6 可以用方法二(表达式)作为对象的属性名,即把表达式放在方括号内
//ES5:
var obj = {
foo: true,
abc: 123
};
//ES6
let propKey = 'foo';
let obj = {
[propKey]: true,
['a' + 'bc']: 123
};
let lastWord = 'last word';
const a = {
'first word': 'hello',
[lastWord]: 'world'
};
a['first word'] // "hello"
a[lastWord] // "world"
a['last word'] // "world"
方法名表达式
let obj = {
['h' + 'ello']() {
return 'hi';
}
};
obj.hello() // hi
属性名表达式与简洁表示法,不能同时使用,会报错
// 报错
const foo = 'bar';
const bar = 'abc';
const baz = { [foo] };
// 正确
const foo = 'bar';
const baz = { [foo]: 'abc'};
属性名表达式如果是一个对象,默认情况下会自动将对象转为字符串[object Object]
const keyA = {a: 1};
const keyB = {b: 2};
const myObject = {
[keyA]: 'valueA',
[keyB]: 'valueB'
};
//[keyA]和[keyB]得到的都是[object Object],所以[keyB]会把[keyA]覆盖掉,而myObject最后只有一个[object Object]属性
myObject // Object {[object Object]: "valueB"}
(3)方法的name属性
函数的name属性,返回函数名。对象方法也是函数,因此也有name属性
const person = {
sayName() {
console.log('hello!');
},
};
person.sayName.name // "sayName"
(4)Object.is():同值相等算法,比较两个值严格相等,但是+0
不等于-0
,NaN
等于自身
ES5 比较两个值是否相等,只有两个运算符:相等运算符(==)和严格相等运算符(===),前者会自动转换数据类型,后者的NaN不等于自身,以及+0等于-0
Object.is('foo', 'foo')
// true
Object.is({}, {})
// false
+0 === -0 //true
NaN === NaN // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
(5 )Object.assign(target,source,source,...):用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)
const target = { a: 1 };
const source1 = { b: 2 };
const source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性
const target = { a: 1, b: 1 };
const source1 = { b: 2, c: 2 };
const source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
只有一个参数,Object.assign
会直接返回该参数
const obj = {a: 1};
Object.assign(obj) === obj // true
如果该参数不是对象,则会先转成对象,然后返回
typeof Object.assign(2) // "object"
由于undefined
和null
无法转成对象,所以如果它们作为参数,就会报错
Object.assign(undefined) // 报错
Object.assign(null) // 报错
如果非对象参数出现在源对象的位置(即非首参数),如果undefined
和null
不在首参数,就不会报错
let obj = {a: 1};
Object.assign(obj, undefined) === obj // true
Object.assign(obj, null) === obj // true
注意点:浅拷贝,同名属性的替换,数组的处理,取值函数的处理
const obj1 = {a: {b: 1}};
const obj2 = Object.assign({}, obj1);
obj1.a.b = 2;
obj2.a.b // 2
const target = { a: { b: 'c', d: 'e' } }
const source = { a: { b: 'hello' } }
Object.assign(target, source)
// { a: { b: 'hello' } }
Object.assign([1, 2, 3], [4, 5])
// [4, 5, 3]
Object.assign
把数组视为属性名为 0、1、2 的对象,因此源数组的 0 号属性4
覆盖了目标数组的 0 号属性1
const source = {
get foo() { return 1 }
};
const target = {};
Object.assign(target, source)
// { foo: 1 }
Object.assign
方法用处:为对象添加属性;为对象添加方法;克隆对象;合并多个对象;为属性指定默认值
class Point {
constructor(x, y) {
Object.assign(this, {x, y});
}
}
Object.assign(SomeClass.prototype, {
someMethod(arg1, arg2) {
···
},
anotherMethod() {
···
}
});
// 等同于下面的写法
SomeClass.prototype.someMethod = function (arg1, arg2) {
···
};
SomeClass.prototype.anotherMethod = function () {
···
};
function clone(origin) {
return Object.assign({}, origin);
}
将多个对象合并到某个对象
const merge =
(target, ...sources) => Object.assign(target, ...sources);
合并后返回一个新对象
const merge =
(...sources) => Object.assign({}, ...sources);
const DEFAULTS = {
logLevel: 0,
outputFormat: 'html'
};
function processContent(options) {
options = Object.assign({}, DEFAULTS, options);
console.log(options);
// ...
}
(6 )属性的可枚举性和遍历
属性的可枚举行:对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为Object.getOwnPropertyDescriptor
let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj, 'foo')
// {
// value: 123,
// writable: true,
// enumerable: true,
// configurable: true
// }
属性的遍历:ES6一共有5种方法for...in
循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)Object.keys(obj)
返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名Object.getOwnPropertyNames(obj)
返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名Object.getOwnPropertySymbols(obj)
返回一个数组,包含对象自身的所有 Symbol 属性的键名
Reflect.ownKeys(obj)
返回一个数组,包含对象自身的所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举
(7)Object.getOwnPropertyDescriptors
方法,返回指定对象所有自身属性(非继承属性)的描述对象
const obj = {
foo: 123,
get bar() { return 'abc' }
};
Object.getOwnPropertyDescriptors(obj)
// { foo:
// { value: 123,
// writable: true,
// enumerable: true,
// configurable: true },
// bar:
// { get: [Function: get bar],
// set: undefined,
// enumerable: true,
// configurable: true } }
(8)super关键字:_proto_属性, Object.setPrototypeOf(),Object.getPrototypeOf()
JavaScript 语言的对象继承是通过原型链实现。
(9)super关键字:this
关键字总是指向函数所在的当前对象,ES6 又新增了另一个类似的关键字super
,指向当前对象的原型对象
const proto = {
foo: 'hello'
};
const obj = {
foo: 'world',
find() {
return super.foo;
}
};
Object.setPrototypeOf(obj, proto);
obj.find() // "hello"
super
关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错
super
都没有用在对象的方法之中
// 报错,super用在属性里面
const obj = {
foo: super.foo
}
// 报错,super用在一个函数里面,然后赋值给foo属性
const obj = {
foo: () => super.foo
}
// 报错,super用在一个函数里面,然后赋值给foo属性
const obj = {
foo: function () {
return super.foo
}
}
用在属性里面
const obj = {
foo: super.foo
}
// 报错,super用在一个函数里面,然后赋值给foo属性
const obj = {
foo: () => super.foo
}
// 报错,super用在一个函数里面,然后赋值给foo属性
const obj = {
foo: function () {
return super.foo
}
}
(10)Object.keys() Object.values() Object.entries()
let {keys, values, entries} = Object;
let obj = { a: 1, b: 2, c: 3 };
for (let key of keys(obj)) {
console.log(key); // 'a', 'b', 'c'
}
for (let value of values(obj)) {
console.log(value); // 1, 2, 3
}
for (let [key, value] of entries(obj)) {
console.log([key, value]); // ['a', 1], ['b', 2], ['c', 3]
}
Object.values() :返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值
数组的扩展
(1)扩展运算符(...):将一个数组转为用逗号分隔的参数序列
console.log(...[1, 2, 3])
// 1 2 3
console.log(1, ...[2, 3, 4], 5)
// 1 2 3 4 5
[...document.querySelectorAll('div')]
// [<div>, <div>, <div>]
//将一个数组转为用逗号分隔的参数序列
function add(x, y) {
return x + y;
}
const numbers = [4, 38];
add(...numbers) // 42
//扩展运算符与正常的函数参数结合使用
function f(v, w, x, y, z) { }
const args = [0, 1];
f(-1, ...args, 2, ...[3]);
//扩展运算符后面可以放置表达式
const arr = [
...(x > 0 ? ['a'] : []),
'b',
];
//扩展运算符后面是一个空数组,则不产生任何效果
[...[], 1]
// [1]
替代函数的apply
//1 扩展运算符可以展开数组,将数组转为函数的参数
// ES5 的写法
function f(x, y, z) {
// ...
}
var args = [0, 1, 2];
f.apply(null, args);
// ES6的写法
function f(x, y, z) {
// ...
}
let args = [0, 1, 2];
f(...args);
//2 Math.max方法
// ES5 的写法
Math.max.apply(null, [14, 3, 77])
// ES6 的写法
Math.max(...[14, 3, 77])
//push函数,将一个数组添加到另一个数组的尾部
// ES5的 写法
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
Array.prototype.push.apply(arr1, arr2);
// ES6 的写法
let arr1 = [0, 1, 2];
let arr2 = [3, 4, 5];
arr1.push(...arr2);
扩展运算符的应用
1)对数组的复制是深拷贝
//ES5
const a1 = [1, 2];
const a2 = a1; //浅拷贝
a2[0] = 2;
console.log( a1[0] ); //2
//ES6
const a1 = [1, 2];
// 写法一
const a2 = [...a1]; //深拷贝
// 写法二
const [...a2] = a1;
a2[0] = 2;
console.log( a1[0] ); //1
2)合并数组,对于成员类型是引用类型,则为浅拷贝
const a1 = [{ foo: 1 }];
const a2 = [{ bar: 2 }];
const a3 = a1.concat(a2);
const a4 = [...a1, ...a2];
a3[0] === a1[0] // true
a4[0] === a1[0] // true
3)与解构赋值结合起来,用于生成数组
扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错
const [first, ...rest] = [1, 2, 3, 4, 5];
first // 1
rest // [2, 3, 4, 5]
const [first, ...rest] = [];
first // undefined
rest // []
const [first, ...rest] = ["foo"];
first // "foo"
rest // []
const [...butLast, last] = [1, 2, 3, 4, 5];
// 报错
const [first, ...middle, last] = [1, 2, 3, 4, 5];
// 报错
(2)Array.from():将两类对象转为真正的数组---类似数组的对象(array-like object)和可遍历(iterable)的对象
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
// ES5的写法
var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']
// ES6的写法
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']
只要是部署了 Iterator 接口的数据结构,Array.from
都能将其转为数组
Array.from('hello')
// ['h', 'e', 'l', 'l', 'o']
let namesSet = new Set(['a', 'b'])
Array.from(namesSet) // ['a', 'b']
扩展运算符(...
)也可以将某些数据结构转为数组:伪数组
// arguments对象
function foo() {
const args = [...arguments];
}
// NodeList对象
[...document.querySelectorAll('div')]
(3)Array.of():将一组值,转换为数组
Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]
Array.of(3).length // 1
Array.of()弥补Array()的缺陷:由于参数个数不同导致的结果差异
Array() // []
Array(3) // [, , ,] 参数一个,指定数组的长度
Array(3, 11, 8) // [3, 11, 8] 参数个数.=2的时候,才返回由参数组成的新数组
Array.of
总是返回参数值组成的数组。如果没有参数,就返回一个空数组
Array.of() // []
Array.of(undefined) // [undefined]
Array.of(1) // [1]
Array.of(1, 2) // [1, 2]
(4)数组实例的copyWithin():在当前数组内部,将指定位置的成员复制到其他位置,会修改当前数组
Array.prototype.copyWithin(target, start = 0, end = this.length)
target(必需):从该位置开始替换数据。如果为负值,表示倒数。
start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示倒数。
end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示倒数
[1, 2, 3, 4, 5].copyWithin(0, 3)
// [4, 5, 3, 4, 5]
(5)数组实例的find()、findIndex():都可以识别NaN成员
find():找出第一个符合条件的数组成员,找到第一个返回true的成员,否则返回undefined
findIndex():返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1
(6)数组实例的fill():使用给定值,填充一个数组
fill(填充元素,start,end)---不包括end
['a', 'b', 'c'].fill(7)
// [7, 7, 7]
new Array(3).fill(7)
// [7, 7, 7]
['a', 'b', 'c'].fill(7, 1, 2)
// ['a', 7, 'c']
注意:如果填充的类型为对象,那么被赋值的是同一个内存地址的对象,而不是深拷贝对象
let arr = new Array(3).fill({name: "Mike"});
arr[0].name = "Ben";
arr
// [{name: "Ben"}, {name: "Ben"}, {name: "Ben"}]
let arr = new Array(3).fill([]);
arr[0].push(5);
arr
// [[5], [5], [5]]
(7)数组实例的entries() keys() values():返回数组 都返回一个遍历器对象,可以用for...of循环遍历
(8)数组实例的includes():Array.prototype.includes
方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes
方法类似
includes(搜素的元素,搜素的起始位置)---位置默认为0,为负数则表示倒数的位置如果大于数组长度则重置为0
[1, 2, 3].includes(2) // true
[1, 2, 3].includes(4) // false
[1, 2, NaN].includes(NaN) // true
(9)数组的空位:数组的某一个位置没有任何值
Array(3) // [, , ,]
空位不是undefined
,一个位置的值等于undefined
,依然是有值的,空位是没有任何值
字符串的扩展
(1)字符的Unicode表示法
(2)codePointAt():
codePointAt
方法会正确返回 32 位的 UTF-16 字符的码点
(3)String.fromCodePoint():从码点返回对应字符,但是这个方法不能识别 32 位的 UTF-16 字符
(4)字符串的遍历器接口:for...of
for (let codePoint of 'foo') {
console.log(codePoint)
}
// "f"
// "o"
// "o"
(5)at():可以识别 Unicode 编号大于0xFFFF
的字符,返回正确的字符
(6)normalize()
(7)includes() startsWith() endsWith()
includes():返回布尔值,表示是否找到了参数字符串。
startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部
let s = 'Hello world!';
s.startsWith('world', 6) // true 第二个参数表示开始搜索的位置直到末尾
s.endsWith('Hello', 5) // true 第二个参数表示对前面元素搜索
s.includes('Hello', 6) // false 第二个参数表示开始搜索的位置直到末尾
(8)repeat():返回一个新字符串,表示将原字符串重复n次
'na'.repeat(0) // ""
'na'.repeat(2.9) // "nana" 小数向下取整
'na'.repeat(Infinity) // 参数为Infinity报错,RangeError
'na'.repeat(-1) // 参数为负数,报错,RangeError
'na'.repeat(-0.9) // "" 参数是0到-1之间,则等同于0
'na'.repeat(NaN) // "" 参数NaN等同0
'na'.repeat('na') // "" 参数为字符串,先转换为数字
'na'.repeat('3') // "nanana"
(9)padStart(),padEnd()
(10)matchAll():返回一个正则表达式在当前字符串的所有匹配
(11)模板字符串:定义多行字符串,在字符串中嵌入变量----模板对象
数值的扩展
(1)二进制和八进制的表示法:用前缀0b
(或0B
)和0o
(或0O
)表示
ES5 开始,在严格模式之中,八进制就不再允许使用前缀0
表示
(2)Number.isFinite() Number.isNaN()
Number.isFinite():检查一个数值是否为有限值,如果参数类型不是数值,一律返回false,不会进行转换
Number.isFinite(15); // true
Number.isFinite(0.8); // true
Number.isFinite(NaN); // false
Number.isFinite(Infinity); // false
Number.isFinite(-Infinity); // false
Number.isFinite('foo'); // false
Number.isFinite('15'); // false
Number.isFinite(true); // false
Number.isNaN():用来检查一个值是否为NaN,参数类型不是NaN,一律返回false
Number.isNaN(NaN) // true
Number.isNaN(15) // false
Number.isNaN('15') // false
Number.isNaN(true) // false
Number.isNaN(9/NaN) // true
Number.isNaN('true' / 0) // true
Number.isNaN('true' / 'true') // true
(3)Number.parseInt() Number.parseFloat()
逐步减少全局性方法,使得语言逐步模块化
(4)Number.isInteger():判断一个数值是否为整数,25 和 25.0 被视为同一个值,参数不是数值返回false
Number.isInteger(25) // true
Number.isInteger(25.1) // false
Number.isInteger(25.0) // true
函数的扩展
(1)函数参数的默认值
function Point(x = 0, y = 0) {
this.x = x;
this.y = y;
}
const p = new Point();
p // { x: 0, y: 0 }
参数变量是默认声明的,所以不能用let
或const
再次声明
function foo(x = 5) {
let x = 1; // error
const x = 2; // error
}
使用参数默认值时,函数不能有同名参数
// 不报错
function foo(x, x, y) {
// ...
}
// 报错 SyntaxError: Duplicate parameter name not allowed in this context
function foo(x, x, y = 1) {
// ...
}
与解构赋值默认值结合使用
function foo({x, y = 5}) {
console.log(x, y);
}
foo({}) // undefined 5
foo({x: 1}) // 1 5
foo({x: 1, y: 2}) // 1 2
foo() // TypeError: Cannot read property 'x' of undefined
函数的length属性:返回没有指定默认值的参数个数
(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2
rest 参数也不会计入length
属性
(function(...args) {}).length // 0
作用域:一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失
//例1
var x = 1;
function f(x, y = x) {
console.log(y);
}
f(2) // 2
//例2,函数体内部的局部变量x影响不到默认值变量x
let x = 1;
function f(y = x) {
let x = 2;
console.log(y);
}
f() // 1
函数体内部的局部变量x影响不到默认值变量x
let x = 1;
function f(y = x) {
let x = 2;
console.log(y);
}
f() // 1
//例3 ,全局变量不存在,则报错
function f(y = x) {
let x = 2;
console.log(y);
}
f() // ReferenceError: x is not defined
//参数x = x形成一个单独作用域。实际执行的是let x = x,由于暂时性死区的原因,这行代码会报错”x 未定义“
var x = 1;
function foo(x = x) {
// ...
}
foo() // ReferenceError: x is not defined
参数x = x形成一个单独作用域。实际执行的是let x = x,由于暂时性死区的原因,这行代码会报错”x 未定义“
var x = 1;
function foo(x = x) {
// ...
}
foo() // ReferenceError: x is not defined
参数的默认值是一个函数也遵守这个规则
//例1:正常
let foo = 'outer';
function bar(func = () => foo) {
let foo = 'inner';
console.log(func());
}
bar(); // outer
//例2:报错,匿名函数里面的foo指向函数外层,但是函数外层并没有声明变量foo,所以就报错了
function bar(func = () => foo) {
let foo = 'inner';
console.log(func());
}
bar() // ReferenceError: foo is not defined
//例1
var x = 1;
function foo(x, y = function() { x = 2; }) { //匿名函数里面的x指向第一个参数x
var x = 3; //这个局部变量x与参数x是不同作用域
y();
console.log(x);
}
foo() // 3
x // 1
//例2
var x = 1;
function foo(x, y = function() { x = 2; }) {
x = 3; //这个x指向第一个参数x
y();
console.log(x);
}
foo() // 2
x // 1
(2)rest参数(形式为...变量名):rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中;rest参数只能是组后一个参数
function add(...values) {
let sum = 0;
for (var val of values) {
sum += val;
}
return sum;
}
add(2, 5, 3) // 10
// 报错
function f(a, ...b, c) {
// ...
}
(3)严格模式:只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错
(4)name属性:返回该函数的函数名
function foo() {}
foo.name // "foo"
注意:将一个匿名函数赋值给一个变量,ES5 的name
属性,会返回空字符串,而 ES6 的name
属性会返回实际的函数名
var f = function () {};
// ES5
f.name // ""
// ES6
f.name // "f"
将一个具名函数赋值给一个变量,则 ES5 和 ES6 的name
属性都返回这个具名函数原本的名字
const bar = function baz() {};
// ES5
bar.name // "baz"
// ES6
bar.name // "baz"
(5)箭头函数:使用(=>)定义函数,使表达更简洁;简化回调函数
注意点:
(a)没有this super arguments new.target绑定-----这些值由外围最近一层费箭头函数决定
(b)不可以使用new
命令-------箭头函数没有[[construct]]方法,不可以当作构造函数,否则会抛出一个错误
(c)没有原型------不能使用new关键字调用箭头函数,所以没有prototype属性
(d)不可以使用arguments
对象-------箭头函数没有arguments绑定,如果要用,可以用 rest 参数和命名参数代替
(e)不可以改变this绑定-------在函数的生命周期内始终保持一致
(f)不可以使用yield
命令,因此箭头函数不能用作 Generator 函数
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
var id = 21;
foo.call({ id: 42 }); //使用了箭头函数,this绑定的是对象{id:42}
// id: 42
function Timer() {
this.s1 = 0;
this.s2 = 0;
// 箭头函数
setInterval(() => this.s1++, 1000);
// 普通函数
setInterval(function () {
this.s2++;
}, 1000);
}
var timer = new Timer();
setTimeout(() => console.log('s1: ', timer.s1), 3100); //s1使用剪头函数,更新了3次
setTimeout(() => console.log('s2: ', timer.s2), 3100); //s2是在普通函数中调用,由于this指向全局的s2,而全局没有,所有为0
// s1: 3
// s2: 0
var f = v => v;
// 等同于
var f = function (v) {
return v;
};
// 报错,大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错
let getTempItem = id => { id: id, name: "Temp" };
// 不报错
let getTempItem = id => ({ id: id, name: "Temp" });
大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错
let getTempItem = id => { id: id, name: "Temp" };
// 不报错
let getTempItem = id => ({ id: id, name: "Temp" });
function foo() {
return () => {
return () => {
return () => {
console.log('id:', this.id);
};
};
};
}
var f = foo.call({id: 1});
var t1 = f.call({id: 2})()(); // id: 1
var t2 = f().call({id: 3})(); // id: 1
var t3 = f()().call({id: 4}); // id: 1
除了this
,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:arguments
、super
、new.target
function foo() {
setTimeout(() => {
console.log('args:', arguments);
}, 100);
}
foo(2, 4, 6, 8)
// args: [2, 4, 6, 8]
由于箭头函数没有自己的this
,所以当然也就不能用call()
、apply()
、bind()
这些方法去改变this
的指向
(function() {
return [
(() => this.x).bind({ x: 'inner' })()
];
}).call({ x: 'outer' });
// ['outer']
(6)双冒号运算符:函数绑定运算符(::),对象::函数,自动将左边的对象,作为上下文环境(即this
对象)
双冒号左边为空,右边是一个对象的方法,则等于将该方法绑定在该对象上面
var method = obj::obj.foo;
// 等同于
var method = ::obj.foo;
let log = ::console.log;
// 等同于
var log = console.log.bind(console);
(7)尾调用:函数式编程的一个重要概念,指某个函数的最后一步是调用另一个函数,通俗的讲是将一个函数作为另一个函数的返回值
function f(x){
return g(x);
}
以下三种情况,都不属于尾调用:
// 情况一,调用函数g之后,还有赋值操作
function f(x){
let y = g(x);
return y;
}
// 情况二,调用后还有操作
function f(x){
return g(x) + 1;
}
// 情况三
function f(x){
g(x);
}
//情况三等于下面形式
function f(x){
g(x);
return undefined;
}
尾调用优化:只保留内层函数的调用帧
只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行“尾调用优化”
//不会进行尾调用优化,因为内层函数inner用到了外层函数addOne的内部变量one
function addOne(a){
var one = 1;
function inner(b){
return b + one;
}
return inner(a);
}
尾递归:函数调用自身,称为递归;如果尾调用自身,就称为尾递归
复杂度 O(n)
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}
factorial(5) // 120
复杂度 O(1)
function factorial(n, total) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
factorial(5, 1) // 120
Fibonacci 数列实现
function Fibonacci (n) {
if ( n <= 1 ) {return 1};
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
Fibonacci(10) // 89
Fibonacci(100) // 堆栈溢出
Fibonacci(500) // 堆栈溢出
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
if( n <= 1 ) {return ac2};
return Fibonacci2 (n - 1, ac2, ac1 + ac2);
}
Fibonacci2(100) // 573147844013817200000
Fibonacci2(1000) // 7.0330367711422765e+208
Fibonacci2(10000) // Infinity
正则的扩展
(1)RegExp构造函数
//方法一:参数是字符串,第二个参数表示正则表达式的修饰符
var regex = new RegExp('xyz', 'i');
// 等价于
var regex = /xyz/i;
//方法二:参数是一个正则表示式,这时会返回一个原有正则表达式的拷贝
var regex = new RegExp(/xyz/i);
// 等价于
var regex = /xyz/i;
(2)字符串的正则方法:match()
、replace()
、search()
、
split()
(3)u修饰符,会正确处理四个字节的 UTF-16 编码
4)Promise对象
5)类---class----ES6之前实现继承是通过原型链实现的,先产生子实例对象(this),然后在将函数原型对象上的属性赋给子实例对象;而类是先通过在子类的constructor中调用super(),即先产生父元素(this),然后在将属性赋到this对象上,因为子类中没有this,用的是父类的this
JavaScript 语言中,生成实例对象的传统方法是通过构造函数;
类的内部包括的类型:构造函数(定义实例的属性)、普通方法、静态方法(静态方法不会被实例对象调用,只能被函数调用,可以被子类继承)
ES6中通过class
关键字,可以定义类;通过new生成类的实例对象;类的数据类型就是函数,类本身就指向构造函数;类的所有方法都定义在类的prototype
属性上面;类的内部所有定义的方法,都是不可枚举的
//定义类,类里面只能包含构造函数、方法(不需要使用function声明),方法之间不需要逗号分隔
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
类的属性名,可以采用表达式
let methodName = 'getArea';
class Square {
constructor(length) {
// ...
}
[methodName]() {
// ...
}
}
类和模块的内部,默认就是严格模式
constructor():类的默认方法,通过new
命令生成对象实例时,自动调用该方法;默认返回实例对象(即this
),完全可以指定返回另外一个对象;跟普通构造函数的一个主要区别,必须使用new
调用,否则会报错,而前者不会报错
与 ES5 一样,实例的属性除非显式定义在其本身(即定义在this
对象上),否则都是定义在原型上(即定义在class
上)
//定义类
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
var point = new Point(2, 3);
point.toString() // (2, 3)
point.hasOwnProperty('x') // true
point.hasOwnProperty('y') // true
point.hasOwnProperty('toString') // false
point.__proto__.hasOwnProperty('toString') // true
与 ES5 一样,类的所有实例共享一个原型对象
class表达式:与函数一样,类也可以使用表达式的形式定义
//这个类的名字是MyClass而不是Me,Me只在 Class 的内部代码可用,指代当前类
const MyClass = class Me {
getClassName() {
return Me.name;
}
};
let inst = new MyClass();
inst.getClassName() // Me
Me.name // ReferenceError: Me is not defined
类不存在变量提升(hoist),这一点与 ES5 完全不同
this指向:类的方法内部如果含有this
,它默认指向类的实例,但是一旦单独使用该方法,很可能报错
class Logger {
printName(name = 'there') {
this.print(`Hello ${name}`);
}
print(text) {
console.log(text);
}
}
const logger = new Logger();
const { printName } = logger; //printName()中的this默认指向类的实例,但是将其单独提取出来,this指向方法运行时的环境,因为找不到print而报错
printName(); // TypeError: Cannot read property 'print' of undefined
name属性:
//name属性总是返回紧跟在class关键字后面的类名
class Point {}
Point.name // "Point"
Class的取值函数(getter)和存值函数(setter)
与 ES5 一样,在“类”的内部可以使用get
和set
关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为
class MyClass {
constructor() {
// ...
}
get prop() {
return 'getter';
}
set prop(value) {
console.log('setter: '+value);
}
}
let inst = new MyClass();
inst.prop = 123;
// setter: 123
inst.prop
// 'getter'
Class的静态方法:类相当于实例的原型,所有在类中定义的方法,都会被实例继承
但是在类的方法前面加上static,该方法不会被实例调用(继承),而是直接通过类来调用
class Foo {
static classMethod() {
return 'hello';
}
}
Foo.classMethod() // 'hello'
var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function
在静态方法中的this指向类,而不是类的实例对象,静态方法可以与非静态方法重名
class Foo {
static bar () {
this.baz();
}
static baz () {
console.log('hello');
}
baz () {
console.log('world');
}
}
Foo.bar() // hello
父类的静态方法,可以被子类继承
class Foo {
static classMethod() {
return 'hello';
}
}
class Bar extends Foo {
}
Bar.classMethod() // 'hello'
Class的静态属性和实例属性
静态属性:指的是 Class 本身的属性,即Class.propName
,而不是定义在实例对象(this
)上的属性
Class 内部只有静态方法,没有静态属性,只能通过下面的方法定义静态属性
class Foo {
}
Foo.prop = 1;
Foo.prop // 1
// 以下两种写法都无效
class Foo {
// 写法一
prop: 2
// 写法二
static prop: 2
}
Foo.prop // undefined
Class的继承:通过extend继承, ES5 通过修改原型链实现继承
class Point {
}
class ColorPoint extends Point { //在子类的构造函数中,只有调用super()之后,才能调用this
constructor(x, y, color) { //子类必须在constructor()中调用super(),否则新建实例会报错,子类的this对象必须先通过父类的构造函数
//得到与父类同样的实例属性和方法,加上子类自己的实例属性和方法
super(x, y); // 调用父类的constructor(x, y),新建父类的this对象
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 调用父类的toString()
}
}
ES5 的继承,实质是先创造子类的实例对象this
,然后再将父类的方法添加到this
上面(Parent.apply(this)
)
ES6 的继承机制完全不同,实质是先创造父类的实例对象this
(所以必须先调用super
方法),然后再用子类的构造函数修改this
Object.getPrototypeOf():从子类上获取父类
//用于一个类是否继承了另外一个类
Object.getPrototypeOf(ColorPoint) === Point
// true
super关键字:当函数使用,当对象使用
当函数使用:代表父类的构造函数,子类的构造函数必须执行一次super
函数;super()内部的 this指向子类;只能用在子类的构造函数之中
class A {
constructor() {
console.log(new.target.name); //指向当前正在执行的函数
}
}
class B extends A {
constructor() {
super(); //返回的是子类B的实例,即super内部的this指的是B
}
}
new A() // A
new B() // B
指向当前正在执行的函数
}
}
class B extends A {
constructor() {
super(); //返回的是子类B的实例,即super内部的this指的是B
}
}
new A() // A
new B() // B
当对象使用:在普通方法中,super指向父类的原型对象,;在静态方法中,指向父类
class A {
p() {
return 2;
}
}
class B extends A {
constructor() {
super();
console.log(super.p()); // 2 super.p()=A.prototype.p() 这里的super指向父类的原型对象,所以super无法调用父类实例上的方法、属性
}
}
let b = new B();
在子类普通方法中通过super
调用父类的方法时,方法内部的this
指向当前的子类实例
class A {
constructor() {
this.x = 1;
}
print() {
console.log(this.x);
}
}
class B extends A {
constructor() {
super();
this.x = 2;
}
m() {
super.print();
}
}
let b = new B();
b.m() // 2
6)模块--module:自动运行在严格模式下,并且没有办法退出运行的JS代码
特点:
a)在模块顶部创建的变量不会自动添加到全局共享作用域,仅在模块的顶级作用域中存在
b)模块必须导出一些外部代码可以访问的元素(变量 函数),也可以从其他模块导入绑定,导入的变量是只读变量
c)模块顶部的this值为undefined
d)模块不支持HTML风格的代码注释
脚本----不是模块的js代码没有上述特性
ES6 模块的设计思想:尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量
ES6 模块不是对象,而是通过export
命令显式指定输出的代码,再通过import
命令输入
// ES6模块,编译时加载或静态加载
import { stat, exists, readFile } from 'fs';
模块功能主要由两个命令构成:export(规定模块的对外接口),import(输入其他模块提供的功能)
使用export命令输出变量:
使用import引入的变量:具有提升效果,会提升到整个模块的头部,首先执行
import是静态执行,所以不能使用表达式和变量
// 报错,使用了表达式
import { 'f' + 'oo' } from 'my_module';
// 报错,使用了变量
let module = 'my_module';
import { foo } from module;
// 报错,使用了if语句
if (x === 1) {
import { foo } from 'module1';
} else {
import { foo } from 'module2';
}
模块的整体加载:用*指定一个对象,所有输出值都加载在这个对象上面
// circle.js
export function area(radius) {
return Math.PI * radius * radius;
}
export function circumference(radius) {
return 2 * Math.PI * radius;
}
import { area, circumference } from './circle';
console.log('圆面积:' + area(4));
console.log('圆周长:' + circumference(14));
import * as circle from './circle';
console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));
export default命令:import可以指定任意名字,且不需要加大括号
// export-default.js
export default function () {
console.log('foo');
}
// import-default.js
import customName from './export-default';
customName(); // 'foo'
export default命令后面不能跟变量声明语句,本质是将后面变量的值赋给变量default
// 正确
export var a = 1;
// 正确
var a = 1;
export default a;
// 错误
export default var a = 1;
// 正确
export default 42;
// 报错
export 42;
Module的加载实现
1)浏览器加载ES6模块:使用<script type='module'>;异步加载,等同defer;多个则顺序执行
<script type="module" src="./foo.js"></script>
ES6模块CommonJS模块的差异:
两个重大差异:
CommonJS 模块输出的是一个值的拷贝(一旦输出一个值,模块内部的变化就影响不到这个值),ES6 模块输出的是值的引用(模块内部的变化会影响到该值)。
CommonJS 模块是运行时加载(加载的是一个对象,运行时才会生成),ES6 模块是编译时输出接口(ES6模块不是对象,对外接口是一种静态定义)
一旦输出一个值,模块内部的变化就影响不到这个值),ES6 模块输出的是值的引用(模块内部的变化会影响到该值)。
CommonJS 模块是运行时加载(加载的是一个对象,运行时才会生成),ES6 模块是编译时输出接口(ES6模块不是对象,对外接口是一种静态定义)
7)Symbol:ES5 的对象属性名都是字符串,这容易造成属性名的冲突,Symbol保证每个属性的名字都是独一无二
Symbol值通过Symbol函数生成,对象的属性名有两种类型:字符串、Symbol类型
//Symbol()函数 前不能使用new,因为Symbol是一个原始类型的值,不是对象
let s = Symbol();
typeof s// "symbol"
let s1 = Symbol('foo'); //接收一个字符串作为参数,易区分Symbol实例
let s2 = Symbol('bar');
s1 // Symbol(foo)
s2 // Symbol(bar)
s1.toString() // "Symbol(foo)"
s2.toString() // "Symbol(bar)"
const obj = {
toString() {
return 'abc';
}
};
const sym = Symbol(obj); //Symbol()的参数是一个对象,会调用该对象的toStrign,将其转为字符串,然后才生成一个Symbol值
sym // Symbol(abc)
// 没有参数的情况
let s1 = Symbol(); //Symbol函数的参数只是对当前Symbol值的描述,相同参数的Symbol函数的返回值是不一样的
let s2 = Symbol();
s1 === s2 // false
// 有参数的情况
let s1 = Symbol('foo');
let s2 = Symbol('foo');
s1 === s2 // false
let sym = Symbol('My symbol'); //Symbol值不能与其他类型的的值进行运算
"your symbol is " + sym
// TypeError: can't convert symbol to string
`your symbol is ${sym}`
// TypeError: can't convert symbol to string
作为属性名的Symbol:一个对象由多个模块构成的情况,保证不会出现同名的属性
//过方括号结构和Object.defineProperty,将对象的属性名指定为一个 Symbol 值
let mySymbol = Symbol();
// 第一种写法
let a = {};
a[mySymbol] = 'Hello!';
// 第二种写法
let a = {
[mySymbol]: 'Hello!'
};
// 第三种写法
let a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });
// 以上写法都得到同样结果
a[mySymbol] // "Hello!"
const mySymbol = Symbol();
const a = {};
a.mySymbol = 'Hello!'; //Symbol值作为对象属性名时,不能用点运算符,点运算符后面总是字符串,所以a的属性名是字符串而不是Symbol值
a[mySymbol] // undefined
a['mySymbol'] // "Hello!"
//对象的内部,使用 Symbol 值定义属性时,Symbol 值必须放在方括号之中
let s = Symbol();
let obj = {
[s]: function (arg) { ... }
};
obj[s](123);
//等同于下面的写法
let obj = {
[s](arg) { ... }
};
8)set和map数据结构
set结构:类似于数组,没有重复的元素,Set()是一个构造函数,用来生成 Set 数据结构
//无参数,成员去重类似===(NaN不相等),在Set数据结构里面是相等的
const s = new Set();
[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));
for (let i of s) {
console.log(i);
}
// 2 3 5 4
// 例一:有参数
const set = new Set([1, 2, 3, 4, 4]);
[...set]
// [1, 2, 3, 4]
// 例二
const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
items.size // 5
// 例三
const set = new Set(document.querySelectorAll('div'));
set.size // 56
// 去除数组的重复成员
[...new Set(array)]
//NaN相等
let set = new Set();
let a = NaN;
let b = NaN;
set.add(a);
set.add(b);
set // Set {NaN}
//两个对象总是不相等
let set = new Set();
set.add({});
set.size // 1
set.add({});
set.size // 2
Set实例的属性:
Set.prototype.constructor:构造函数,默认就是Set函数。
Set.prototype.size:返回Set实例的成员总数
Set实例的方法:操作方法(用于操作数据)和遍历方法(用于遍历成员)
//操作方法
add(value):添加某个值,返回 Set 结构本身
delete(value):删除某个值,返回一个布尔值,表示删除是否成功
has(value):返回一个布尔值,表示该值是否为Set的成员
clear():清除所有成员,没有返回值
//遍历方法,遍历顺序就是插入顺序
keys():返回键名的遍历器 键名和键值是一样的
values():返回键值的遍历器
entries():返回键值对的遍历器
forEach():使用回调函数遍历每个成员
数组成员去重:Array.from()可以将 Set 结构转为数组;或者扩展运算符(...)和Set结构
//Array.from()结合Set数据结构
function dedupe(array) {
return Array.from(new Set(array));
}
dedupe([1, 1, 2, 3]) // [1, 2, 3]
//扩展运算符结合Set数据结构
let set = new Set(['red', 'green', 'blue']);
let arr = [...set];
// ['red', 'green', 'blue']
用Set实现并、交、差集
let a = new Set([1, 2, 3]);
let b = new Set([4, 3, 2]);
// 并集
let union = new Set([...a, ...b]);
// Set {1, 2, 3, 4}
// 交集
let intersect = new Set([...a].filter(x => b.has(x)));
// set {2, 3}
// 差集
let difference = new Set([...a].filter(x => !b.has(x)));
// Set {1}
WeakSet:不重复的值的集合
与 Set 有两个区别:WeakSet 的成员只能是对象,而不能是其他类型的值;WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,WeakSet 不可遍历
//WeakSet()可以接受一个数组或类似数组的对象作为参数,并且数组的成员只能是对象
const a = [[1, 2], [3, 4]];
const ws = new WeakSet(a);
// WeakSet {[1, 2], [3, 4]} 数组的成员成为WeakSet的成员
const b = [3, 4];
const ws = new WeakSet(b);
// Uncaught TypeError: Invalid value used in weak set(…)
//WeakSet结构有3种方法
WeakSet.prototype.add(value):向 WeakSet 实例添加一个新成员。
WeakSet.prototype.delete(value):清除 WeakSet 实例的指定成员。
WeakSet.prototype.has(value):返回一个布尔值,表示某个值是否在 WeakSet 实例之中
WeakSet 的一个用处,是储存 DOM 节点,而不用担心这些节点从文档移除时,会引发内存泄漏
Map结构:Map的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(clash)的问题。
JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键
Map数据结构就解决了这个问题;类似于对象,也是键值对的集合,“键”可以是字符串,各种类型的值(包括对象),也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现
//Map()无参,添加成员,获取成员,判断成员,删除长远
const m = new Map();
const o = {p: 'Hello World'};
m.set(o, 'content')
m.get(o) // "content"
m.has(o) // true
m.delete(o) // true
m.has(o) // false
//Map()有参,数组的成员是一个个表示键值对的数组
const map = new Map([
['name', '张三'],
['title', 'Author']
]);
map.size // 2
map.has('name') // true
map.get('name') // "张三"
map.has('title') // true
map.get('title') // "Author"
//只有对同一个对象的引用,Map 结构才将其视为同一个键
只有对同一个对象的引用,Map 结构才将其视为同一个键
const map = new Map();
map.set(['a'], 555); //表面是同一个键,但是内存地址不一样
map.get(['a']) // undefined
Map的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map 将其视为一个键,比如0
和-0
就是一个键,布尔值true
和字符串true
则是两个不同的键。另外,undefined
和null
也是两个不同的键。虽然NaN
不严格相等于自身,但 Map 将其视为同一个键
let map = new Map();
map.set(-0, 123);
map.get(+0) // 123
map.set(true, 1);
map.set('true', 2);
map.get(true) // 1
map.set(undefined, 3);
map.set(null, 4);
map.get(undefined) // 3
map.set(NaN, 123);
map.get(NaN) // 123
Map实例的属性和操作方法
map.size属性
set(key, value) //返回当前的Map对象,可采用链式写法,如果key已经有值,则键值会被更新,否则就新生成该键
get(key) //get方法读取key对应的键值,如果找不到key,返回undefined
has(key) //返回布尔值
delete(key) //删除某个键,返回true。如果删除失败,返回false
clear() //清除所有成员,没有返回值
Map实例的遍历方法
keys():返回键名的遍历器
values():返回键值的遍历器
entries():返回所有成员的遍历器
forEach():遍历 Map 的所有成员
WeakMap:与Map结构类似,用于生成键值对的集合
WeakMap与Map的区别:只接受对象作为键名;键名所指向的对象,不计入垃圾回收机制
// WeakMap 可以使用 set 方法添加成员
const wm1 = new WeakMap();
const key = {foo: 1};
wm1.set(key, 2);
wm1.get(key) // 2
// WeakMap 也可以接受一个数组,
// 作为构造函数的参数
const k1 = [1, 2, 3];
const k2 = [4, 5, 6];
const wm2 = new WeakMap([[k1, 'foo'], [k2, 'bar']]);
wm2.get(k2) // "bar"
const map = new WeakMap();
map.set(1, 2)
// TypeError: 1 is not an object!
map.set(Symbol(), 2)
// TypeError: Invalid value used as weak map key
map.set(null, 2)
// TypeError: Invalid value used as weak map key
只有get() set() has() delete()
9)Proxy:用于修改某些操作的默认行为,可以对外界的访问进行过滤和改写
//构造函数Proxy()生成Proxy实例,target要拦截的目标对象,handller--对象,定制拦截行为
var proxy = new Proxy(target, handler);
//第一个参数是要代理的目标对象(空对象),第二个参数是配置对象
//要使得Proxy起作用,必须针对Proxy实例(上例是proxy对象)进行操作,而不是针对目标对象(上例是空对象)进行操作
var proxy = new Proxy({}, {
//target--目标对象,property--要访问的属性
get: function(target, property) {
return 35;
}
});
proxy.time // 35
proxy.name // 35
proxy.title // 35
//如果handler没有设置任何拦截,那就等同于直接通向原对象,handler是一个空对象,没有任何拦截效果,访问proxy就等同于访问target
var target = {};
var handler = {};
var proxy = new Proxy(target, handler);
proxy.a = 'b';
target.a // "b"
var proxy = new Proxy({}, {
get: function(target, property) {
return 35;
}
});
let obj = Object.create(proxy); //proxy对象是obj对象的原型
obj.time // 35 //在obj的prototype上面找到属性time
Proxy支持13种拦截操作
1)get(target, propKey, receiver):拦截对象属性的读取,比如proxy.foo和proxy['foo']
三个参数,依次为目标对象----Object、属性名和 proxy 实例本身(严格地说,是操作行为所针对的对象,可选)
//访问目标对象不存在的属性,会抛出一个错误。如果没有这个拦截函数,访问不存在的属性,只会返回undefined
var person = {
name: "张三"
};
var proxy = new Proxy(person, {
get: function(target, property) {
if (property in target) { return target[property];
} else {
throw new ReferenceError("Property \"" + property + "\" does not exist.");
}
}
});
proxy.name // "张三"proxy.age // 抛出一个错误
//get方法可以通过Object.create()实现继承
let proto = new Proxy({}, {
get(target, propertyKey, receiver) {
console.log('GET ' + propertyKey);
return target[propertyKey];
}
});
let obj = Object.create(proto);//proto定义在obj上的原型对象上,实现继承obj.foo
// "GET foo"利用 Proxy,可以将读取属性的操作(get),转变为执行某个函数,从而实现属性的链式操作
var pipe = (function () {
return function (value) {
var funcStack = [];
var oproxy = new Proxy({} , {
get : function (pipeObject, fnName) {
if (fnName === 'get') {
return funcStack.reduce(function (val, fn) {
return fn(val);
},value);
}
funcStack.push(window[fnName]);
return oproxy;
}
});
return oproxy;
}
}());
var double = n => n * 2;
var pow = n => n * n;
var reverseInt = n => n.toString().split("").reverse().join("") | 0;
pipe(3).double.pow.reverseInt.get; // 63
2)set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v,返回一个布尔值
四个参数,依次为目标对象----Object、属性名、属性值和 Proxy 实例本身(可选)
作用:数据验证(只设置预设的数据)、数据绑定(对象变化时,自动更新DOM)
结合set和get可以实现内部属性被外部读写
3)has(target, propKey):拦截HasProperty propKey in proxy的操作,返回一个布尔值
判断对象是否具有某个属性,两个参数,分别是目标对象-------Object、需查询的属性名
var handler = {
has (target, key) {
if (key[0] === '_') {
return false;
}
return key in target;
}
};
var target = { _prop: 'foo', prop: 'foo' };
var proxy = new Proxy(target, handler);
'_prop' in proxy // false
4)deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值
拦截delete操作,如果这个方法抛出错误或者返回false,当前属性就无法被delete命令删除,代理对象target---Object
var handler = {
deleteProperty (target, key) {
invariant(key, 'delete');
return true;
}
};
function invariant (key, action) {
if (key[0] === '_') {
throw new Error(`Invalid attempt to ${action} private "${key}" property`);
}
}
var target = { _prop: 'foo' };
var proxy = new Proxy(target, handler);
delete proxy._prop
// Error: Invalid attempt to delete private "_prop" property
5)getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(target, propKey),返回属性的描述对象或undefined。target----Object
拦截Object.getOwnPropertyDescriptor()
var handler = {
getOwnPropertyDescriptor (target, key) {
if (key[0] === '_') {
return;
}
return Object.getOwnPropertyDescriptor(target, key);
}
};
var target = { _foo: 'bar', baz: 'tar' };
var proxy = new Proxy(target, handler);
Object.getOwnPropertyDescriptor(proxy, 'wat')
// undefined
Object.getOwnPropertyDescriptor(proxy, '_foo')
// undefined
Object.getOwnPropertyDescriptor(proxy, 'baz')
// { value: 'tar', writable: true, enumerable: true, configurable: true }
6)defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)--------只有属性enumerable configurable writable get set value。Object.defineProperties(proxy, propDescs),返回一个布尔值,target----Object
var handler = {
defineProperty (target, key, descriptor) {
return false; //false--导致对象不能添加新属性,true--对象可以添加新属性
}
};
var target = {};
var proxy = new Proxy(target, handler);
proxy.foo = 'bar' // 不会生效
5)ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性
8)preventExtensions(target):拦截Object.preventExtensions(target),返回一个布尔值,target-----Object
10)isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值,target-----Object
9)getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象或null
11)setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截
12)apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...),即拦截函数的调用、call/apply操作-----代理目标为function
三个参数,分别是目标对象-----Function、目标对象的上下文对象(this
)、目标对象的参数数组
//拦截目标对象上的方法,换成apply上定义的方法
var target = function () { return 'I am the target'; };
var handler = { apply: function () { return 'I am the proxy'; }};var p = new Proxy(target, handler);p() //变量p是 Proxy 的实例,当它作为函数调用时(p()),就会被apply方法拦截,返回一个字符串// "I am the proxy"
var twice = {
apply (target, ctx, args) {
return Reflect.apply(...arguments) * 2;
}
};
function sum (left, right) {
return left + right;
};
var proxy = new Proxy(sum, twice);
proxy(1, 2) // 6
proxy.call(null, 5, 6) // 22
proxy.apply(null, [7, 8]) // 30
13)construct(target, args):拦截new命令即 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)
返回值必须为一个对象,否则报错------代理目标为函数Function
拦截对象的写法
var handler = {
//target--目标对象,args--构造函数的参数对象,newTarget--创造实例对象时,new命令作用的构造函数(下面例子的p)
construct (target, args, newTarget) {
return new target(...args);
}
};
var p = new Proxy(function () {}, {
construct: function(target, args) {
console.log('called: ' + args.join(', '));
return { value: args[0] * 10 };
}
});
(new p(1)).value
// "called: 1"
// 10