ES6虽然已经用了很久,但是还没系统的过一遍,并且只知用法不知内部实现,此次整理除了系统的过一遍外,进行知识串联,还会了解其内部实现原理。
另外推荐JSRUN这款在线编辑器,它支持es6语法。
ECMA是javascript的规格,javascript是ECMA的实现。初学者一开始学的其实就是ECMA3.0版本的语法,而4.0版本没有通过,大部分内容都被es6继承了,又因历史原因,ES6指的并不是一个特定版本的标准,而是涵盖了ES2015、ES2016、ES2017等,因此es6是一个内容又多又重要的一个版本。
各大浏览器的最新版本,对 ES6 的支持可以查看kangax.github.io/es5-compat-table/es6/。
各种运行环境对ES6的支持情况ruanyf.github.io/es-checker。
babel是将es6转为es5的转码器,有了转码器就可以在不支持es6的环境中(浏览器的发展速度跟不上ECMA的发布)使用es6语法。
因为ECMA的版本都是向下兼容的,因此ES2017是包含ES2016和ES2015的,那么对应不同的版本,babel也提供了对应的转换器。
babel-preset-es2015将es2015版本的js转译为es5
babel-preset-es2016将es2016版本的js转译为es5
babel-preset-es2017将es2017版本的js转译为es5
针对js规范制定的4个阶段,babel也有对应的转译器:
Stage0:preset-stage-0
Stage1:preset-stage-1
Stage2:preset-stage-2
Stage3:preset-stage-3
不同阶段的转译器之间是包含的关系,preset-stage-0转译器除了包含了preset-stage-1的所有功能还增加了transform-do-expressions插件和transform-function-bind插件,同样preset-stage-1转译器除了包含preset-stage-2的全部功能外还增加了一些额外的功能
除了上述,babel有分转译插件(plugins)和转译器(presets),转译插件是针对一种语法的单一功能插件,比如transform-es2015-arrow-functions针对箭头函数的转译插件,也有针对转译插件合集的转译器,如transform-es2015-classes(es2015 class类转译插件),一般都是用的转译器,而转译器又分为三种:
babel可以在浏览器、命令行、gulp和webpack中使用。
使用babel-cli:
npm install --save-dev babel-cli;
{
"name": "es6",
"version": "1.0.0",
"description": "",
"main": "arrow.js",
"scripts": {
"build": "babel src -d lib"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"babel-cli": "^6.24.1"
}
}
{
"presets":[], //presets用于配置我们所需要的转译器
"plugins":[] //plugins用于配置我们转译所需要的插件
}
presets是设定转码规则,根据官方提供的规则集按需求安装后加入配置文件
# 最新转码规则
$ npm install --save-dev babel-preset-latest
# react 转码规则
$ npm install --save-dev babel-preset-react
# 不同阶段语法提案的转码规则(共有4个阶段),选装一个
$ npm install --save-dev babel-preset-stage-0
$ npm install --save-dev babel-preset-stage-1
$ npm install --save-dev babel-preset-stage-2
$ npm install --save-dev babel-preset-stage-3
如果不想要.babelrc文件且用webpack或者gulp,也可以在package.json中对Babel进行配置,
{
"name": "es6",
"version": "1.0.0",
"description": "",
"main": "arrow.js",
"scripts": {
"build": "babel src -d lib --comments=true"
},
"babel":{
//babel选项
"presets":["es2015"],
"comments":false
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"babel-cli": "^6.24.1",
"babel-loader": "^7.1.1",
"babel-preset-env": "^1.6.0",
"babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1",
"webpack": "^3.2.0"
}
}
常见的Babel转译器:
babel的配置:
babel的配置选项非常多,每个转译器也都有自己的配置选项,下面介绍最常用的。
env
默认值为一个空对象{}. env用于配置代码需要兼容的环境,比如你的代码要在chrome52上运行,可以这样配置.babelrc。
{
"presets": [
["env", {
"targets": {
"chrome": 52
}
}]
]
}
配置代码兼容最新的node,可以这样设置.babelrc
{
"presets": [
["env", {
"targets": {
"node": "current"
}
}]
]
}
ignore,忽略某些文件:
{
"presets":["env"],
"ignore":["foo.js"]
}
minified
是否压缩转译后的代码,默认值为false.
{
"presets":["env"],
"ignore":["foo.js"],
"minified":true
}
plugins
配置转译所需要的插件。使用插件的顺序是按照插件在数组中的顺序依次调用的。比如如下命令,转译的时候先使用transform-decorators-legacy转译,再使用transform-class-properties转译
{
"plugins": [
"transform-decorators-legacy",
"transform-class-properties"
]
}
presets
配置你要使用的转译器。使用转译器的顺序是按照转译器在数组中的反顺序进行调用的。先使用数组最末尾的转译器,然后使用倒数第2个,倒数第3个,依次类推。比如使用下面命令的时候,先使用stage-2转译器转译,再react转译器转译,最后使用es2015转译器转译。
{
"presets": [
"es2015",
"react",
"stage-2"
]
}
如果同时存在plugins和presets,则先使用plugins转译
plugin的调用顺序是从第一个到最后一个,
presets的调用的顺序是相反的,从最后一个到第一个
在webpack中使用Babel:
var path = require("path");
module.exports = {
entry: './src/person.js',
output: {
path: path.resolve(__dirname,"lib"),
filename: 'person.compiled.js',
},
module: {
loaders: [{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
query:{
presets:["env"]
}
}]
}
}
entry为入口文件,我们选择当前目录下,src文件夹下的person.js文件作为入口。output为输出选项,path为输出的目录,filename为输出文件名。query选项为.babelrc中的配置选项。在webpack中设置了query字段后,就不再需要.babelrc文件了。
ES5只用var和function两种声明变量的方式,而ES6有六种:var、function、let、const、import、class。
新增let、const分别定义变量和常量:
const foo = Object.freeze({})
foo.prop = 123;//在严格模式下会报错,常规模式下不起作用
彻底冻结对象的函数:
let constantize = (obj)=>{
Object.freeze(obj);
Object.keys(obj).forEach((key,i)=>{
if(typeof obj[key] == 'object‘){
constantize(obj[key])
}
})
}
用var定义是会有变量提升的(提到当前作用域的最顶部),而let和const都不能变量提升,因此存在暂时性死区(TDZ);
在同一个作用域中let和const无法使用同名变量或常量,不管是var先定义的,还是let、const先定义的,都无法再次定义同名的;
let、const在块级作用域({})中声明的变量只在代码块内有效:ES6之前只有全局作用域、函数作用域、eval作用域(eval直接调用为当前作用域,间接调用为全局作用域),ES6加入了块级作用域(有{}的都属于块级作用域)。
为什么需要块级作用域?
避免内层变量覆盖外层变量;
避免计数的循环变量泄露成全局变量;
ES5中自执行函数可以直接用块级作用域代替:
// ES5
(function(){
var a = 1;
}())
// ES6
{
let a = 1;
}
块级作用域声明的函数是当作let,但是es6附录里面规定浏览器可以不遵循,因此在某些es6浏览器中函数声明仍然会类似于var声明,所以为了避免环境差异导致行为过大,避免块级作用域内声明函数,如果实在需要可以写成函数表达式而不是函数声明式
在es5中最典型for循环中计数变量泄漏为全局变量:
for (var i = 0; i < clickBoxs.length; i++){
clickBoxs[i].onclick = iteratorFactory(i);
}
原因是因为var声明的变量全局范围内有效,每次循环新的值都会覆盖旧的值,在es5中解决办法是用闭包
function iteratorFactory(i) {
return function(e) {
console.log(i);
}
}
var clickBoxs = document.querySelectorAll('.clickBox')
for (var i = 0; i < clickBoxs.length; i++){
clickBoxs[i].onclick = iteratorFactory(i);
}
而在es6中直接用let代替var定义变量即可,用let声明的每次循环都是新的i值,是js引擎内部会记录上一次循环的值。
复习一下什么是闭包:
当函数中的内部函数被一个变量引用就创建了一个闭包,但闭包实际上是函数和函数内部能访问到的变量(环境)总和就是一个闭包。
function a() {
var i = 0;
function b() {
alert(++i);
}
return b;
}
var c = a();
c();
闭包的作用就是在a执行完成并返回后,闭包使得js的垃圾回收机制GC不会回收a所占用的资源,因为内部函数b的执行需要依赖a,因此整个a就得以保存下来。
js解释器工作机制:
1. 当定义a的时候把a所在的环境设置为a的作用域链(如果a是一个全局函数,则a的scope chain中只有window对象);
2. 执行a的时候,a进入到相应的执行环境,a添加一个scope属性记录a的作用域,创建活动对象(call object)并添加到作用域的最顶端,此时a的作用域链包含了两个对象,a的活动对象和window对象
3. 在a的活动对象添加一个arguments属性,保存调用a时所需传递的参数,并把内部函数b的引用也添加到a的活动对象中(活动对象保存arguments以及内部变量)
b函数也是同理,因此a函数返回函数b给c,函数b的作用域链包含对a活动对象的引入,因此b可以访问a中所有变量和函数,函数b又被c引用,因此函数a不会被gc回收。
当函数b执行的时候先搜索自身的活动对象,如果存在则返回,如果b存在prototype对象,则查找完活动对象会去找自己的原型对象,不存在就搜索a的活动对象,依次查找直到找到为止,如果整个作用域链都无法找到就返回undefined。
函数的作用域是在定义函数的时候决定的,而不是执行函数的时候,但是普通函数的this是在执行函数的时候确定的。
闭包与内存泄漏:闭包的变量本身是我们所要的,其实算不上内存泄漏,而闭包造成的内存泄漏是ie的bug,在闭包使用完之后依然回收不了闭包里面的变量,所以这是ie的问题,不是闭包的问题。
this 不指向函数本身,也不指向函数的词法作用域(定义时候的作用域)而是函数被调用时发生的绑定。
作用域:运行环境给的一段生存环境(栈内存);
作用域链:变量和函数都能提前声明和定义,提前声明和定义放在堆内存中,js从上到下执行,遇到变量就去内存地址中查找,如果有就使用,没有就去父级作用域中查找,直到找到window(如果在浏览器中);
上下文:当前可执行代码的引用;
function foo() {
console.log( this.a );
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函数别名!
var a = "oops, global"; // a是全局对象的属性
bar(); // "oops, global"
function fn(){
console.log(this)
}
document.getElementById('div1').onclick = function (){
console.log(this) //div1
fn()//window
}
document.getElementById('div1').onclick = fn// div1
自执行函数、定时器的this是window。
this的绑定方式:
默认绑定
隐式绑定
obj.foo();
显示绑定
call\bind\apply
如果绑定时候传入null或者undefined则实际是默认绑定
new绑定
var bar = new foo(2);
do表达式:
do { … } 表达式执行一个代码块(包含一个或多个语句),并且返回其中最后一 个语句的结果值,然后赋值给变量 a。 其目的是将语句当作表达式来处理(语句中可以包含其他语句),从而不需要将语句封装 为函数再调用 return 来返回值
本质上,块级作用域是一个语句,将多个操作封装在一起,没有返回值。
{
let t = f();
t = t * t + 1;
}
上面代码中,块级作用域将两个语句封装在一起。但是,在块级作用域以外,没有办法得到t的值,因为块级作用域不返回值,除非t是全局变量。
但是这样写,变量x会得到整个块级作用域的返回值。
let x = do {
let t = f();
t * t + 1;
};
在浏览器中指的是window,self指向顶层对象。
在node环境中指的是global,没有self。
在web worker中没有window,有self。
在es6之前,全局变量和顶层对象的属性是等价的,但是在es6中逐渐将两者剥离开来,因为顶层对象的属性于全局变量挂钩会不利于模块化编程(顶层对象的属性是可以任意处读写的),并且window对象在浏览器中指的是浏览器窗口对象,是一个有实体意义的对象。
因此es6中规定如果还是var和function关键字定义的还是全局变量,如果let/const/class声明的就不属于顶层对象的属性。
为了一段代码可以在不同环境都取得顶层对象,现在有一个提案就是引入global作为顶层对象。
ES module VS commonjs
// commonjs
const {foo} = require('./a.js')
module.exports.foo = "foo";
// ES module
import * as rename from './a.js'
import {a,b} from './a.js'
export default a; //导出一个整体
export const a; //部分导出
ES6 关键字class定义的类其实就是一个语法糖,其绝大多数功能ES5都可以看到,不同的是不存在变量提升。
class Foo{
constructor(x,y){ //构造方法,通过new生成实例对象的时候自动调用
this.x = x; //this代表实例对象
this.y = y
return Object.create(null)//默认返回实例对象
}
toString(){//原型对象属性
return `(${this.x},${this.y})`
}
get prop(){} //get关键字拦截属性取值行为
set prop(value){}//set关键字拦截属性存值修改为
static classMethod(){} //静态方法,只属于类本身,实例无法调用,内部this指向类而不是实例,但是子类可以继承父类的静态方法。class内部无法定义静态属性,只能外部Foo.prop=1;去定义
}
class child extends Foo{
constructor(){
super()// 子类必须调用,因为子类没有自己的this对象,要通过此继承父类的this,因此要调用super才能使用this,相当于 Foo.constructor.call(this);super当作对象调用的时候,普通方法中指向父类的原型对象,静态方法中指向父类
}
} //继承父类
复习JS中的继承:
js中的继承一共六种。
function Father(name){
this.name = name;
}
Father.prototype.sleeping = function (){};
function Child(){};
Child.prototype = new Father();
优点:
1.简单纯粹,父类新增原型方法和原型属性子类都能访问到;
2.实例是子类和父类的实例;
缺点:
1.无法多继承;
2.创建子类实例的时候无法向父类传参;
3.子类新增属性和方法的时候无法;
4.原型对象的所有属性都会被所有实例共享;
function Father(name){
this.name = name;
}
Father.prototype.sleeping = function (){};
function Child(){
Father.call(this);
}
优点:
1.创建子类实例的时候可以向父类传参;
2.可以多继承(多个call);
缺点:
1.实例只是子类的实例,不是父类的实例;
2.只能继承父类的实例属性和方法,无法继承原型属性和方法;
3.无法实现函数复用,每个子类都有父类实例函数的副本,影响性能;
function Father(name){
this.name = name;
}
Father.prototype.sleeping = function (){};
function Child(){
let instance = new Father();
instance.age = 1;
return instance
}
缺点:
1.不支持多继承;
2.实例是父类的实例不是子类的实例;
function Father(name){
this.name = name;
}
Father.prototype.sleeping = function (){};
function Child(){
var _father = new Father()
for(let p in _father){
Child.prototype[p] = _father[p];
}
}
优点:
1.支持多继承;
缺点:
1.效率低,占用内存高;
2.无法获取父类不可枚举方法;
function Father(name){
this.name = name;
}
Father.prototype.sleeping = function (){};
function Child(){
Father.call(this);
}
Child.prototype = new Father()
Child.prototype.constructor = Child(修正构造函数指向)
优点:
1.可以实例继承和原型链继承;
2.即是子类的实例也是父类的实例;
3.函数可复用;
4.可向父类传参;
缺点:
1.生成父类两份实例;
6.寄生组合继承(最优)
function Father(name){
this.name = name;
}
Father.prototype.sleeping = function (){};
function Child(){
Father.call(this);
}
(function(){
let _f = function(){};
_f.prototype = Father.prototype;
Child.prototype = new _f();
Child.prototype.constructor = Child;
})()
优点:
1.可向父类传参;
2.避免生成两份父类实例;
提到原型链继承,顺便就回顾一下JS中的原型及原型链。
在ES6前JS中是没有类的概念,只有对象,继承是对象继承对象而不是类继承类,一个对象除了有自己创建的属性和运行时定义的属性,还享用其原型的对象属性,因此每一个对象都有自己的原型对象,所有对象形成一个树状层级系统,最顶端的root节点是原生对象,只有它没有对象,其他对象都是直接或者间接继承它。
在JS中一切皆对象,那么每一个对象都是通过以对象为模板创建对象,再初始化内部属性。
原型对象值存在于函数对象,因此所有的对象追根溯源找到的都是函数对象,本质上这些对象是通过new Function创建的,而每一个函数对象创建的时候都会自带一个prototype属性,指向其原型对象。
proto是对象的属性,这个属性指向创建这个函数对象的引用类型中继承而来的属性和方法。
比方说通过构造函数创建对象的时候,let b = new A();
new出来的是普通对象,不是函数对象,所以只有proto属性,没有函数对象的prototype属性,但是当这个普通对象的proto指向原型对象,就可以继承来自原型的引用类型的属性和方法。
当let obj = new Function()
函数.proto == Function.prototype
Function.prototype.proto == Object.prototype
Object.proto.proto == null
prototype有一个constructor属性,指向创建当前函数对象的构造函数:
a.prototype.constructor == a;
普通对象:
有proto属性,指向其原型对象,没有prototype属性,原型对象本身也是普通对象。
函数对象:
new Function创建出来的都是函数对象,有用proto属性和prototype属性
总结:实例的内部指针__proto__指向原型对象(prototype),原型对象的constructor指向构造函数,构造函数的prototype指向原型对象。
原型链的概念:
当读取对象方法的时候,会先找对象本身是否有此方法,当找不到,就会找obj.__proto__方法,还是找不到,就会找obj.proto.__proto__一层一层向上找,这个链子就是原型链。
js中的函数复习:
函数的属性:
函数的角色:
箭头函数:
1.省略function关键字,写法更简单;
2.当省略{}的时候可以省略return;
3.this是定义时候的this,与传统的函数执行时候的this不同;
场景:如定时器的this是全局对象,如果用箭头函数的写法就是当前对象
怪异模式:浏览器会按照自己的渲染机制解析代码。
兼容模式:浏览器以宽松向后兼容的方式渲染,防止旧站点无法工作。
严格模式:按照W3C规范以浏览器最高支持模式进行渲染。
Doctype:告诉浏览器用什么文档类型和规范解析文档。
不同的模式对于布局、样式解析、脚本解析都会有不同。
什么时候用严格模式:以优秀开发者要求自己,在不限定特定模式下开发都尽可能用严格模式。
如何使用:
严格模式的内容:
严格模式对正常js语义做了一些更改,在严格模式下会抛出错误来消除一些原本的静默错误并且有时候严格模式下代码运行更快。
严格模式与混杂模式的不同:
es6之前要改变this有三种方法:
fn.call(this,arg1,arg2);
fn.apply(this,[arg1,arg2]);
fn.bind(this,arg1,arg2); // 返回一个函数
//////////待验证
原声js实现call:
Function.prototype.my_call = function (context) {
if(this && this.constructor !== Function){
throw new Error(`${this.name}.my_call is not a function`)
}
context = context || window;
context.fn = this;//eval用间接赋值
let args = [].prototype.slice(context).shift();
eval(`${context.fn}(${args.toString})`)
delete context.fn
};
//////////待验证
原声js实现apply:
Function.prototype.my_apply = functopn (context,arr){
if(this && this.constructor !== function){
throw new Error(`${this.name}.my_apply is not a function`)
}
context = context || window;
context.fn = this;
if(arr && arr.constructor !== Array){
eval(`${context.fn}(${arr})`)
}else if (!arr){
delete context.fn;
}else{
throw new Error('createListFromArrayLike called on non-object')
}
};
//////////待验证
原声js实现bind:
Function.prototype.my_bind = functopn (context){
if (this && this.constructor !== Function)
throw new Error("Function.prototype.mybind - what is trying to be bound is not callable");
let self = this;
let arg = Array.prototype.slice.call(arguments, 1);
function fbound() {
// 普通函数: this => window 构造函数: this => 实例对象
let args = Array.prototype.slice.call(arguments);
self.apply(this instanceof self ? this : context, arg.concat(args));
}
//寄生继承
var FNOP = function () {};
FNOP.prototype = this.prototype;
fbound.prototype = FNOP.prototype;
return fbound;
};
在严格模式下:
'use strict'
fn.call()//严格模式下是this是undefined,普通模式为window
fn.call(null)//严格模式下是this是null,普通模式为window
fn.call(undefined)//严格模式下是this是undefined,普通模式为window
function fn1() {
console.log(1);
}
function fn2() {
console.log(2);
}
fn1.call(fn2) //1
fn1.call.call(fn2) //2
let ary = [1,2,3,3434,23,121,11,2];
1. 排序取值
ary.sort(function()=>{ return a-b;})
let min = ary[0];
let max = ary[ary.length-1];
2. 假设法
let min = ary[0];
let max = ary[1];
for(let i =1;i<ary.length;i++){
let cur = ary[i];
cur > max? max = cur:null;
cur < min? min = cur:null;
}
3. 内置方法
Math.min/Math.min
但是要一个个放入:
Math.min(...ary);
Math.min.apply(null,ary)
eval("Math.min("+ary.toString+")")
把伪数组转数组:
[].slice.call(arguments);
Array.from(arguments);
伪数组排序:
Array.sort.call(arguments,()=>a-b);
[].shift.call(arguments);
[].pop.call(arguments);
原先用unicode表示字符串允许的区间是\u0000——\uFFFF表示,超出这个范围就必须用双字节表示,但是在es6增强了,可以用{}包裹超出这个范围的码点。
‘\u{1F680}’ === ‘\uD83D\uDE80’
``中用${}包裹变量
startsWith():返回布尔值,表示参数字符串是否在源字符串的头部。
endsWith():返回布尔值,表示参数字符串是否在源字符串的尾部。
字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。padStart()用于头部补全,padEnd()用于尾部补全。
‘x’.padStart(5, ‘ab’) // ‘ababx’
‘x’.padStart(4, ‘ab’) // ‘abax’
‘x’.padEnd(5, ‘ab’) // ‘xabab’
‘x’.padEnd(4, ‘ab’) // ‘xaba’
String.raw方法,往往用来充当模板字符串的处理函数,返回一个斜杠都被转义(即斜杠前面再加一个斜杠)的字符串,对应于替换变量后的模板字符串。
String.rawHi\n${2+3}!
;
// “Hi\n5!”
String.rawHi\u000A!
;
// ‘Hi\u000A!’
复习:
字符串两个方法:search()、replace()
var f = function(s) {
return s.replace(/-\w/g, function(x) {
return x.slice(1).toUpperCase();
})
}
正则方法:test()返回布尔
exec()返回匹配结果的正则
修饰符:
i对大小写不敏感
g全局匹配
m多匹配
[]字符集,符合其一即可
[^]不匹配里面字符集
[0-9]
[|]
\转译符
\d 查找数字 \D非数字
\s查找空白字符
\w 等于[a-zA-Z0-9]
\b 匹配单词边界
\uxxxx 查找以16进制规定的unicode字符
+至少一个
在es5中写正则:
var regex = new RegExp('xyz',i);
var regex = /xyz/i;
var regex = new RegExp(/xyz/i);
但是不允许:
var regex = new RegExp(/xyz/,i);
在es6中允许这么写,第二个参数可以写修饰符,如果第一个参数是正则对象且也指定了修饰符,那采取第二个参数的修饰符。
原先math、replace、search、split都是字符串的方法,在es6调为RegExp的对象。
String.prototype.match 调用 RegExp.prototype[Symbol.match]
String.prototype.replace 调用 RegExp.prototype[Symbol.replace]
String.prototype.search 调用 RegExp.prototype[Symbol.search]
String.prototype.split 调用 RegExp.prototype[Symbol.split]
新增y修饰符表粘连(sticky):
y和g相似,也是表示全局匹配,下一次的匹配位置是从上一次匹配成功的下一个位置开始,但是和g不同的是,g只要保证剩余中存在匹配即可,而y必须是从剩余位置的第一个位置开始:
var s = 'aaa_aa_a';
var r1 = /a+/g;
var r2 = /a+/y;
r1.exec(s) // ["aaa"]
r2.exec(s) // ["aaa"]
r1.exec(s) // ["aa"]
r2.exec(s) // null
正则面试题:
js实现千位分隔符:
function format(str){
var regx = /\d{1,3}(?=(\d{3}+$)/g;
return (number + '').replace(regx,'$&,')
}
function isEmail(email) {
var regx = /^([a-zA-Z0-9_\-])+@([a-zA-Z0-9_\-])+(\.[a-zA-Z0-9_\-])+$/;
return regx.test(email);
}
Math.trunc(4.9) // 4
Math.trunc(-4.1) // -4
Math.sign方法用来判断一个数到底是正数、负数、还是零。
Math.cbrt方法用于计算一个数的立方根。
引入了三角函数、指数对数方法。
ES2016 新增了一个指数运算符(**)。
2 ** 2 // 4
2 ** 3 // 8
let a = 1.5;
a **= 2;
// 等同于 a = a * a;
let b = 4;
b **= 3;
// 等同于 b = b * b * b;
要求:等号右边必须是可迭代对象;
let [a,b,c] = [1,2,3];
let [, , third] = [1,2,3];
let [x,y,...z] = ['a']; //x==a,y==undefined,z==[]
let [foo,[[bar],baz]] = [1,[[1],2]];
部分解构:
let [a, [b], d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4
带上默认值:
ES6 内部使用严格相等运算符(===),判断一个位置是否有值。所以,如果一个数组成员不严格等于undefined,默认值是不会生效的
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'
let [x = 1] = [null];//x =null
如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。
function f() {
console.log('aaa');
}
let [x = f()] = [1];
默认值可以引用解构赋值的其他变量,但该变量必须已经声明。
let [x = y, y = 1] = []; // ReferenceError
对象解构赋值和数组不同的是,数组是按顺序取值,对象是通过key
let { baz } = { foo: "aaa", bar: "bbb" }; //baz = undefined
如果变量名与属性名不一致,必须写成下面这样:
var { foo: baz } = { foo: 'aaa', bar: 'bbb' };//baz "aaa"
变量的声明和赋值是一体的,且es6用的是let,下面这种情况就会报错:
let foo;
let {foo} = {foo: 1}; // SyntaxError: Duplicate declaration "foo"
可写成这种形式:
let baz;
({bar: baz} = {bar: 1}); // 成功
这种写法必须要(),否则解释器会认为{}是一个代码块而非结构赋值
const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"
let {length : len} = 'hello';
len // 5
数值和布尔值都会先转换为对象,解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于undefined和null无法转为对象,所以对它们进行解构赋值,都会报错。
let {toString: s} = 123;
s === Number.prototype.toString // true
let {toString: s} = true;
s === Boolean.prototype.toString // true
function add([x, y]){
return x + y;
}
add([1, 2]); // 3
let { id, status, data: number } = jsonData;
对于获取深层级json数据有妙用,避免了使用时用链式写法值为undefined的情况
等号右边必须是可遍历对象(数据结构有Iterator接口,如set也是可遍历对象)常用:数组解构赋值,对象解构赋值,字符串解构赋值。
注意:解构赋值shi浅拷贝
let people2 = {
name: 'ming',
age: 20,
color: ['red', 'blue']
}
let { name, age } = people2;
let [first, second] = people2.color;
let [ , , third] = ["foo", "bar", "baz"];
third // "baz"
//结构赋值可以有默认值
let [x, y = 'b'] = ['a']; // x='a', y='b'
//es6内部用严格相等(===),所以:
let [x = 1] = [undefined];
x // 1
let [x = 1] = [null];
x // null null !== undefined
function f() {
console.log('aaa');
}
let [x = f()] = [1];
//上面代码中,因为x能取到值,所以函数f根本不会执行。上面的代码其实等价于下面的代码。
let x;
if ([1][0] === undefined) {
x = f();
} else {
x = [1][0];
}
//默认值可以引用解构赋值的其他变量,但该变量必须已经声明。
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
let { foo: foo, bar: bar } = { foo: "aaa", bar: "bbb" };
//解构赋值本质是这样,因此如果已经有let声明,再用解构赋值会报错
let foo;
let {foo} = {foo: 1}; // SyntaxError: Duplicate declaration "foo"
//但是用var是可以的
let baz;
({bar: baz} = {bar: 1}); // 成功 圆括号是必须的,否则会报错。因为解析器会将起首的大括号,理解成一个代码块,而不是赋值语句。
// 错误的写法
let x;
{x} = {x: 1};
// SyntaxError: syntax error
avaScript引擎会将{x}理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免JavaScript将其解释为代码块,才能解决这个问题。
var node = {
loc: {
start: {
line: 1,
column: 5
}
}
};
var { loc: { start: { line }} } = node;
line // 1
loc // error: loc is undefined
start // error: start is undefined
上面代码中,只有line是变量,loc和start都是模式,不会被赋值。
解构赋值时,如果等号右边是数值和布尔值,则会先转为对象。
let {toString: s} = 123;
s === Number.prototype.toString // true
let {toString: s} = true;
s === Boolean.prototype.toString // true
//函数参数的解构赋值
[[1, 2], [3, 4]].map(([a, b]) => a + b);
// [ 3, 7 ]
用途:
交换变量的值
let x = 1;
let y = 2;
[x, y] = [y, x];
如果只想获取键名,或者只想获取键值,可以写成下面这样。
// 获取键名
for (let [key] of map) {
// ...
}
// 获取键值
for (let [,value] of map) {
// ...
}
Array.from();将类数组和可迭代对象(set、map)转换为数组,一般操作nodeList集合(querySelectorAll)或者arguments;
拓展运算符也可以进行转换,如[...arguments] 用的原理是Iterator接口
Array.from还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组。
Array.from(arrayLike, x => x * x);
// 等同于
Array.from(arrayLike).map(x => x * x);
Array.from([1, 2, 3], (x) => x * x)
// [1, 4, 9]
如果map函数里面用到了this关键字,还可以传入Array.from的第三个参数,用来绑定this。
Array.from({ length: 2 }, () => 'jack')
// ['jack', 'jack']
Array.for() 也可以写成Array()
Array() // []
Array(3) // [, , ,] 当一个参数的时候指数组长度
Array(3, 11, 8) // [3, 11, 8]
推荐用此方法,此是为了弥补Array()的不足,参数不同会导致行为有差异
Array.of() // []
Array.of(undefined) // [undefined]
Array.of(1) // [1]
Array.of(1, 2) // [1, 2]
//此方法会修改当前数组
Array.copyWithin(targetIndex,startIndex,endIndex);
[1, 2, 3, 4, 5].copyWithin(0, 3)
// [4, 5, 3, 4, 5]
//比起indexof来说可以识别(找到NaN),并且第二个参数可以用来绑定回调函数的this
Array.find()
Array.findIndex()
Array.fill();指定值填充数组,第二个第三个参数用于填充的起始和结束位置
new Array(3).fill(7)
['a', 'b', 'c'].fill(7, 1, 2)
// ['a', 7, 'c']
Array.entires();
Array.keys();
Array.values();
都返回一个遍历器对象,用于遍历数组:
for (let index of ['a', 'b'].keys()) {
console.log(index);
}
// 0
// 1
for (let elem of ['a', 'b'].values()) {
console.log(elem);
}
// 'a'
// 'b'
for (let [index, elem] of ['a', 'b'].entries()) {
console.log(index, elem);
}
// 0 "a"
// 1 "b"
如果不使用for...of循环,可以手动调用遍历器对象的next方法,进行遍历。
let letter = ['a', 'b', 'c'];
let entries = letter.entries();
console.log(entries.next().value); // [0, 'a']
console.log(entries.next().value); // [1, 'b']
console.log(entries.next().value); // [2, 'c']
Array.prototype.includes方法返回一个布尔值,表示某个数组是否包含给定的值
没有该方法之前,我们通常使用数组的indexOf方法,检查是否包含某个值。
该方法的第二个参数表示搜索的起始位置,默认为0。
indexOf方法有两个缺点,一是不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于-1,表达起来不够直观。二是,它内部使用严格相当运算符(===)进行判断,这会导致对NaN的误判。
[NaN].indexOf(NaN)
// -1
includes使用的是不一样的判断算法,就没有这个问题。
[NaN].includes(NaN)
// true
另外,Map和Set数据结构有一个has方法,需要注意与includes区分。
Map结构的has方法,是用来查找键名的,比如Map.prototype.has(key)、WeakMap.prototype.has(key)、Reflect.has(target, propertyKey)。
Set结构的has方法,是用来查找值的,比如Set.prototype.has(value)、WeakSet.prototype.has(value)。
Array.map()
Array.filter()
Array.some()
Array.forEach()
Array.every()
es6将空位视为undefined,es5每个方法处理空位不同,因此尽量避免空位。
function foo(num = 200) {
return num;
}
注意事项:
1.形参定义的不能再用let\const在函数作用域内重新定义;
2.
function foo({x, y = 5}) {
console.log(x, y);
}
这个是结构赋值的默认值,不是函数形参的默认值;
3. 函数默认值要写在最后才有意义,如果写在非尾部,那么想省略的时候是fn(3, , ,2),但是这样的写法是不被允许的,因此函数形参写在最后才有意义;
4. 传入函数参数如果传入undefined会触发默认值,传入null则不会触发;
5. 函数.length属性指的是预期传入的参数,如果函数是有默认值,有默认值后面的形参个数都不计入(function (a = 0, b, c) {}).length // 0,如果函数形参定义的是rest参数,(function(...args) {}).length // 0 那么length为0
6. 函数默认值如果是变量的话,那么在赋值的时候是在函数声明的时候(预解释)的时候就进行。
7. function f(y = x) {
let x = 2;
console.log(y);
}
f() // ReferenceError: x is not defined
在预解释的时候,函数内部无x全局也无x,因此会报ReferenceError
8.
下面这样写,也会报错。
var x = 1;
function foo(x = x) {
// ...
}
foo() // ReferenceError: x is not defined
上面代码中,参数x = x形成一个单独作用域。实际执行的是let x = x,由于暂时性死区的原因,这行代码会报错”x 未定义“。
因为是用let定义的,在let定义之前就用自己定义的x所以会报TDZ的原因。
9.
var x = 1;
function foo(x, y = function() { x = 2; }) {
var x = 3;
y();
console.log(x);
}
foo() // 3
x // 1
函数声明的时候会开辟一个局部作用域,声明结束后作用域会释放,所以在形参y的默认值匿名函数中的x是第一个形参x,于内部作用域和全局作用域的x没有关系
9. 默认参数的应用:
10.function throwIfMissing() {
throw new Error('Missing parameter');
}
function foo(mustBeProvided = throwIfMissing()) {
return mustBeProvided;
}
foo()
// Error: Missing parameter
如果没有传入参数,就会报错
function foo(optional = undefined) { ··· }
如果将形参设置为undefined,则代表此参数可以省略。
11.es6中规定,如果函数参数用了结构赋值默认值就不能在函数体内定义use strict,因为在函数中用严格模式是对函数参数和函数体是同时生效的,但是引擎先解析函数参数,无法得知函数体内用了严格模式
形式为…变量名(变量代表一个数组)
function foo(x, y, ...rest) {
//es5中的arguments不是真正的数组,而rest是真正的数组
}
rest使用场景:
1.与结构赋值一起用:
const [first, ...rest] = [1, 2, 3, 4, 5];
2. 数组合并:[...arr1, ...arr2, ...arr3]
拓展运算符(spread)形式是三个点(…),好比rest参数的逆运算。
如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。
const […butLast, last] = [1, 2, 3, 4, 5];
// 报错
const [first, …middle, last] = [1, 2, 3, 4, 5];
// 报错
拓展运算符可以用于字符串,且能够正确识别32位的Unicode字符:
‘x\uD83D\uDE80y’.length // 4
[…‘x\uD83D\uDE80y’].length // 3 能正确识别2个字符
正确返回字符串长度的函数,可以像下面这样写。
function length(str) {
return […str].length;
}
length(‘x\uD83D\uDE80y’) // 3
只要实现了Itreater接口的对象都可以用拓展运算符转成真正的数组(对象没有部署此接口)。
1.不能当作构造函数,不能new
2.不能使用yield
3.this是定义时的this,不是运算时的this
4.不可以使用arguments,可以使用rest参数
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,所以不管包几层,指的都是f的this
箭头函数的应用,pipeline的原理:
const pipeline = (…funcs) =>
val => funcs.reduce((a, b) => b(a), val);
const plus1 = a => a + 1;
const mult2 = a => a * 2;
const addThenMult = pipeline(plus1, mult2);
addThenMult(5)
// 12
绑定this:
在es5中是通过call、bind、apply去绑定this,在es6中用this就无需绑定this,到了es7有绑定this运算符:
foo::bar(…arguments);
// 等同于
bar.apply(foo, arguments);
如果双冒号左边为空,右边是一个对象的方法,则等于将该方法绑定在该对象上面。
var method = obj::obj.foo;
// 等同于
var method = ::obj.foo;
let log = ::console.log;
// 等同于
var log = console.log.bind(console);
由于双冒号运算符返回的还是原对象,因此可以采用链式写法。
document.querySelectorAll(“div.myClass”)
::find(“p”)
::html(“hahaha”);
Tail call尾调用
概念:函数的最后一步是调用另一个函数,即尾调用
如果调用后还有其他操作就不属于尾调用:
function f(x){
return g(x) + 1;//还有赋值所以不属于尾调用
}
function f(x){
g(x); //函数最后一步是return,所以这类事return undefined,因此也不算尾调用
}
尾调用优化:
函数在调用的时候会形成调用帧,那么如果多次调用就会形成调用栈,如果用尾调用就能节省内存,那么如递归这类多次调用函数的就可以用尾递归去优化
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}
可以优化成这样:
function factorial(n, total) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
尾递归在严格模式下才可以实现优化,因为arguments,caller这两个可以跟踪函数的调用栈,而尾递归中函数调用栈改写,只有在严格模式下这两个参数才禁用
那么在非严格模式下进行优化,蹦床函数:
let color = ['red', 'yellow'];
let colorful = [...color, 'green', 'blue'];
console.log(colorful); // ["red", "yellow", "green", "blue"]
et num = [1, 3, 5, 7, 9];
let [first, second, ...rest] = num;
console.log(rest); // [5, 7, 9]
// ES5
function people(name, age) {
return {
name: name,
age: age
};
}
// ES6
function people(name, age) {
return {
name,
age
};
}
// ES5
var people1 = {
name: 'bai',
getName: function () {
console.log(this.name);
}
};
// ES6
let people2 = {
name: 'bai',
getName () {
console.log(this.name);
}
};
Object.assign(target,origin1,origin2);任意多个源对象的可枚举属性拷贝给目标对象
如果某个方法的值是一个Generator函数,前面需要加上星号。
var obj = {
* m(){
yield 'hello world';
}
};
当用字面量形式定义对象的时候可以用表达式:
let propKey = 'foo';
let obj = {
[propKey]: true,
['a' + 'bc']: 123
};
并且在取值的时候也可以用表达式:
let lastord = 'apple';
a[lastWord]
但是属性名简写和变量不可以同时使用:
var baz = { [foo] };
Object.is();
在es5中比较值相等,有==,和===,前者会自动转换数据类型,后者NaN不等于NaN,而Object.is()的核心概念就是值相等既相等(引用类型仍然是比较内存地址)
特殊:
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
Object.assign(target,source1,source2);
对象合并,是浅拷贝,遇到同名属性,替换
如果target不是对象,会转为对象,如果不能转,如null或者undefined,会报错
如果是source为非对象并且不能转换为对象,那么会略过此参数
有些函数库比如Loadsh会把Object.assign可以处理成深拷贝
Object.assign()的用途:
class Pointer{
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){
let originProto = Object.getPrototyoeOf(origin);//获取原型属性
return Object.assign(Object.create(originProto),origin);//拷贝原始对象自身的值
}
可以为属性指定默认值:
const DEFAULTS = {
logLevel: 0,
outputFormat: 'html'
};
function processContent(options) {
options = Object.assign({}, DEFAULTS, options);
}
获取对象的某个属性的描述信息
Object.getOwnPropertyDescriptor(obj,'foo');
得到值:
{
value:1,
writable:true,
enumerable:true,
configurable:true
}
当enumerable为true则代表可枚举,此时就可以通过for in,object.keys()等方法
Reflect.keys(obj) 返回包含对象所有key的数组
Object.entries只输出属性名非 Symbol 值的属性。将来可能会有Reflect.ownEntries()方法,返回对象自身的所有属性。
const firstName = (message
&& message.body
&& message.body.user
&& message.body.user.firstName) || 'default';
可以写成这样:
const firstName = message?.body?.user?.firstName || 'default';
“Null 传导运算符”有四种用法。
obj?.prop // 读取对象属性
obj?.[expr] // 同上
func?.(...args) // 函数或对象方法的调用
new C?.(...args) // 构造函数的调用
是js的第七种数据类型,继Number、String、Null、Boolean、Undefined、Object
之后的原始数据类型。
特定及用处:
let s = Symbol()
typeof s //'symbol'
symbol是一种数据类型,不是对象,所以不能用new
当传入参数,表示的是symbol的描述
let s = Symbol('fppp')
s.toString() //fppp 既将symbol转为字符串,则调用它的参数,如果symbol的参数是一个对象,则调用这个对象的toString
const obj = {
toString() {
return 'abc';
}
};
const sym = Symbol(obj);
sym // Symbol(abc)
各个symbol都是各不相同的,
var s1 = Symbol('foo');
var s2 = Symbol('foo');
s1 === s2 // false
不管symbol有没有参数都是不相同的
2. symbol不能和其他数据类型做运算
"your symbol is " + sym
// TypeError: can't convert symbol to string
虽然不能计算,但是symbol可以转换为其他数据类型
转为字符串:
var sym = Symbol('My symbol');
String(sym) // 'Symbol(My symbol)'
转为布尔:
var sym = Symbol();
Boolean(sym) // true
3. 当symbol作为对象的属性时的写法:
let mySymbol = Symbol();
写法一:let a = {}; a[mySymbol] = 'hello';
写法二:let a = {[mySymbol]:'hello'}//要写[]的原因是因为symbol不是字符串
写法三:let a = {}; Object.defineProperty(a,mySymbol,{value:'hello'});
当对象的属性名为symbol的时候不能用点写法。
4.魔术字符串
魔术字符串是指与代码强耦合的具体字符串和数字,为了代码含义更清晰以及便于维护一般都是讲魔术字符串写成变量的形式
jsrun中的列子:
var shapeType = {
triangle: 'Triangle'
};
function getArea(shape, options) {
var area = 0;
switch (shape) {
case shapeType.triangle:
area = .5 * options.width * options.height;
break;
}
return area;
}
getArea(shapeType.triangle, { width: 100, height: 100 });
上面代码中,我们把“Triangle”写成shapeType对象的triangle属性,这样就消除了强耦合。
如果仔细分析,可以发现shapeType.triangle等于哪个值并不重要,只要确保不会跟其他shapeType属性的值冲突即可。因此,这里就很适合改用Symbol值。
const shapeType = {
triangle: Symbol()
};
上面代码中,除了将shapeType.triangle的值设为一个Symbol,其他地方都不用修改。
symbol作为属性名的时候,在遍历属性的时候如果是通过for in,for of,Object.getOwnPropertyNames()等返回,但是它不是私有属性,要获取symbol作为属性名的要通过Object.getOwnPropertySymbols,获取所有symbol属性名的数组
要获取所有键名,需要通过Reflect.keys()
但是可以利用这个特性用symbol去定义非私有但是又只想内部访问的属性
var size = Symbol('size');
class Collection {
constructor() {
this[size] = 0;
}
}
Symbol每次调用就创建一个新的值,而Symbol.for()通过传入参数,如果值不存在,会新创建一个,如果值不存在,会返回一个全局注册的值
用Symbol.for("cat")30次,每次都会返回同一个 Symbol 值,但是调用Symbol("cat")30次,会返回30个不同的Symbol值。
Symbol.for("bar") === Symbol.for("bar")
// true
Symbol("bar") === Symbol("bar")
// false
Symbol.keyFor方法返回一个已登记的 Symbol 类型值的key。
var s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"
Symbol.for为Symbol值登记的名字,是全局环境的,可以在不同的 iframe 或 service worker 中取到同一个值。
jsrun提供的一个实例模式:
任何时候调用同一个类,返回的都是同一个实例
对你node来说每一个文件都是一个类,怎么保证每次执行这个文件都是返回同一个实例
很容易想到的是挂在在全局上
// mod.js
function A() {
this.foo = 'hello';
}
if (!global._foo) {
global._foo = new A();
}
module.exports = global._foo;
但是有个问题就是全局属性是可写的,任何文件都可以修改,要不能修改就可以用symbol,类似只读不可写
为了防止这种情况出现,我们就可以使用Symbol。
// mod.js
const FOO_KEY = Symbol.for('foo');
function A() {
this.foo = 'hello';
}
if (!global[FOO_KEY]) {
global[FOO_KEY] = new A();
}
module.exports = global[FOO_KEY];
上面代码中,可以保证global[FOO_KEY]不会被无意间覆盖,但还是可以被改写。
var a = require('./mod.js');
global[Symbol.for('foo')] = 123;
如果键名使用Symbol方法生成,那么外部将无法引用这个值,当然也就无法改写。
// mod.js
const FOO_KEY = Symbol('foo');
// 后面代码相同 ……
js中的新的数据结构,接收数组或者类数组作为参数:
数组去重[…new Set(Array)]
set添加值的时候用的是精准运算符,类似===,但是和全等的区别是NaN等于自身
如果set.add({}),set.add({})两次,那么set的size为2,因为对象和对象是不相等的
set.add()//返回数据本身
set.has()//false
set.delete()//返回数据
set.clear() 清除所有值
set的遍历也有set.keys()、set.values()、set.entires()、set.forEach()
set取并集:
let union = new Set([...a,...b]);
set去交集:
let intersect = new Set([...a].filter(x=>b.has(x)));
set取差集:
let difference = new Set([...a].filter(x=>!b.has(x)));
想改变set结构:
let set = new Set([...set].map(x=>x*2));
或者
let set = new Set(Array.from(set,x=>x*2));
WeakSet
特点:
1.和set一样是不重复的值的集合;
2.weakest的成员只能是弱引用的对象,因此weakest不可遍历(因为随时可能被垃圾回收机制回收);
为什么要有map,因为js中的对象,本质上是键值对的组合,而key一般都是字符串的形式,这就会有很大的限制,而map把对象的字符串-值变成值-值,是一种更完善的hash结构
map方法:
map.size()
map.has()
map.get()
map.forEach()
如果key是引用数据类型,但是内存地址不一样,是被认为是两个键值对,这样就解决了同名属性的问题
将map数据结构转换为数组最快也是拓展运算符:
[…map.keys()]
[…map.values()]
[…map.entires()]
WeakMaps
1.和map的区别就是只接受对象作为键名;a
2.不计入垃圾回收机制;
因为不能遍历,因此WeakMap只有四个方法可用:get()、set()、has()、delete()。
什么是内存泄漏:
当一块内存不再被应用程序使用的时候,由于某种原因并没有返回给操作系统或者空闲内存池的现象。
显示内存管理是程序员告诉编译器何时不再需要某块内存,而js是垃圾回收语言中的一员,由j s引擎周期性的检查是否分配出去的内存是否可以释放。不管是哪一种,都是由内存泄漏的风险。
垃圾回收机制:
垃圾收集器使用mark-and-sweap的算法,首先建立一个根节点列表,根节点通常是那些被保留在代码中的全局变量,比如js中的window,所有子节点被递归检查,每块可以从根节点被访问的内存都不会视为垃圾,反之标记为垃圾,而垃圾收集器就会释放这些内存并将它们返回给操作系统,不同的垃圾回收机制有点区别,但是大致都是如此,本质就是可以访问的内存被标记为非内存,其余视为垃圾。
所以在js中的内存泄漏:
1.意外的全局变量;
2.被遗漏的定时器setIinternal或者回调函数(观察者模式);
3.闭包;
用于修改某些操作的默认行为,等同于对语言层面作出修改,所以属于一种元编程(meta programming),即对编程语言进行编程。
proxy直译为代理,但是可以理解成架设一层拦截,外层对该对象的访问都必须先通过这层拦截,因此可以对外界的访问进行过滤和改写。
如果一个属性不可配置(configurable)和不可写(writable),则该属性不能被代理,通过 Proxy 对象访问该属性会报错。
es6原声提供proxy构造函数,用来生成proxy的实例:
let proxy = new Proxy(target, handler);
fe:
let proxy = new Proxy({},{
get:function(get,property){};
})
要注意的是,proxy是针对Proxy的实例,而不是对目标对象
proxy实例也可以作为其他对象的原型对象:
let proxy = new Proxy({},{
get:function (){};
})
let obj = Object.create(proxy)
1)get(target, propKey, receiver)
拦截对象属性的读取,比如proxy.foo和proxy['foo']。
最后一个参数receiver是一个对象,可选
利用 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
上面代码设置 Proxy 以后,达到了将函数名链式使用的效果。
(2)set(target, propKey, value, receiver)
拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v,返回一个布尔值。
(3)has(target, propKey)
拦截propKey in proxy的操作,返回一个布尔值。
(4)deleteProperty(target, propKey)
拦截delete proxy[propKey]的操作,返回一个布尔值。
(5)ownKeys(target)
拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy),返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
(6)getOwnPropertyDescriptor(target, propKey)
拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
(7)defineProperty(target, propKey, propDesc)
拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
(8)preventExtensions(target)
拦截Object.preventExtensions(proxy),返回一个布尔值。
(9)getPrototypeOf(target)
拦截Object.getPrototypeOf(proxy),返回一个对象。
(10)isExtensible(target)
拦截Object.isExtensible(proxy),返回一个布尔值。
(11)setPrototypeOf(target, proto)
拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。
如果目标对象是函数,那么还有两种额外操作可以拦截。
(12)apply(target, object, args)
拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
(13)construct(target, args)
拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)。
proxy的this问题:
proxy不是透明代理,即不做任何拦截的情况下无法与目标对象的行为一直,因为目标对象的this会指向proxy代理。
const _name = new WeakMap();
class Person {
constructor(name) {
_name.set(this, name);
}
get name() {
return _name.get(this);
}
}
const jane = new Person('Jane');
jane.name // 'Jane'
const proxy = new Proxy(jane, {});
proxy.name // undefined
因此要解决这个办法内部要绑定this:
const target = new Date('2015-01-01');
const handler = {
get(target, prop) {
if (prop === 'getDate') {
return target.getDate.bind(target);
}
return Reflect.get(target, prop);
}
};
const proxy = new Proxy(target, handler);
proxy.getDate() // 1
设计此对象的目的:
var loggedObj = new Proxy(obj, {
get(target, name) {
console.log('get', target, name);
return Reflect.get(target, name);
},
deleteProperty(target, name) {
console.log('delete' + name);
return Reflect.deleteProperty(target, name);
},
has(target, name) {
console.log('has' + name);
return Reflect.has(target, name);
}
});
上面代码中,每一个Proxy对象的拦截操作(get、delete、has),内部都调用对应的Reflect方法,保证原生行为能够正常执行。添加的工作,就是将每一个操作输出一行日志。
有了Reflect对象以后,很多操作会更易读。
// 老写法
Function.prototype.apply.call(Math.floor, undefined, [1.75]) // 1
// 新写法
Reflect.apply(Math.floor, undefined, [1.75]) // 1
Reflect对象一共有13个静态方法。
Reflect.apply(target,thisArg,args)
Reflect.construct(target,args)
Reflect.get(target,name,receiver)
Reflect.get方法查找并返回target对象的name属性,如果没有该属性,则返回undefined
Reflect.set(target,name,value,receiver)
Reflect.defineProperty(target,name,desc)
Reflect.deleteProperty(target,name)
Reflect.has(target,name)
Reflect.ownKeys(target)
Reflect.isExtensible(target)
Reflect.preventExtensions(target)
Reflect.getOwnPropertyDescriptor(target, name)
Reflect.getPrototypeOf(target)
Reflect.setPrototypeOf(target, prototype)
var promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);//将promise的对象从未完成变成成功
} else {
reject(error);//将promise的对象从未完成变成失败
}
});
Promise.prototype.then()
then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。
Promise.prototype.catch()
此方法是.then(null, rejection)的别名,用于指定发生错误时的回调函数。
getJSON('/posts.json').then(function(posts) {
// ...
}).catch(function(error) {
// 处理 getJSON 和 前一个回调函数运行时发生的错误
console.log('发生错误!', error);
});
上面代码中,getJSON方法返回一个 Promise 对象,如果该对象状态变为Resolved,则会调用then方法指定的回调函数;如果异步操作抛出错误,状态就会变为Rejected,就会调用catch方法指定的回调函数,处理这个错误。另外,then方法指定的回调函数,如果运行中抛出错误,也会被catch方法捕获。
Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。
getJSON('/post/1.json').then(function(post) {
return getJSON(post.commentURL);
}).then(function(comments) {
// some code
}).catch(function(error) {
// 处理前面三个Promise产生的错误
});
上面代码中,一共有三个Promise对象:一个由getJSON产生,两个由then产生。它们之中任何一个抛出的错误,都会被最后一个catch捕获。
一般来说,不要在then方法里面定义Reject状态的回调函数(即then的第二个参数),总是使用catch方法。
// bad
promise
.then(function(data) {
// success
}, function(err) {
// error
});
// good
promise
.then(function(data) { //cb
// success
})
.catch(function(err) {
// error
});
Promise.all方法用于将多个Promise实例,包装成一个新的Promise实例。
Promise.all方法的参数可以不是数组,但必须具有Iterator接口,且返回的每个成员都是Promise实例。
(1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
(2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
类似&&
Promise.race()
Promise.race方法同样是将多个Promise实例,包装成一个新的Promise实例。
var p = Promise.race([p1, p2, p3]);
上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。
Promise.resolve()将现有对象转为Promise对象
done()
Promise对象的回调链,不管以then方法或catch方法结尾,要是最后一个方法抛出错误,都有可能无法捕捉到(因为Promise内部的错误不会冒泡到全局)。因此,我们可以提供一个done方法,总是处于回调链的尾端,保证抛出任何可能出现的错误。
finally()
finally方法用于指定不管Promise对象最后状态如何,都会执行的操作。它与done方法的最大区别,它接受一个普通的回调函数作为参数,该函数不管怎样都必须执行。
目前js有四种表示集合的数据结构,数组/对象/map/set,除了自身还有组合的使用,那么就需要统一的接口去处理不同的数据结构。
遍历器(iterator)就是一种为各种不同数据结构提供统一访问的接口,任何数据只要部署了iterator接口就可以完成遍历操作。
iterator的遍历过程:
创建一个指针对象,指向当前数据结构的起始位置(遍历器对象本质上就是一个指针对象)-》第一次调用指针对象的next方法,将指针指向数据结构的第一个成员,再次调用时候指向下一个成员
每次调用next方法都会返回数据结构当前的成员信息(返回一个包含value和done两个属性的对象,value是当前属性的布尔值,done是布尔值,表示是否遍历结束)
在ES6中,有些数据结构原生具备Iterator接口(比如数组),即不用任何处理,就可以被for…of循环遍历,有些就不行(比如对象)。原因在于,这些数据结构原生部署了Symbol.iterator属性(详见下文),另外一些数据结构没有。凡是部署了Symbol.iterator属性的数据结构,就称为部署了遍历器接口。调用这个接口,就会返回一个遍历器对象。
let obj = {
data: [ 'hello', 'world' ],
[Symbol.iterator]() {
const self = this;
let index = 0;
return {
next() {
if (index < self.data.length) {
return {
value: self.data[index++],
done: false
};
} else {
return { value: undefined, done: true };
}
}
};
}
};
只要对象有Symbol.iterator属性就能遍历
generator可以理解成一个状态机,内部封装了多个状态,而执行generator函数会返回一个遍历器对象,所以generator除了状态机还是一个遍历器对象生成函数
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
1. 关键字和函数名之间有一个*;
2. 内部用yield定义不同内部状态;
3. 如果没有再遇到新的yield语句,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值
yield语句后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,因此等于为JavaScript提供了手动的“惰性求值”(Lazy Evaluation)的语法功能
任意一个对象的Symbol.iterator方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。
由于Generator函数就是遍历器生成函数,因此可以把Generator赋值给对象的Symbol.iterator属性,从而使得该对象具有Iterator接口。
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1, 2, 3]
yield句本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield语句的返回值。
这个功能有很重要的语法意义。Generator 函数从暂停状态到恢复运行,它的上下文状态(context)是不变的。通过next方法的参数,就有办法在 Generator 函数开始运行之后,继续向函数体内部注入值。也就是说,可以在 Generator 函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。
function* foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}
var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }
上面代码中,第二次运行next方法的时候不带参数,导致y的值等于2 * undefined(即NaN),除以3以后还是NaN,因此返回对象的value属性也等于NaN。第三次运行Next方法的时候不带参数,所以z等于undefined,返回对象的value属性等于5 + NaN + undefined,即NaN。
如果向next方法提供参数,返回结果就完全不一样了。上面代码第一次调用b的next方法时,返回x+1的值6;第二次调用next方法,将上一次yield语句的值设为12,因此y等于24,返回y / 3的值8;第三次调用next方法,将上一次yield语句的值设为13,因此z等于13,这时x等于5,y等于24,所以return语句的值等于42。
由于next方法的参数表示上一个yield语句的返回值,所以第一次使用next方法时,不能带有参数
如果想要第一次调用next方法时,就能够输入值,可以在Generator函数外面再包一层。
function wrapper(generatorFunction) {
return function (...args) {
let generatorObject = generatorFunction(...args);
generatorObject.next();
return generatorObject;
};
}
const wrapped = wrapper(function* () {
console.log(`First input: ${yield}`);
return 'DONE';
});
wrapped().next('hello!')
// First input: hello!
for...of循环可以自动遍历Generator函数时生成的Iterator对象,且此时不再需要调用next方法。
function *foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}
for (let v of foo()) {
console.log(v);
}
// 1 2 3 4 5
利用for...of循环,可以写出遍历任意对象(object)的方法。原生的JavaScript对象没有遍历接口,无法使用for...of循环,通过Generator函数为它加上这个接口,就可以用了。
function* objectEntries(obj) {
let propKeys = Reflect.ownKeys(obj);
for (let propKey of propKeys) {
yield [propKey, obj[propKey]];
}
}
let jane = { first: 'Jane', last: 'Doe' };
for (let [key, value] of objectEntries(jane)) {
console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe
上面代码中,对象jane原生不具备Iterator接口,无法用for...of遍历。这时,我们通过Generator函数objectEntries为它加上遍历器接口,就可以用for...of遍历了。加上遍历器接口的另一种写法是,将Generator函数加到对象的Symbol.iterator属性上面。
js是单线程,实现异步编程就是通过回调函数。
promise不是新语法,只是回调嵌套写法的优化,Generator是比promise更好的写法。
coroutine(协程):多个线程互相协作,完成异步任务。
generator就是协程在es6的实现,最大的特点就是可以交出函数的执行权(暂停函数),整个generator就是封装的异步任务,或者说是异步任务的容器,异步任务重需要暂停的都用yield去语句注明。
调用generator函数,不会返回结果,而是返回一个指针对象,调用指针的next方法,会移动内部指针到第一个yield,即x+2,每次调用next会返回一个对象,包含value和done,value属性是yield后面的表达值,done表示的是generator函数是否执行完毕
function* gen(x) {
var y = yield x + 2;
return y;
}
var g = gen(1);
g.next() // { value: 3, done: false }
g.next() // { value: undefined, done: true }
generator函数可以暂停函数和恢复函数,这是其能封装异步任务的根本原因,除此之外还能函数体外数据交换以及错误处理机制。next返回的value就是向函数体外输出的数据,而next接收的参数就是向generator函数体内输入的数据。
function* gen(x){
var y = yield x + 2;
return y;
}
var g = gen(1);
g.next() // { value: 3, done: false } 1+2为3,被next输出到函数体外
g.next(2) // { value: 2, done: true } next的参数2为上个异步阶段的返回值,即y的值,就是这一步的value值
generator还可以部署错误处理代码,出错的代码与处理错误的代码可以实现时间与空间的分离,对异步编程十分重要。
function* gen(x){
try {
var y = yield x + 2;
} catch (e){
console.log(e);
}
return y;
}
var g = gen(1);
g.next();
g.throw('出错了'); 这里抛出的错误可以被函数体内的try catch所捕获
// 出错了
使用generator实现异步任务的封装:
var fetch = require('node-fetch');
function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}
var g = gen();
var result = g.next();
result.value.then(function(data){ //使用then是因为fetch返回的是promise对象
return data.json();
}).then(function(data){
g.next(data);
});
thunk函数是自动执行generator函数的一种方法,参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数。
一种意见是"传值调用"(call by value),即在进入函数体之前,就计算x + 5的值(等于6),再将这个值传入函数f。C语言就采用这种策略。
f(x + 5)
// 传值调用时,等同于
f(6)
另一种意见是“传名调用”(call by name),即直接将表达式x + 5传入函数体,只在用到它的时候求值。Haskell 语言采用这种策略。
f(x + 5)
// 传名调用时,等同于
(x + 5) * 2
thunk是传名调用:
function f(m) {
return m * 2;
}
f(x + 5);
// 等同于
var thunk = function () {
return x + 5;
};
function f(thunk) {
return thunk() * 2;
}
JavaScript 语言是传值调用,它的 Thunk 函数含义有所不同。在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数。
// 正常版本的readFile(多参数版本)
fs.readFile(fileName, callback);
// Thunk版本的readFile(单参数版本)
var Thunk = function (fileName) {
return function (callback) {
return fs.readFile(fileName, callback);
};
};
var readFileThunk = Thunk(fileName);
readFileThunk(callback);
任何函数,只要参数有回调函数,就能写成 Thunk 函数的形式。下面是一个简单的 Thunk 函数转换器。
// ES5版本
var Thunk = function(fn){
return function (){
var args = Array.prototype.slice.call(arguments);
return function (callback){
args.push(callback);
return fn.apply(this, args);
}
};
};
// ES6版本
var Thunk = function(fn) {
return function (...args) {
return function (callback) {
return fn.call(this, ...args, callback);
}
};
};
var readFileThunk = Thunk(fs.readFile);
readFileThunk(fileA)(callback);
var fs = require('fs');
var thunkify = require('thunkify');//生产环境用此模块
var readFileThunk = thunkify(fs.readFile);
var gen = function* (){
var r1 = yield readFileThunk('/etc/fstab');
console.log(r1.toString());
var r2 = yield readFileThunk('/etc/shells');
console.log(r2.toString());
};
手动执行:
var g = gen();
var r1 = g.next();
r1.value(function (err, data) {
if (err) throw err;
var r2 = g.next(data);
r2.value(function (err, data) {
if (err) throw err;
g.next(data);
});
});
Thunk 函数的自动流程管理
Thunk 函数真正的威力,在于可以自动执行 Generator 函数。下面就是一个基于 Thunk 函数的 Generator 执行器。
function run(fn) {
var gen = fn();
function next(err, data) {
var result = gen.next(data);
if (result.done) return;
result.value(next);
}
next();
}
function* g() {
// ...
}
run(g);
有了这个执行器,执行 Generator 函数方便多了。不管内部有多少个异步操作,直接把 Generator 函数传入run函数即可。当然,前提是每一个异步操作,都要是 Thunk 函数,也就是说,跟在yield命令后面的必须是 Thunk 函数。
var g = function* (){
var f1 = yield readFile('fileA');
var f2 = yield readFile('fileB');
// ...
var fn = yield readFile('fileN');
};
run(g);
没研究懂//这里待续
async其实是generator的语法糖
前文有一个 Generator 函数,依次读取两个文件。
var fs = require(‘fs’);
var readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function(error, data) {
if (error) reject(error);
resolve(data);
});
});
};
var gen = function* () {
var f1 = yield readFile(’/etc/fstab’);
var f2 = yield readFile(’/etc/shells’);
console.log(f1.toString());
console.log(f2.toString());
};
写成async函数,就是下面这样。
var asyncReadFile = async function () {
var f1 = await readFile(’/etc/fstab’);
var f2 = await readFile(’/etc/shells’);
console.log(f1.toString());
console.log(f2.toString());
};
一比较就会发现,async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await,仅此而已。
async函数对 Generator 函数的改进,体现在以下四点。
async function getStockPriceByName(name) {
var symbol = await getStockSymbol(name);
var stockPrice = await getStockPrice(symbol);
return stockPrice;
}
getStockPriceByName(‘goog’).then(function (result) {
console.log(result);
});
async function f() {
return ‘hello world’; 返回的是then的参数
}
f().then(v => console.log(v))
// “hello world”
上面代码中,函数f内部return命令返回的值,会被then方法回调函数接收到。
async函数内部抛出错误,会导致返回的 Promise 对象变为reject状态。抛出的错误对象会被catch方法回调函数接收到。
async function f() {
throw new Error(‘出错了’);
}
f().then(
v => console.log(v),
e => console.log(e)
)
// Error: 出错了