ES6新特性
let与const变量以及块级作用域
解决var带来的变量声明提升的问题
块级作用域:一个{…}为一个块
变量重复声明:var可以,let不可以;var后面的覆盖前面的
let和const由于是块级作用域所以没有变量提升
模板字符串
即插值字符串,通过模板拼接字符串。
解构赋值(模式匹配)
使用解构从数组和对象中提取值并赋值给独特的变量。
-
数组解构
let x=1;
ley y=2;
[x,y]=[y,x];//交换
let nums=[1,2,3];
[n,,m]=nums;//匹配赋值
-
对象解构
let point={x:1,y:2};
let {x1:x,y1:y}=point;//赋予point的引用
箭头函数
即使用Lambda表达式表示函数来代替匿名函数。
函数的默认参数
在声明函数时为参数赋予初值,从而使该参数具备了默认值。
面向对象的支持
通过class关键创建类,通过extends继承,通过super访问父类。
对象字面量简写法
使用和所分配的变量名称相同的名称初始化对象时,如果属性名称和所分配的变量名称一样,则可以从对象属性中删除这些重复的变量名称。
Symbol类型
提供了一种表示独一无二的值的方式,从而解决可以解决类似对象属性名称冲突的问题。
新数据结构--Set与Map
Set提供了一种类似于数组,但每个成员值都是唯一的。
Map提供了一种类似于对象的键值对集合,但键的类型不限于字符串,可以是各种类型的值。
Proxy(代理)
用于修改某些操作的默认行为,等同于在语言层面做出修改,属于一种“元编程(meta programming)”,即对编程语言进行编程。代理允许拦截在目标对象上的底层操作。
Reflect(反射)
给底层操作提供默认行为的方法的集合,这些操作是能够被代理重写的。反射和代理方法是一一对应的。
Promise
异步编程的一种解决方案。提供了一个容器,其中保存着某个未来才会结束的事件的结果。
Iterator(遍历器)与for…of循环
Iterator是一种接口,为各种不同的数据结构提供统一的访问机制。
for…of循环作为便利所有具有Iterator接口的数据结构的统一方法。
模块化
一个模块就是一个独立的文件,文件内部的所有变量,外部无法获取。
通过export规定模块对外的接口,通过import输入其他模块提供的功能。
异步编程
JavaScript是单线程的
- 同一个时间只能做一件事情。
- 作为浏览器脚本语言,JavaScript主要是与用户互动和操作DOM。
- 为了避免复杂性和DOM渲染冲突,JavaScript诞生之初就被设计为单线程
任务队列
单线程意味着,所有任务需要排队
- 同步任务: 在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。
- 异步任务: 不直接进入主线程,而进入*“任务队列”*的任务,只有“任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行
异步执行的运行机制
所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
任务队列(事件队列)
任务队列是一个事件队列(消息队列)(函数执行交给队列)
- IO设备完成一项任务,就在事件队列中添加一个事件,表示相关的异步任务可以进入“执行栈”了。
- 主线程读取事件队列,就是触发其中的事件。
- 事件队列中除了IO设备的事件以外,还包括一些用户产生的事件(如鼠标点击等)。
- 只要指定过回调函数,这些事件发生时就会进入事件队列,等待主线程读取。
回调函数
那些会被主线程挂起的代码
异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数
ES6中存在两种事件队列
宏任务与微任务:参考资料 zhuanlan.zhihu.com/p/24460769
传统的异步编程模式
异步返回值
通过为异步操作提供一个回调,在此回调函数中包含要使用异步返回值的代码,将异步结果作为该函数的参数。
function double(value, callback) {
setTimeout(() => {
let r = value * 2;
if (callback) {
callback(r, value);
}
}, 1000);
}
console.log("start");
double(100);
double(100, function (result, value) {
console.log(`${value}*2=${result}`);
});
console.log("end");
失败的处理
异步操作的失败处理在回调模型中也要考虑,因此自然就出现了成功回调和失败回调。
function double(value, success, failure) {
setTimeout(() => {
try {
if (typeof value !== "number") {
throw new TypeError("The value must be a number");
} else {
let r = value * 2;
if (success) {
success(r, value);
}
}
} catch (err) {
if (failure) {
failure(err);
}
}
}, 1000);
}
double(
"100",
function (result, value) {
console.log(`${value}*2=${result}`);
},
function (err) {
console.log(err.message);
}
);
//如果异步返值又依赖另一个异步返回值,回调的情况会进一步变复杂。
double(
100,
function (result, value) {
console.log(`${value}*2=${result}`);
double(result, (y) => console.log(`Success: ${y}`));
});
使用try-catch结构,抛出错误不会终止程序执行,会被catch捕获,如果成功会执行try中else的部分
原生Ajax中的异步编程
Ajax
Asynchronous JavaScript and XML,异步JavaScript和XML技术。
用于实现与服务器进行异步交互的功能。
Ajax核心
XMLHttpRequest对象
由浏览器提供,开发者可以使用它发出HTTP和HTTPS请求。
不需要刷新(重新提交)页面,就可以获取服务器最新的相应,从而修改页面的内容。
常用属性:
增加:response+responseType="json"搭配接收json格式
常用方法:
-
open(method, url, async, user, psw)
规定请求- method:请求类型 GET 或 POST
- url:文件位置
- async:true(异步)或 false(同步)
- user:可选的用户名称
- psw:可选的密码
-
setRequestHeader()
向要发送的报头添加标签/值对
const ajax = {
xhr: new XMLHttpRequest(),
get: function (url, succ, fail) {
if (!this.xhr) {
if (fail) {
fail(new Error("No XMLHttpRequest Object"));
return false;
}
}
this.xhr.onreadystatechange = function () {
if (this.readyState == 4 && this.status == 200) {
if (succ && typeof succ === "function") {
succ(this.response);
}
} else {
if (fail && typeof fail === "function") {
fail(new Error("HTTP status Rxception"));
}
}
};
this.xhr.open("GET", url);
this.xhr.responseType = "json";
this.xhr.send();
},
};
调用:
JSON
JavaScript Object Notation,JavaScript对象表示法。
一种轻量级的数据交换格式,具有易读易写、易解析、易于使用等优点,被广泛应用于Web应用程序中。
- Web应用程序中的前后端数据交换和存储:JSON可以轻松地将数据转换为字符串,然后在Web应用程序的前后端之间进行传递和存储。
- 数据接口的数据传输:JSON格式的数据可以方便地在数据接口之间传输和解析,使得不同系统之间的数据交互变得更加容易。
- 配置文件:JSON格式的配置文件可以方便地存储和解析应用程序的配置信息。
- 日志文件:JSON格式的日志文件可以方便地存储和分析应用程序的运行日志。
- 数据库存储:一些数据库支持存储和查询JSON格式的数据,使得在数据库中存储复杂的数据结构变得更加容易。
{
"name": "Tom",
"age": 25,
"gender": "male"
}
在JSON中,字符串必须使用双引号引起来,而数字、布尔值、null等数据类型不需要引号。
JSON支持以下几种数据类型
对象(object):由一组键值对组成,用花括号表示,例如上面的示例。
数组(array):由一组有序的值组成,用方括号表示。
字符串(string):由双引号括起来的一组字符,例如:"hello world"。
数字(number):整数或浮点数,例如:100、3.14。
布尔值(boolean):true或false。
空值(null):表示一个空值,例如:null。
使用JSON.stringify()将对象转换为字符串
使用JSON.parse()将字符串转换为对象
Promise
Promise 是异步编程的一种解决方案,比传统的解决方案—回调函数和事件—更合理和更强大。
- Promise是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。
- 语法上,Promise 是一个对象,从它可以获取异步操作的消息。
- Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
Promise对象的特点
对象的状态不受外界影响
- Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
一旦状态改变,就不会再变,任何时候都可以得到这个结果
- Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)
Promise的缺点
- 无法取消Promise,一旦新建它就会立即执行,无法中途取消。
- 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
- 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)
//整体是个宏任务
console.log("a");//同步代码
setTimeout(() => console.log("b"));//宏任务
let p = new Promise((resolve, reject) => {
resolve();
console.log("c");//同步代码
}).then(() => {
console.log("d");//微任务
});
console.log("e");//同步代码
//执行顺序: a c e d b
执行一个宏任务,会清空(全部执行)微任务,然后渲染一次页面,再执行下一个宏任务
理解宏任务和微任务:
基本用法
1.先用Promise构造函数用来生成Promise实例
const promise = new Promise(function (resolve, reject) {
//...do something
if (/*异步操作成功*/) {
resolve(value);
} else {
reject(reason);
}
});
参数
resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved)异步成功
reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected)异步失败
2.再用then方法分别指定resolved状态和rejected状态的回调函数
promise(...).then(function(value){
//value
}, function(error){
//error
});
then方法可以接受两个回调函数作为参数。
第一个回调函数是Promise对象的状态变为resolved时调用,
第二个回调函数是Promise对象的状态变为rejected时调用。
其中,第二个函数是可选的,不一定要提供。
这两个函数都接受Promise对象传出的值作为参数
then()方法
始终返回promise对象,可以隐式由them自己创建,也可以显式创建(需要再此执行promise任务时)
then()方法中onfulfilled函数也可以返回显式创建的Promise对象
- then方法始终会返回一个新的Promise对象。
- 可以代码显式创建,也可以由then方法内部隐式创建。
- 如果需要在then方法中执行一个异步任务,就需要显式创建一个新的Promise对象
静态方法
Promise.resolve()方法
- 返回一个具有给定值的 Promise 对象。
- 如果传入的参数是一个 Promise 实例,则该实例将被直接返回(见下面的实例);
- 否则,将创建一个新的 Promise 对象,并将传入的参数作为 Promise 对象的fulfilled值。
let p = new Promise((resolve) => {
setTimeout(resolve, 1000, "hello");
});
let p2 = Promise.resolve(p);
console.log(p == p2); //true,直接返回实例没有新创建对象
Promise.reject()方法
Promise还具有reject()方法,用于返回一个rejected状态的Promise实例,但一般情况下没有太大用处。
catch问题
var p1=new Promise((resolve, reject)=>{
setTimeout(()=>reject(new Error('fail')), 3000);
});
var p2=new Promise((resolve, reject)=>{
setTimeout(()=>resolve(p1), 1000);
});
p2.then(result=>console.log(result))
.catch(error=>console.log(error));
一般,不要在then方法中定义rejected状态的回调函数,而应该总是使用catch方法,这样就可以捕获整个链式调用中抛出的错误
Promise.all( )方法
Promise.all( )实参是所有Promise实例的字面量组成的数组,执行完毕的结果是所有输出结果的所组成的数组
var p1 = new Promise((res, rej) => {
setTimeout(() => {
res("p1");
}, 1000);
});
var p2 = new Promise((res, rej) => {
setTimeout(() => {
res("p2");
}, 2000);
});
var p3 = new Promise((res, rej) => {
setTimeout(() => {
res("p3");
}, 3000);
});
Promise.all([p1, p2, p3]).then((r) => {
console.log(r);//输出数组,顺序和all中参数顺序相同
}).catch((err) => console.log(err.messsage));
Promise.race(iterable) 方法
iterable为包含了多个promise对象的可迭代数据结构,如数组。
该方法返回一个 promise。
一旦迭代器中的某个 promise 解决或拒绝,返回的 promise 就会解决或拒绝。
链式调用
Promise实例可以链式的then( )调用,并且可以一直调用下去
let promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
let t = true;
if (t) {
resolve("success");
} else {
reject("failed");
}
}, 1000);
});
promise1.then((r) => {
console.log(r);
return r + "1";
}).then((r) => {
console.log(r);
return r + "2";
}).then((r) => {
console.log(r);
});
例子
function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms, 'done’);
});
}
timeout(5000).then((value) => {
console.log(value);
});
console.log('this going on....');
异步加载图片
function loadImageAsync(url){
return new Promise(function(resolve, reject){
var image=new Image();
image.onload=function(){
image.src=url;
resolve(this);
};
image.onerror=function(){
reject(new Error('Counld not load image at ' + url));
};
});
}
loadImageAsync('http://p.ananas.chaoxing.com/star3/origin/56eba451498edbe2900a334f.png').then(function(image){
document.body.appendChild(image);
console.log(`Image loaded:${image.src}`);
}, function(error){
console.log(error);
});
异步处理AJAX结果
var getJSON = function (url) {
var promise = new Promise(function (resolve, reject) {
var client = new XMLHttpRequest();
client.open('GET', url);
client.onreadystatechange = handler;
client.responseType = 'json';
client.setRequestHeader('Accept', 'application/json');
client.send();
function handler() {
if (this.readyState !== 4) return;
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
}
});
return promise;
};
getJSON('post.json').then(function (json) {
let { name, age } = json;
console.log(`Contents: name->${name},age->${age}`);
}, function (error) {
console.error('Error!', error);
});
Fetch API
Fetch API 提供了一个获取资源的接口(包括跨网络通信)。
对于任何使用过 XMLHttpRequest 的人都能轻松上手,而且新的 API 提供了更强大和灵活的功能集。
- 例如,请求和响应。
提供了一个全局 fetch() 方法,该方法提供了一种简单,合理的方式来跨网络异步获取资源。
fetch() 方法
用于发起获取资源的请求
-
它返回一个 promise,这个 promise 会在请求响应后被 resolve,并传回 Response 对象。
-
当遇到网络错误时,fetch() 返回的 promise 会被 reject,并传回 TypeError,虽然这也可能因为权限或其它问题导致。
-
成功的 fetch() 检查不仅要包括 promise 被 resolve,还要包括 response.ok 属性为 true。
- HTTP 404 状态并不被认为是网络错误。
网络正常的情况下,fetch()方法返回一个fulfilled状态的Promise。
其中包含了http整个回应的对象,该回应对象会在该Promise对象调用then()方法时传递给其onfulfilled回调函数,即response参数。
通过response对象的json()方法,可以将该回应主体解析为JS对象。
fetch(url, options)中options的用法(*为默认值)
•Using Fetch
•developer.mozilla.org/zh-CN/docs/…
•fetch()方法
异步函数(async/await)
ES2017引入了async函数,使异步操作变得更加方便
Async
async函数返回的Promise对象会运行执行(resolve)异步函数的返回结果,或者如果异步函数抛出异常的话会运行拒绝(reject)。
async会把返回值传递给Promise.resolve( )
await
异步函数可以包含await指令,该指令会暂停异步函数的执行,并等待Promise执行,然后继续执行异步函数,并返回结果。(异步变同步)
- 如果等待的不是 Promise 对象,则返回该值本身。
- await 关键字只在异步函数(async)内有效
代码分析
输出顺序为:3 1 4 5 2 6
•async 函数
•es6.ruanyifeng.com/#docs/async
•1 分钟读完《10 分钟学会 JavaScript 的 Async/Await》
模块化
ES6引入了模块机制,解决了作用域的问题,并让JS应用变得更加结构化
模块:使用不同方式加载的JS文件(相较于JS文件作为脚本的加载)
•模块与脚本的不同
•模块代码自动运行在严格模式下,并且没有任何办法跳出严格模式。
•在模块的顶级作用域创建的变量,不会被自动添加到共享的全局作用域,它们只会在模块顶级作用域的内部存在。
•模块顶级作用域的this值为undefined。
•模块不允许在代码中使用HTML风格的注释(这是JS来自于早期浏览器的历史遗留特性)。
•对于需要让模块外部代码访问的内容,模块必须导出它们。
•允许模块从其他模块导入绑定。
导出:export关键字
将已发布的代码部分公开给其他模块
最简单的做法
- 将export放在任意变量、函数或类声明之前,从模块中将其公开出去。
export var color = "red";
export let name = "Nicholas";
export const magicNumber = 7;
export function sum(num1, num2) {
return num1 + num2;
}
export class Rectangle {
constructor(length, width) {
this.length = length;
this.width = width;
}
}
function substract(num1, num2) {
return num1 - num2;
}
function multiply(num1, num2) {
return num1 * num2;
}
export { multiply };
在模块中尽量不要导出变量,因为导出后会变为常量,如果想要修改模块内容,可以使用函数/对象中转(其中存的是引用,引用固定但是引用的值不固定)
导入:import关键字
•在其他模块内使用import来访问已被导出的功能。
语法:
import { identifier1, identifier2, … } from “./module1.js”;- identifier表示需要导入的标识符
- ./module1.js表示需导入的标识符的来源模块。可以是相对路径也可以是绝对的,绝对路径只能是url链接
- 有时,为了区别模块和脚本,会使用.mjs作为模块文件的后缀名。
- 导入绑定的列表看起来与对象结构相似,但并无关联。
- 当从模块导入了一个绑定时,该绑定表现得就像使用了 const 的定义。这意味着不能再定义另一个同名变量(包括导入另一个同名绑定),也不能在对应的import语句之前使用此标识符(也就是要受暂时性死区限制),更不能修改它的值。
导入单个绑定
例如:在app.js中导入example.mjs的sum函数。
import { sum } from "./example.mjs";
console.log(sum(2, 3));
sum = 1;
//别名设置
import { sum as qiuhe} from "./example.mjs";
console.log(qiuhe(2, 3));
sum = 1;
要确保在导入的文件名前面使用/、./或../,以便在浏览器与Node.js之间保持良好兼容性。
浏览器中使用模块
- 使用
- 使用
- 使用type=“module”
<script type="module" src="app.js"></script>
<script type="module">
import { sum } from "./example.mjs";
let result = sum(1, 2);
console.log(result);
</script>
js中一旦有一处使用import或者export,那么js就会自动成为一个封装的模块,外部不能直接调用内部没有export的内容
浏览器中使用模块加载顺序
所有模块,无论是用
- 下载并解析 app.js ;
- 递归下载并解析在 app.js 中使用 import 导入的资源;
- 解析内联模块;
- 递归下载并解析在内联模块中使用 import 导入的资源example.mjs;
浏览器中使用模块执行顺序
一旦加载完毕,直到页面文档被完整解析之前,都不会有任何代码被执行。在文档解析完毕后,会发生下列行为:
- 递归执行 app.js 导入的资源;(递归完成之前不会执行)
- 执行 app.js ;
- 递归执行内联模块导入的资源example.mjs;(递归完成之前不会执行)
- 执行内联模块;
浏览器模块说明符方案
浏览器要求模块说明符必须为下列格式之一:
- 以 / 为起始,表示从根目录开始解析
- 以 ./ 为起始,表示从当前目录开始解析
- 以 ../ 为起始,表示从父级目录开始解析
- URL 格式
例如:拥有一个位于 https://localhost/modules/module.js 的模块文件
- 从 https://localhost/modules/example1.js 导入
- import { first } from "./example1.js";
- 从 from https://localhost/example2.js 导入
- import { second } from "../example2.js";
- 从 from https://localhost/example3.js 导入
- import { third } from "/example3.js";
- 从 from others.com/example4.js 导入
- import { fourth } from "others.com/example4.js";
导入多个绑定
例如:在app.js中同时导入example.mjs的sum、multiply函数以及magicNumber常量
import { sum, multiply, magicNumber } from "./example.mjs";
console.log(sum(1, magicNumber));
console.log(multiply(1, 2));
完全导入一个模块
允许将整个模块当作单一对象进行导入,该模块的所有导出都会作为该对象的属性。
例如:在app.js中导入example.mjs的所有绑定
import * as example from "./example.mjs";
console.log(example.sum(1, example.magicNumber));
console.log(example.multiply(1, 2));
console.log(example);
该导入模式被称为“命名空间导入”。
- example对象并不存在于example.mjs文件中;
- 它作为一个命名空间对象被创建;
- 其中包含了example.mjs的所有导出成员;
注意事项
无论对同一个模块使用了多少次import,该模块都只会执行一次。
- 导出模块的代码执行后,已被实例化的模块就保留在内存中,随时都能被其他import引用。
-
import * as example1 from "./example.mjs"; import * as example2 from "./example.mjs"; console.log(example1 == example2); - 若同一个应用中的其他模块要从example.mjs导入绑定,也使用的是同一个模块实例
export和import必须被用在其他语句或表达式的外部,只能在模块的顶级作用域中使用
重命名导出与导入
重命名导出
在模块中使用as用导出名称表示本地名称。
重命名导入
在导入时也可以使用as用本地名称表示到处名称
模块的默认值
使用default关键字将模块中单个变量、函数或类指定为该模块的默认值。
在模块只能设置一个默认的导出
导入
面向对象编程
类
- ECMAScript 6 新引入的 class 关键字具有正式定义类的能力。
- 类( class)是ECMAScript 中新的基础性语法糖结构。
- 虽然 ECMAScript 6 类表面上看起来可以支持正式的面向对象编程,但实际上它背后使用的仍然是原型和构造函数的概念。
class
name和age为属性sayHello为方法
对象
使用new关键字来实例化一个类,生成一个对象
extends
n使用extends关键字来实现继承,子类可以继承父类的属性和方法。
super
用于调用父类的构造函数或者父类的属性和方法。
class student extends Person {
constructor(name,age,grade){
super( name,age);//必须写在子类构造器第一句
this.grade = grade;
}
study() {
super. sayHello();
console. log(`I am studying in grade${this.grade}.`);
}
}
this.sayHello()也可以调用到对应方法
先调用父类的构造函数初始化一个父类对象,再进行子类的构造
访问器属性
getter与setter
可以使用getter和setter来访问和设置对象的属性
约定以下划线的属性在内部使用,不在外部访问,外部访问需要调用getter 的属性时使用不带下划线版本
get name()提供了对属性name的getter,通过它仅能获取name的值大写形式。
set age(value)提供了对属性age的setter,并通过条件判断对接收的只进行验证。如果验证通过,则将值保存到age中。
get birthyear()则是获取对属性_age的计算值。
它们被定义为类的属性,但是被访问和设置时表现为方法。(用属性的形式访问和修改)
在新规定以前没有办法设置私有属性,可用如下方法闭包实现内部变量
私有成员
使用增加哈希前缀 # 的方法来定义私有类字段,这一隐秘封装的类特性由 JavaScript 自身强制执行。仅对当前类内部有效。
定义私有属性要在构造器前先声明
developer.mozilla.org/zh-CN/docs/…
静态成员
使用static关键字来定义类的静态方法和属性,它们属于类本身而不是类的实例
静态方法不可以访问实例成员,但是实例方法可以访问静态成员
调用对应静态成员方法需要通过类名调用,因为静态成员不属于任何一个实例
与模块化一起
类作为逻辑结构完成的代码单元,具有一定的独立性。
结合模块化,可以更好的利用类来规划代码结构,提高代码的可读性和可维护性。
例如
- 将Person类放于person.js文件,并导出。
- 将Student类放于student.js文件,从person.js导入Person类,作为Student类的父类,并导出Student类。
- 在应用层面的app.js中从对应的模块代码文件导入需要的类,对其进行编程。
Symbol
概述
ES6引入新类型Symbol,表示独一无二的值,作为JavaScript语言的第7种类型。
let s = Symbol();
console.log(typeof s);//symbol
Symbol函数前不能使用new命令,因为生成的Symbol是一个原始类型的值,不是对象。
Symbol函数可以接受一个字符串作为参数,表示对Symbol实例的描述。
主要是为了在控制台显示,或转换为字符串时比较容易区分。
let s1 = Symbol('foo');
let s2 = Symbol('bar');
console.log(s1, s1.toString());
console.log(s2, s2.toString());
如果Symbol的参数是一个对象,就会调用其toString方法,将其转换为字符串,然后才生成一个Symbol值。
const obj = {
toString() {
return 'abc’;
}
};
const sym = Symbol(obj);
console.log(sym);
Symbol函数的参数只表示当前Symbol值的描述。
相同参数的Symbol函数的返回值是不相等的。
let s1 = Symbol(); let s2 = Symbol(); console.log('s1==s2', s1 == s2);//falselet s1 = Symbol('foo'); let s2 = Symbol('foo'); console.log('s1==s2', s1 == s2);//falseSymbol值不能与其他类型的值进行运算,否则会报错。
Symbol值可以显式的转换为字符串。
Symbol值可以显式的转换为布尔值,但不能转换为数值。
作用
作为属性名的Symbol
Symbol作为对象属性名保证不会出现同名的属性。
对一个对象由多个模块构成的情况非常有用,能防止一个键被不小心改写或覆盖。
Symbol值作为属性名时不能使用点运算符。
let proSym = Symbol();
let a = {};
a.proSym='Hello';
console.log(a[proSym]);
console.log(a['proSym']);
在对象内部使用Symbol值定义属性时,Symbol值必须放在方括号中
let d = {
[proSym](name) {
console.log('hello' + name);
}
};
d[proSym]('xiaohua');
Symbol类型可以用于定义一组常量,保证这组常量的值都是不相等的。
let log = {};
log.levels = {
DEBUG: Symbol('debug’),
INFO: Symbol('info’),
WARN: Symbol('warn')
};
console.log(log.levels.DEBUG, 'debug message');
console.log(log.levels.INFO, 'info message');
实例
消除魔术字符串
let shapeType = {
triangle: 'Triangle’,
square: 'Square'
};
function getArea(shape, options) {
let area = 0;
switch (shape) {
case shapeType.triangle:
console.log(shapeType.triangle);
area = 0.5 * options.width * options.height;
break;
case shapeType.square:
console.log(shapeType.square);
area = options.height ** 2;
break;
default:
throw new Error('undefined shape’);
}
return area;
}
r = getArea(shapeType.square, { height: 20 });
改进:
shapeType = {
triangle: Symbol(),
square: Symbol()
};
r = getArea(shapeType.square, { height: 20 });
属性名的遍历
Symbol作为属性名时,该属性不会出现在for…in和for…of循环中
也不会被Object.keys()、Object.getOwnPropertyNames()返回
- 可以使用
Object.getOwnPropertySymbols()方法获取指定对象的所有Symbol属性名 Reflect.ownKeys方法可以返回所有类型的键名,包括常规键名和Symbol键名。
Symbol.for()与Symbol.keyFor()
- nSymbol.for方法接受一个字符串,然后搜索有没有以该参数作为名称的Symbol值,若有,则返回这个Symbol值,否则新建并返回一个以该字符串为名称的Symbol值。
- nSymbol.keyFor方法返回一个已登记的Symbol类型值的key。
内置的Symbol值
Symbol.hasInstance属性
n指向一个内部方法,对象使用instanceof运算符时会调用这个方法,判断该对象是否为某个构造函数的实例。
- 如,
foo instanceof Foo则是在内部调用了FooSymbol.hasInstance。
Symbol. isConcatSpreadable属性
表示该对象使用Array.prototype.concat()时是否可以展开。
该对象只能是数组或类似数组的对象。
Symbol. species属性
指向当前对象的构造函数。
创建实例时默认会调用这个方法,即使用这个属性返回的函数当作构造函数来创建新的实例对象。
Symbol.match属性
指向一个函数,当执行str.match(myObject)时,如果该属性存在,会调用它返回该方法的返回值。
Symbol.replace属性
指向一个方法,当对象被String.prototype.replace方法调用时返回该方法的返回值。
Symbol.search属性
指向一个方法,当对象被String.prototype.seach方法调用时返回该方法的返回值。
Symbol.split属性
指向一个方法,当对象被String.prototype.split方法调用时返回该方法的返回值。
Symbol.iterator属性
指向该对象的默认遍历器方法。
Symbol.toPrimitive属性
指向一个方法,对象被转换为原始类型的值时会调用这个方法,返回该方法对应的原始类型值。
Symbol.toStringTag属性
指向一个方法,在对象调用Object.prototype.toString方法时,如果这个属性存在,则返回值会出现在toString方法返回的字符串中,表示对象的类型。
如[object Object]、[object Array]
Symbol.unscopables属性
指向一个对象,指定了使用with关键字时哪些属性会被with环境排除。
Set与Map
数组在JavaScript中的使用正如其他语言的数组一样,但缺少更多类型的集合导致数组也经常被当作队列与栈来使用。
数组只使用了数值型的索引,而如果非数值型的索引是必要的,开发者便会使用非数组的对象。
这种技巧引出了非数组对象的定制实现,即 Set 与 Map。
Set
基本用法:类似于数组,但其成员的值都是唯一的
- Set本身是一个构造函数,用来生成Set实例。
const s = new Set();
[2, 3, 4, 5, 2, 2].forEach(x => s.add(x));
for (let i of s) {
console.log(i);
}
add( )方法可以添加元素到Set实例中,但不会添加重复的值
- Set函数可以接受一个数组(或具有Iterable接口的其他数据结构)作为参数,来初始化Set实例。
let set = new Set([1, 2, 3, 4, 4]);
console.log([...set]);
属性
constructor,构造函数,默认就是Set函数。
size,返回Set实例的成员数量。
方法
操作方法,用于操作数据
add(value),添加值,返回该Set实例的引用。
delete(value),删除值,返回一个布尔值,表示删除是否成功。
has(value),返回一个布尔值,表示该值是否是Set实例的成员。
clear( ),清除所有成员,没有返回值。
let set = new Set(); set.add(1).add(2).add(2); console.log(set); console.log(set.has(2)); console.log(set.has(3)); set.delete(1); console.log(set); set.clear(); console.log(set);
遍历方法,用于遍历成员
keys( ),返回键名的遍历
values( ),返回键值的遍历器
由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致。
let set = new Set(["red", "green", "blue"]);
for (let item of set.keys()) {
console.log(item);
}
for (let item of set.values()) {
console.log(item);
}
//Set结构实例的默认遍历器生成函数就是values()方法
for (let item of set) {
console.log(item);
}
entries( ),返回键值对的遍历器。
for (let item of set.entries()) {
console.log(item);
}
forEach( ),使用回调函数遍历每个成员,没有返回值。
set.forEach(value => console.log(value.toUpperCase()));
set.forEach((value, key, s) => {
console.log(s.size);
console.log(value);
});
应用
去掉数组中的重复值
let items = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
console.log(...items);
去掉字符串中重复字符
let str = "abbcddef";
console.log([...new Set(str)].join(""));
扩展运算符、map、filter和 Set 结构相结合,实现并集、交集、差集。
let a = new Set([1, 2, 3]);
let b = new Set([4, 3, 2]);
//并集
let union = new Set([...a,...b]);
console.log(union);
//交集
let interect = new Set([...a].filter(x => b.has(x)));
console.log(interect);
//差集
let diffs = new Set([...a].filter(x => !b.has(x)));
console.log(diffs);
WeakSet
与Set的区别
•WeakSet的成员只能是对象。
•WeakSet中的对象都是弱引用。
•垃圾回收机制不考虑WeakSet对该对象的引用。
•如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占的内存。
应用场景
•WeakSet 适合临时存放一组对象,以及存放跟对象绑定的信息。
•只要这些对象在外部消失,它在 WeakSet 里面的引用就会自动消失。
ES6 规定 WeakSet 不可遍历。
Map
Map类型是键值对的有序列表,而键和值都可以是任意类型
相较于Object
Object只能用字符串当作键,其他类型作为键时会自动转换为字符串。
字符串-值
- Map可以直接使用各种类型的值(包括对象)作为键。
值-值
- Object重在表达对象,Map重在表达数据结构--字典。
基本用法
初始化
let map=new Map([
['name','zhang'],
['title','Author']
]);
console.log(map.size);
console.log(map.has('name'));
console.log(map.get('name'));
console.log(map.has('title'));
console.log(map.get('title'));
set(key, value),设置(添加)键值对。
get(key),通过key获取对应值。
const map0 = new Map()
.set(1, "a")
.set(2, "b")
.set(3, "c");
console.log(map0);
const map1 = new Map([...map0].filter(([k, v]) => k < 3));
console.log(map1);
const map2 = new Map([...map0].map(([k, v]) => [k * 2, "_" + v]));
console.log(map2);
类似Set的方法和属性:
- has(key)
- delete(key)
- clear( )
- size
WeakMap与Map的区别
WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。
WeakMap的键名所指向的对象,不计入垃圾回收机制。
迭代器与生成器
迭代器基本概念
迭代器不保存数据,只按照一定规则产生数据
Iterator 的作用
-
为各种数据结构,提供一个统一的、简便的访问接口。
-
使得数据结构的成员能够按某种次序排列。
-
ES6提供了新的遍历命令for...of循环来消费Iterator 接口。
-
任何数据结构只要部署 Iterator 接口,就可以完成遍历操作。
- 即依次处理该数据结构的所有成员。
Iterator 的遍历过程
创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
- 第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
- 第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
- 不断调用指针对象的next方法,直到它指向数据结构的结束位置。
Iterator接口
Symbol.iterator属性
ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性。
- 或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。
原生具备 Iterator 接口的数据结构
- Array
- Map
- Set
- String
- TypedArray(类型化的数组)
- 函数的 arguments 对象
- NodeList 对象
可迭代的条件
-
拥有Symbol.iterator属性
-
Symbol.iterator中返回的对象中有next()方法
-
next()方法中返回值结构必须是下面的结构
{ value:xxx, done:faslse|true }
以数组为例展示
实现iterator接口的自定义类
class RangeIterator {
constructor(start, stop, step) {
this.value = start;
this.stop = stop;
this.step = step;
}
[Symbol.iterator]() {
return this;
}
next() {
let value = this.value;
if (value < this.stop) {
this.value += this.step;
return { done: false, value: value };
}
return { done: true, value: undefined };
}
}
使用
function range(start, stop, step = 1) {
return new RangeIterator(start, stop, step);
}
for (let value of range(0, 9, 2)) {
console.log(value);
}
实现对象添加 Iterator 接口
let obj = {
data: ["a", "b", "c"],
[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 };
}
},
};
},
};
使用
for (let d of obj) { console.log(d); }
生成器函数
通过Generator函数可以实现Iterator 接口迭代
async和awit的底层实现
yield会暂停代码执行,直到next()调用
let obj = {
name1: "Tom",
name2: "Jerry",
name3: "Mickey",
name4: "Miney",
*[Symbol.iterator]() {
yield this.name1;
yield this.name2;
yield this.name3;
},
};
console.log(obj[Symbol.iterator]().next());
console.log([...obj]);
for (let k of obj) {
console.log(k);
}
由于 Generator 函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志。
遍历器对象的next方法的运行逻辑
遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。
如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
如果该函数没有return语句,则返回的对象的value属性值为undefined。
return方法
如果for...of循环提前退出(通常是因为出错,或者有break语句),就会调用return方法。如果一个对象在完成遍历前,需要清理或释放资源,就可以部署return方法。
function* helloWorldGenerator() {
yield "hello";
yield "world";
return "ending";
}
let hw = helloWorldGenerator();
console.log(hw.next());
console.log(hw.next());
console.log(hw.next());
console.log(hw.next());
对象进阶与扩展
基本概念
JavaScript对象的特征
-
JavaScript的基本数据类型,一种复合值,可看做是属性的无序集合。
- 每个属性都是一个名/值对。
- 属性名是字符串,因此可以把对象看成是从字符串到值得映射。
-
对象除了可以保持自有的属性,还可以从一个称为原型的对象继承属性。
- 原型式继承(prototypal inheritance)是JavaScript的核心特征。
-
对象是动态的,可以增加或删除属性。
-
除了字符串、数值、true、false、null和undefined,其他值都是对象
-
对象最常见的用法是对其属性进行创建、设置、查找、删除、检测和枚举等操作。
-
属性值是任意JavaScript值,或者是一个getter或setter函数。
-
每个属性还有一些与之相关的值,称为“属性特征(property abttribute)”。
- 可写(writable attribute),表明是否可以设置属性的值。
- 可枚举(enumerable attribute),表明是否可以通过for/in结构返回该属性。
- 可配置(configurable attribute),表明是否可以删除或修改该属性。
-
-
每个对象还拥有三个相关的对象特性。
- 对象的原型(prototype),指向另一个对象,该对象的属性会被当前对象继承。
- 对象的类(class),一个标识对象类型的字符串。
- 对象的扩展标记(extensible flag),指明了是否可以向该对象添加新属性。
-
对象的分类
- 内置对象,native object
- 宿主对象,host object
- 自定义对象,user-defined object
-
属性的分类
- 自有属性,own property
- 继承属性,inherited property
创建对象
Object.create( )方法
// obj1继承了属性x和y。
let obj1 = Object.create({ x: 1, y: 2 });
//obj2不继承任何属性和方法。
let obj2 = Object.create(null);
//obj3是一个普通的空对象。
let obj3 = Object.create(Object.prototype);
属性相关
访问属性
let author = {
"first name": "Tonny",
"last-name": "Michael",
age: 40,
};
console.log(author["first name"], author["last-name"]);
let { "first name": fname, "last-name": lname } = author;
console.log(fname, lname, author.age);
作为关联数组的对象
let result = {
data1: {
name: "Language",
value: "Chinese",
},
data2: {
name: "Country",
value: "China",
},
data3: {
name: "Gender",
value: "Male",
},
};
for (let i = 1; i < 4; i++) {
let data = result["data" + i];
console.log(`${data.name}-->${data.value}`);
}
继承
理解原型继承链
let a = {};
a.x = 1;
let b = inherit(a);
b.y = 2;
let c = inherit(b);
c.z = 3;
console.log(c.toString());
console.log(c.x + c.y + c.z);
属性访问错误
查询一个不存在的属性并不会报错,如果在对象o自身的属性或继承的属性中均未找到属性x,属性访问表达式o.x返回undefined。
但是,如果对象不存在,那么试图查询这个不存在的对象的属性就会报错。
- null和undefined值都没有属性,因此查询这些值的属性会报错。
- 如果对象中有undefined值转化成字符串时会被忽略
let one = {};
// let one = { two: { three: 3 } };
console.log(one.two.three);
if (one) {
if (one.two) {
if (one.two.three) console.log(one.two.three);
}
}
console.log(one && one.two && one.two.three);
console.log(one?.two?.three); // Null传导运算符
上述代码中,&&符号会从前往后判断是否为true,如果都为true才输出最右边的结果,否则任何地方为null或undefined都直接返回undefined,规避了报错,ES6中可用null传导运算符判断?前的对象存不存在,也同样规避了报错
在这些场景下给对象o设置属性p会失败:
- o中的属性p是只读的:不能给只读属性重新赋值(defineProperty()方法中有一个例外,可以对可配置的只读属性重新赋值)。
- o中的属性p是继承属性,且它是只读的:不能通过同名自有属性覆盖只读的继承属性。
- o中不存在自有属性p:o没有使用setter方法继承属性p,并且o的可扩展性(extensible attribute)是false。如果o中不存在p,而且没有setter方法可供调用,则p一定会添加至o中。但如果o不是可扩展的,那么在o中不能定义新属性。
删除属性
检测属性
判断某个属性是否存在于某个对象中,可以通过in运算符、hasOwnPreperty()和propertyIsEnumerable()方法,甚至也可以仅通过属性查询。
let o = { x: 1 };
console.log("x" in o);//判断包含继承属性
console.log("y" in o);
console.log("toString" in o);
console.log(o.hasOwnProperty("x"));//判断自己有的属性
console.log(o.hasOwnProperty("y"));
console.log(o.hasOwnProperty("toString"));
console.log(o.propertyIsEnumerable("x"));//判断属性是否可枚举
console.log(o.propertyIsEnumerable("y"));
console.log(o.propertyIsEnumerable("toString"));
console.log(Object.prototype.propertyIsEnumerable("toString"));
枚举属性
for/in循环可以在循环体中遍历对象中所有可枚举的属性(包括自有属性和继承的属性),把属性名称赋值给循环变量。
对象继承的内置方法不可枚举的,但在代码中给对象添加的属性都是可枚举的。
let o =Object.create({m:10,n:20});
o.x=1; o.y=2; o.z=3;
for (let p in o) {
console.log(p, o[p]);
}
利用for/in循环,可以对两个对象进行各种形式的合并。
nObject.keys(),它返回一个数组,这个数组由对象中可枚举的自有属性的名称组成。
let o = Object.create({ m: 10, n: 20 });
o.x = 1; o.y = 2; o.z = 3;
console.log(Object.keys(o));
Object.getOwnPropertyNames(),它和Ojbect.keys()类似,只是它返回对象的所有自有属性的名称,而不仅仅是可枚举的属性。
let o = Object.create({ m: 10, n: 20 });
o.x = 1; o.y = 2; o.z = 3;
console.log(Object.getOwnPropertyNames(o));
getter和setter
属性的特性
Object.getOwnPropertyDescriptor()可以获得某个对象特定属性的属性描述符。
通过一个名为“属性描述符”(property descriptor)的对象实现属性特性的查询和设置操作。
这个对象代表那4个特性。
描述符对象的属性和它们所描述的属性特性是同名的。因此,数据属性的描述符对象的属性有value、writable、enumerable和configurable。
Object.definePeoperty()设置属性的特性
传入要修改的对象、要创建或修改的属性的名称以及属性描述符对象。
let o = {};
Object.defineProperty(o, "x", {
value: 10,
writable: true,
enumerable: false,
configurable: false,
});
console.log(o.x, Object.keys(o));
Object.defineProperty(o, "x", { enumerable: true });//传入Object.defineProperty()的属性描述符对象不必包含所有4个特性。
如果要同时修改或创建多个属性,使用Object.defineProperties()
- 第一个参数是要修改的对象。
- 第二个参数是一个映射表,它包含要新建或修改的属性的名称,以及它们的属性描述符。
let p = Object.defineProperties(
{},
{
x: { value: 1, writable: true, enumerable: true, configurable: true },
y: { value: 1, writable: true, enumerable: true, configurable: true },
r: {
get: function () {
return Math.hypot(this.x, this.y);
},
enumerable: true,
configurable: true,
},
}
);
对象特性
原型属性
原型属性是在实例对象创建之初就设置好的。
通过对象直接量创建的对象使用Object.prototype作为它们的原型。
通过new创建的对象使用构造函数( constructor属性)的prototype属性作为它们的原型。
通过Object.create()创建的对象使用第一个参数(也可以是null)作为它们的原型
将对象作为参数传入Object.getPrototypeOf()可以查询它的原型
要想检测一个对象是否是另一个对象的原型(或处于原型链中),使用isPrototypeOf()方法。
和instanceof运算符非常类似
let o = { x: 1 };
let p = Object.create(o);
p.y = 2;
console.log(Object.getPrototypeOf(p));
console.log(o.isPrototypeOf(p));
可扩展性
对象的可扩展性用以表示是否可以给对象添加新属性。
所有内置对象和自定义对象都是显式可扩展的。
宿主对象的可扩展性是由JavaScript引擎定义的。
序列化对象
Object 构造函数的方法
参考资料:
developer.mozilla.org/zh-CN/docs/…
函数扩展与新增
基本概念
一段JavaScript代码,它只定义一次,但可能被执行或调用任意次
JavaScript函数是参数化的。
函数的定义会包括一个称为形参(parameter)的标识符列表,这些参数在函数体中像局部变量一样工作。
函数调用会为形参提供实参的值。
函数使用实参的值来计算返回值,成为该函数调用表达式的值。
除实参外,每次调用还会拥有另一个值(本次调用的上下文),这就是this关键字的值。
如果函数挂载在一个对象上,作为对象的属性,称它为对象的方法
- 当通过这个对象来调用函数时,该对象就是此次调用的上下文(context),也就是该函数的this的值。
用于初始化一个新创建的对象的函数称为构造函数(constructor)
函数即对象,程序可以随意操控它们(JS中万物皆对象)
- 如,JavaScript可以把函数赋值给变量,或者作为参数传递给其他函数。
- 因为函数就是对象,所以可以给它们设置属性,甚至调用它们的方法
函数可以嵌套在其他函数中定义,从而可以访问它们被定义时所处的作用域中的任何变量。如此,函数构成了一个闭包* *(closure)**,它给JavaScript带来了非常强劲的编程能力。
函数定义
函数定义方式
函数声明语法(会提升)
function add(num1, num2) {
return num1 + num2;
}
函数表达式(不会提升)
let sub = function (num1, num2) {
return num1 - num2;
};
箭头函数(ES6)
let mul = (mun1, num2) => num1 * num2;
let mul = (mun1, num2) => {
return num1 * num2;
};
let f1 = () => {
console.log("arrow function");
};
let f2 = x => x ** 2;
let xx = ((x) => x ** x)(4);//立即执行函数
Function构造函数
这个构造函数接收任意多个字符串参数,最后一个参数始终会被当成函数体,而之前的参数都是新函数的参数
let sum = new Function(
"num1",
"num2",
"let result=num1+num2; return result;"
);
不推荐使用这种语法来定义函数,因为这段代码会被解释两次:第一次是将它当作常规ECMAScript 代码,第二次是解释传给构造函数的字符串。这显然会影响性能。
函数命名
函数参数
特征
ECMAScript 函数既不关心传入的参数个数,也不关心这些参数的数据类型。
- 定义函数时要接收两个参数,并不意味着调用时就传两个参数。
原因
因为 ECMAScript 函数的参数在内部表现为一个数组。
- 函数被调用时总会接收一个数组,但函数并不关心这个数组中包含什么。
- 传进函数的每个参数值都被包含在arguments 对象(类数组)中。
arguments对象
一个类数组对象(但不是 Array 的实例),因此可以使用中括号语法访问其中的元素。
要确定接收到的参数个数,可以访问 arguments.length 属性
function likes(name, fav) {
let favs = [...arguments].slice(1);
let info = `${name}的爱好是${favs}。`;
console.log(info);
}
likes("xiaoming", "看书", "游泳", "旅游");
likes("小明", "读书", "篮球", "游泳", "骑车");
默认参数
扩展参数
function sum() {
let r = 0;
for (let i = 0; i < arguments.length; i++) {
r += arguments[i];
}
return r;
}
let nums = [1, 2, 3, 4, 5];
console.log(sum.apply(null, nums));//apply第二个参数必须是数组
console.log(sum(...nums));
apply()可以指定执行上下文对象,就是this指向
apply()方法的语法如下:javascriptCopy CodefunctionName.apply(thisArg, [argsArray])其中,
functionName是要调用的函数名称,thisArg是函数中this引用的对象,argsArray是一个包含参数的数组,其中每个元素都是单个参数值。
剩余参数
function sum1(name) {
let r = 0;
for (let i = 1; i < arguments.length; i++) {
r += arguments[i];
}
console.log(`${name}总分为:${r}。`);
}
sum1("Tom", 80, 90, 100);
function sum2(name, ...scores) {
//此处...做收集运算符,用来收集剩余参数
let r = scores.reduce((x, y) => x + y, 0);
console.log(`${name}总分为:${r}。`);
}
sum2("Tom", 80, 90, 100);
常见问题
function sum1() {
return Array.from(arguments).reduce((x, y) => x + y, 0);
}
let sum2 = () => {
return Array.from(arguments).reduce((x, y) => x + y, 0);
};
console.log(sum1(1, 2, 3), sum2(1, 2, 3));
//箭头函数不支持arguments对象。
let sum3 = (...nums) => {
return nums.reduce((x, y) => x + y, 0);
};
console.log(sum3(1, 2, 3));
//箭头函数支持剩余参数。
函数调用
构成函数主体的JavaScript代码在定义之时并不会执行,只有调用该函数时,它们才会执行。
有4种方式来调用JavaScript函数:
- 作为函数
- 作为方法
- 作为构造函数
- 通过它们的call()和apply()方法间接调用
方法调用
一般情况下,与普通函数的使用方式一致。
方法是属于某个特定对象才能调用的函数。
方法调用和函数调用有一个重要的区别,即:调用上下文。
属性访问表达式由两部分组成:一个对象(o)和属性名 (m)。在像这样的方法调用表达式里,对象o成为调用上下文,函数体可以使用关键字this引用该对象。
let calculator = {
oper1: 10,
oper2: 20,
add: function () {
this.result = this.oper1 + this.oper2;
},
};
calculator.add();
console.log(calculator.result);
调用上下文
关键字this没有作用域的限制,嵌套的函数不会从调用它的函数中继承this
let obj = {
x: 10,
fn: function () {
this.x++;
function ff() {
console.log(this.x);
}
ff();
},
};
obj.fn();//undefined
//利用函数闭包传递this指向
let obj = {
x: 10,
fn: function () {
this.x++;
let self = this;
function ff() {
console.log(self.x);
}
ff();
},
};
obj.fn();//11
//ES6方法
let obj = {
x: 10,
fn: function () {
this.x++;
let self = this;
function ff() {
console.log(self.x);
}
ff();
},
};
obj.fn();
//箭头函数没有自己的this,会向外寻找this
构造函数调用
间接调用
使用函数对象的call( )和apply( )方法可以间接调用函数
- 第一个参数指定调用上下文(函数内部的this),第二个参数给函数传递参数
- call第二个参数是原来需要传的参数,apply第二个参数只能是数组
let obj1 = {
x: 100,
y: 200,
show: function (n = 1, m = 1) {
return `(${this.x * n},${this.y * m})`;
},
concat: function () {
let r = [this.x, this.y];
for (let a of arguments) r.push(a);
return r;
},
};
let obj2 = { x: 111, y: 222 };
let r1 = obj1.show.call(obj2);
let r2 = obj1.show.call(obj2, 10, 100);
console.log(r1, r2);
let r3 = obj1.concat.call(obj2, 11, 22, 33);
let r4 = obj1.concat.apply(obj2, [2, 3, 4, 5]);
console.log(r3, r4);
回调函数
被作为实参传入另一函数,并在该外部函数内被调用,用以来完成某些任务的函数,称为回调函数。
函数对象
函数属性
length属性
在函数体里,arguments.length表示传入函数的实参的个数。
而函数本身的length属性是只读的,它代表函数声明的实际参数的数量。
prototype属性
每一个函数都包含一个prototype属性,这个属性是指向一个对象的引用,这个对象称做“原型对象”(prototype object)。
每一个函数都包含不同的原型对象。当将函数用做构造函数的时候,新创建的对象会从原型对象上继承属性。
自定义属性
函数是一种特殊的对象,可以拥有属性。
function fx(a, b) {
if (fx.count) fx.count++;
else fx.count = 1; return a + b;
}
fx(1, 2);fx(2, 3);fx(3, 4);fx(4, 5);
console.log(fx.count);
函数方法
call( )和apply( )方法
通过调用方法的形式来间接调用函数。
call()和apply()的第一个实参是要调用函数的主体对象,它是调用上下文,在函数体内通过this来获得对它的引用。
bind( )方法
将函数绑定至某个对象,且可以绑定参数值。
当在函数f()上调用bind()方法并传入一个对象o作为参数,这个方法将返回一个新的函数。
(以函数调用的方式)调用新的函数将会把原始的函数f()当做o的方法来调用。
传入新函数的任何实参都将传入原始函数。
通过bind()为函数绑定调用上下文后,返回的函数不能通过call和apply修改调用上下文对象。
let obj = {
x: 10,
show: function (y) {
let r = "";
for (let i = 0; i < y; i++) {
r += this.x + " ";
}
console.log(r);
},
};
obj.show(4);
let ss = obj.show.bind({ x: 1000 });
ss(3);
ss.call({ x: 2000 }, 3);
数组进阶与扩展
基本概念
数组是值的有序集合
- 每个值叫做一个元素,而每个元素在数组中有一个位置,以数字表示,称为索引。
数组是无类型的
- 数组元素可以是任意类型,并且同一个数组中的不同元素也可能有不同的类型。
- 数组的元素可以是对象或其他数组,从而创建复杂的数据结构。
数组是动态的
- 根据需要它们会增长或缩减,并且在创建数组时无须声明一个固定的大小或者在数组大小变化时无须重新分配空间。
数组可以是稀疏的
- 数组元素的索引不一定要连续的,它们之间可以有空缺。
- 每个JavaScript数组都有一个length属性。
- 针对非稀疏数组,该属性就是数组元素的个数。
- 针对稀疏数组,length比实际元素个数要大。
操作数组
类似于C/C++的固定长度的数组
增加:
- push():在尾部插入
- unshift():在头部插入
删除:
- pop():在尾部删除
- unshift():在头部删除
数组遍历
数组方法
实例方法
join( )
- 将数组中所有元素都转化为字符串并连接在一起,返回最后生成的字符串。
- 可以指定一个可选的字符串在生成的字符串中来分隔数组的各个元素。如果不指定分隔符,默认使用逗号。
let arr = [1, 2, 3, 4, 5];
console.log(arr.join("-"), arr.join());
reverse( )
- 将数组中的元素颠倒顺序,返回逆序的数组。
let r_arr = arr.reverse();
console.log(r_arr, arr);
sort( )
- 将数组中的元素排序并返回排序后的数组。
- 当不带参数调用sort()时,数组元素以字母表顺序排序(如有必要将临时转化为字符串进行比较)。
let data = [22, 4, 111, 30];
let sdata = data.sort();
console.log(data, sdata);
- 给sort()方法传递一个比较函数。该函数决定了它的两个参数在排好序的数组中的先后顺序。
sdata = data.sort(function (a, b) {
return a - b;
});
console.log(sdata);
concat( )
- 创建并返回一个新数组,它的元素包括调用concat()的原始数组的元素和concat()的每个参数
let a1 = arr.concat(10, 20);
let a2 = arr.concat([10, 20]);
let a3 = arr.concat([10, 20], [30, 40]);
let a4 = arr.concat([10, [20, 30]]);
console.log(a1);
console.log(a2);
console.log(a3);
console.log(a4);
console.log([...arr, 10, 20]);
slice( )
- 返回指定数组的一个片段或子数组。
- 它的两个参数分别指定了片段的开始和结束的位置。
let sub_arr = arr.slice(3, 8);
console.log(sub_arr);
sub_arr = arr.slice(1, -1);
console.log(sub_arr);
splice( )
- 是在数组中插入或删除元素的通用方法。
- 不同于slice()和concat(),splice()会修改调用的数组。
ES6新增了7个方法用于对ES5的补充
copyWith( )
会在当前数组内部将指定位置的元素复制到其他位置(会覆盖原有元素),返回当前数组。
let arr = [1, 2, 3, 4, 5];
console.log(arr.copyWithin(0, 2, 4), arr);
find( )和findIndex( )
找出第一个符合条件的数组元素(或其索引)
let a = arr.find(function (value, index, arr) {
return value > 2 && index > 3;
});
console.log(a)
fill( )
使用给定值填充一个数组。
arr.fill(100, 1, 3);
console.log(arr);
entries( )、keys( )和values( )用于遍历数组
includes( )判断数组中是否包含给定的值,与字符串的includes方法类似
Array.from( )
用于将两类对象转为真正的数组
- 类似数组的对象
- 可遍历的对象
<body>
<ul id="list-nums">
<li>100</li>
<li>200</li>
<li>300</li>
</ul>
<script>
let lis = document.querySelectorAll("#list-nums li");
console.log(lis);
let items = Array.from(lis, function (item) {
return item.textContent;
});
console.log(items);
</script>
</body>
Array.of( )将一组值转换为数组
let nums = Array.of(...arr, 7, 8, 9);
console.log(nums);
正则表达式基础
创建正则表达式对象
- JavaScript中使用RegExp对象来封装一个正则表达式,并提供相关的方法和属性。
两种创建方法
字面量
let reg = /\bis\b/g; //g表示全文匹配
let str = "He is a boy. This is a dog. Where is she?";
console.log(str.replace(reg, "IS"));
构造函数
let reg = new RegExp("\bis\b", "g");
let str = "He is a boy. This is a dog. Where is she?";
console.log(str.replace(reg, "IS"));
修饰符
g:global,全文搜索,若不添加,则搜索到第一个匹配即停止。
i:ignore case,忽略大小写,正则表达式是大小写敏感的。
m:multiple lines,多行搜索,搜索时识别换行符。
let str = "He is a boy. Is he?";
console.log(str.replace(/\bis\b/gi, "0"));
元字符
正则表达式由两种基本字符类型组成:
- 原义文本字符,如:a,abc 等。
- 元字符,在正则表达式中有特殊意义的非字母字符。如:\b 用于匹配单词边界(如空格,句号)。
- 一些特殊的符号。如:* + ? $ ^ . | \ ( ) { } [ ]
字符类
一般情况下,正则表达式一个字符对应字符串的一个字符。
- 例如:表达式 ab\t 的含义是 "ab"紧接着一个 tab(制表符)。
当需要匹配一类字符时,可以使用[ ]来构造一个简单的类。
- 所谓类,是指符合某些特性的对象,一个泛指,而不是特指某个字符。
- 例如:表达式[abc]把字符 a、b、c 归位一类,表达式可以匹配这类字符,即匹配其中之一。
let str = "a1b2c3d4";
console.log(str.replace(/[abc]/g, "x"));
字符类取反
使用元字符*^创建反向类(负向类)*,即匹配不属于该类的字符。
例如:[^abc]表示不是字符 a、b、c 其中之一的字符。
let str = "a1b2c3d4";
console.log(str.replace(/[^abc]/g, "x"));
范围类
需要匹配数字时,可以使用范围类。
- 例如:[a-z]表示从 a 到 z 之间的任意字符,且包含 a 和 z 本身。
let str = "a1b2c3d4z0";
console.log(str.replace(/[a-z]/g, "X"));
n在[ ]中可以将一些范围连续书写。
let str = "a1b2c3d4A5B6C7D8";
console.log(str.replace(/[a-zA-Z0-9]/g, "*"));
str = "2020-03-04";
console.log(str.replace(/[0-9-]/g, "0"));
预定义类
边界
量词
匹配模式
分组
例如: Byron{3}表示对 n 匹配 3 次,而不是 Byron。如果要对 Byron 匹配三次,就需要对其进行分组。
console.log("a1b2c3d4".replace(/[a-z]\d{3}/g, "X"));
console.log("a1b2c3d4".replace(/([a-z]\d){3}/g, "X"));
或
使用 | 表示或,表示|左右字符二选一。
- 例如: 比较 Byron|Casper 和 Byr(on|Ca)sper
console.log("ByronCasper".replace(/Byron|Casper/g, "X"));
console.log("ByrCasperByronsper"
.replace(/Byr(on|Ca)sper/g, "X"));
反向引用
使用$n的形式引用模式中分组匹配到的文本,n为索引,从1开始
-
n例如:把 2020-03-04 替换成 03/04/2020。
console.log("2020-03-04" .replace(/(\d{4})-(\d{2})-(\d{2})/g, "$2/$3/$1"));
忽略分组
当不想捕获分组时,可以使用?:
- 例如:(?:Byron).(ok)就会忽略对第一个分组的捕获
console.log("2020-03-04"
.replace(/(\d{4})-(\d{2})-(?:\d{2})/g, "$2/$3/$1"));
前瞻
正则表达式从文本头部向尾部开始解析,文本尾部方向,称为“前”。
前瞻,在正则表达式匹配到规则的时候,向前检查是否符合断言。符合和不符合特定断言称为“肯定/正向”和“否定/负向”匹配。
- 正向前瞻,exp(?=assert)
- 负向前瞻,exp(?!assert)
- exp 和 assert 都是正则表达式,匹配到 exp 时还要判断 assert 是否符合,如果符合才会被匹配。
- 例如:表达式\w(?=\d),表示匹配到一个单词\w 时还需要向后判断是否为一个数字\d。
console.log("a2*34V8".replace(/\w(?=\d)/g, "X"));
console.log("a2*34V8".replace(/\w(?!\d)/g, "X"));
RegExp对象方法
字符串正则方法
- search(reg),用于检索字符串中指定的子字符串,或检索与正则表达式相匹配的子字符串。返回第一个匹配结果的 index,没有匹配到返回-1。不执行全局匹配。
- match(reg),检索字符串以找到一个或多个与 regexp 匹配的文本,未找到返回 null,找到后返回一个数组。与 RegExp 的 exec()方法相同。
- split(reg),利用 regexp 匹配结果作为分隔符对字符串进行分割,返回一个数组。
- replace(reg, newStr),将 regexp 的匹配结果替换成 newStr,返回一个新字符串。
let str = "<java> and <javascript> is deferent!";
console.log(str.match(/<\S*>/g));
console.log(str.replace(/<(\S*)>/g, "<<$1>>"));
console.log(str.split(/[<>]/g));
- replace(reg, function)的用法
let s1 = "a1b23d4e5".replace(/\d/g, function(match, index, origin) {
console.log(index);
return parseInt(match) + 1;
});
let s2 = "a1b23d4e5".replace(/(\d)(\w)(\d)/g,
function(match, group1, group2, group3, index, origin) {
console.log(match);
return group1 + group3;
});