React基础入门 与 ES6

文章目录

ECMAScript6

1、ES6简介

1.1、什么是ES6

ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了。它的目标,是使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。

1.2、ECMAScript和JavaScript的关系

一个常见的问题是,ECMAScript 和 JavaScript 到底是什么关系?

要讲清楚这个问题,需要回顾历史。1996 年 11 月,JavaScript 的创造者 Netscape 公司,决定将 JavaScript 提交给标准化组织 ECMA,希望这种语言能够成为国际标准。次年,ECMA 发布 262 号标准文件(ECMA-262)的第一版,规定了浏览器脚本语言的标准,并将这种语言称为 ECMAScript,这个版本就是 1.0 版。该标准从一开始就是针对 JavaScript 语言制定的,但是之所以不叫 JavaScript,有两个原因。一是商标,Java 是 Sun 公司的商标,根据授权协议,只有 Netscape 公司可以合法地使用 JavaScript 这个名字,且 JavaScript 本身也已经被 Netscape 公司注册为商标。二是想体现这门语言的制定者是 ECMA,不是 Netscape,这样有利于保证这门语言的开放性和中立性。因此,ECMAScript 和 JavaScript 的关系是,前者是后者的规格,后者是前者的一种实现(另外的 ECMAScript 方言还有 JScript 和 ActionScript)。日常场合,这两个词是可以互换的。

1.3、为什么要学习ES6?

这个问题可以转换一种问法,就是学完es6会给我们的开发带来什么样便利?chrome解释javascript的引擎叫做V8,有一个人把V8引擎转移到了服务器,于是服务器端也可以写javascript,这种在服务器端运行的js语言,就是Node.js。Node.js一经问世,它优越的性能就表现了出了,很多基于nodejs的web框架也应运而生,express就是之一,随之而来的就是全栈MEAN mogoDB,Express,Vue.js,Node.js开发,javaScript越来越多的使用到web领域的各个角落,js能做的事情也越来越多。Babel是一个广泛使用的ES6转码器,可以将ES6代码转为ES5代码,从而在现有环境执行。这意味着,你可以用ES6的方式编写程序,又不用担心现有环境是否支持。nodejs是一种开发趋势,Vue.js这种前端框架是一种开发趋势,ES6被普及使用也是趋势。目前一些前端框架都在使用ES6语法,例如Vue、React、D3等等,所以ES6也是学习好前端框架的基础。

2、ES6环境搭建

由于有些低版本的浏览器还不支持ES6的语法,所以在不使用框架的情况下,需要将ES6语法转换为ES5语法。

2.1、前期准备

先创建一个项目,项目中有两个文件夹,src和dist,一个html文件

src:将编写的ES6的js文件放到此文件夹中(这里是index.js文件)

dist:将通过Babel编译成的ES5的js文件放到此文件中(这里是index.js文件)

html:注意:将dist中编译好的文件引入到HTML文件中,而不是src中的js文件

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Document</title>
	<script src="./dist/index.js"></script>
</head>
<body>
	Hello ES6
</body>
</html>
2.2、ES6环境搭建

第一步

在src目录下,新建index.js文件。这个文件很简单,我们只作一个a变量的声明,并用console.log()打印出来。

let a = 1;
console.log(a);

第二步

在项目的根目录初始化项目并生成package.json文件(可以根据自己的需求进行修改)

cnpm init -y
{
    "name": "es6",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
     },
    "keywords": [],
    "author": "",
    "license": "ISC"
}

第三步

安装Babel插件(将ES6语法转换为ES5)

cnpm install -g babel-cli

第四步

当然现在还不能正常转换,还需要安装ES5所需的一个包

cnpm install --save-dev babel-preset-es2015 babel-cli 安装完成后,package.json会有所变化
{
    "name": "es6",
     "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
    },
    "keywords": [],
    "author": "",
    "license": "ISC",
    "devDependencies": {
    "babel-cli": "^6.26.0",
    "babel-preset-es2015": "^6.24.1"
    }
}

第五步:

在项目的根目录添加一个 .babelrc 文件,并添加内容

{
    "presets":[
        "es2015"
    ],
    "plugins": []
}

在windows系统中创建.babelrc文件的方法

方法一:根目录下,创建“.babelrc.”文件名就可以了!(前后共两个点)

方法二:cmd进入根目录,输入“type null>.babelrc”,回车即可!

第六步:

安装完成后我们可以通过命令进行转换

babel src/index.js -o dist/index.js

第七步:

可以将命令进行简化(package.json进行配置)

"scripts": {
    "test": "echo "Error: no test specified" && exit 1"
},

修改为:

{
  	"name": "es6",
  	"version": "1.0.0",
  	"description": "",
  	"main": "index.js",
  	"scripts": {
        "test": "babel src/index.js -o dist/index.js"
  	},
  	"keywords": [],
  	"author": "",
  	"license": "ISC",
  	"devDependencies": {
		"babel-cli": "^6.26.0",
		"babel-preset-es2015": "^6.24.1"
  	}
}

然后我们可以通过下面命令转义代码:

npm run test

3、let与const

ES2015(ES6) 新增加了两个重要的 JavaScript 关键字: letconst

let 声明的变量只在 let 命令所在的代码块内有效,const 声明一个只读的常量,一旦声明,常量的值就不能改变。

3.1、let命令

let命令有以下特点:

(1)代码块内有效

ES2015(ES6) 新增加了两个重要的 JavaScript 关键字: letconst。let 声明的变量只在 let 命令所在的代码块内有效,const 声明一个只读的常量,一旦声明,常量的值就不能改变。

{
    let a = 1;
    var b = 2;
    console.log(a);//输出1
    console.log(b);//输出2
}
console.log(a);//报错 ReferenceError: a is not defined
console.log(b);//输出2

(2)不能重复声明

let 只能声明一次 var 可以声明多次:

let a = 1;
let a = 2;//报错 Identifier 'a' has already been declared
var b = 3;
var b = 4;
console.log(a);
console.log(b);//输出4

for 循环计数器很适合用 let

for (var i = 0; i < 10; i++) {
  setTimeout(function(){
    console.log(i);
  })
}
// 输出十个 10
for (let j = 0; j < 10; j++) {
  setTimeout(function(){
    console.log(j);
  })
}
// 输出 0123456789

变量 i 是用 var 声明的,在全局范围内有效,所以全局中只有一个变量 i, 每次循环时,setTimeout 定时器里面的 i 指的是全局变量 i ,而循环里的十个 setTimeout 是在循环结束后才执行,所以此时的 i 都是 10。

变量 j 是用 let 声明的,当前的 j 只在本轮循环中有效,每次循环的 j 其实都是一个新的变量,所以 setTimeout 定时器里面的 j 其实是不同的变量,即最后输出 12345。(若每次循环的变量 j 都是重新声明的,如何知道前一个循环的值?这是因为 JavaScript 引擎内部会记住前一个循环的值)。

(3)不存在变量提升

let 不存在变量提升,var 会变量提升:

console.log(a);  //ReferenceError: a is not defined
let a = "apple";

console.log(b);  //undefined
var b = "banana";

变量 b 用 var 声明存在变量提升,所以当脚本开始运行的时候,b 已经存在了,但是还没有赋值,所以会输出 undefined。变量 a 用 let 声明不存在变量提升,在声明变量 a 之前,a 不存在,所以会报错。

(4)暂时性死区

只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。

var tmp = 123;

if (true) {
  tmp = 'abc'; // ReferenceError
  let tmp;
}

上面代码中,存在全局变量tmp,但是块级作用域内let又声明了一个局部变量tmp,导致后者绑定这个块级作用域,所以在let声明变量前,对tmp赋值会报错。

ES6 明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。

总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。

if (true) {
  // TDZ开始
  tmp = 'abc'; // ReferenceError
  console.log(tmp); // ReferenceError

  let tmp; // TDZ结束
  console.log(tmp); // undefined

  tmp = 123;
  console.log(tmp); // 123
}

上面代码中,在let命令声明变量tmp之前,都属于变量tmp的“死区”。

“暂时性死区”也意味着typeof不再是一个百分之百安全的操作。

typeof x; // ReferenceError
let x;

另外,下面的代码也会报错,与var的行为不同。

// 不报错
var x = x;

// 报错
let x = x;
// ReferenceError: x is not defined

上面代码报错,也是因为暂时性死区。使用let声明变量时,只要变量在还没有声明完成前使用,就会报错。上面这行就属于这个情况,在变量x的声明语句还没有执行完成前,就去取x的值,导致报错”x 未定义“。

ES6 规定暂时性死区和let、const语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。这样的错误在 ES5 是很常见的,现在有了这种规定,避免此类错误就很容易了。

总之,暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。

3.2、const命令

const 声明一个只读变量,声明之后不允许改变。意味着,一旦声明必须初始化,否则会报错。

基本用法:

const PI = "3.1415926";
PI  // 3.1415926

const MY_AGE;  // 报错 SyntaxError: Missing initializer in const declaration

const声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值。

const foo;
// 报错 SyntaxError: Missing initializer in const declaration

上面代码表示,对于const来说,只声明不赋值,就会报错。const的作用域与let命令相同:只在声明所在的块级作用域内有效。

if (true) {
  const MAX = 5;
}

MAX // Uncaught ReferenceError: MAX is not defined

const命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。

if (true) {
  console.log(MAX); // ReferenceError
  const MAX = 5;
}

上面代码在常量MAX声明之前就调用,结果报错。const声明的常量,也与let一样不可重复声明。

var message = "Hello!";
let age = 25;

// 以下两行都会报错
const message = "Goodbye!";
const age = 30;

暂时性死区:

var PI = "a";
if(true){
  console.log(PI);  //报错 ReferenceError: PI is not defined
  const PI = "3.1415926";
}

ES6 明确规定,代码块内如果存在 let 或者 const,代码块会对这些命令声明的变量从块的开始就形成一个封闭作用域。代码块内,在声明变量 PI 之前使用它会报错。

注意要点

const 如何做到变量在声明初始化之后不允许改变的?其实 const 其实保证的不是变量的值不变,而是保证变量指向的内存地址所保存的数据不允许改动。此时,你可能已经想到,简单类型和复合类型保存值的方式是不同的。是的,对于简单类型(数值 number、字符串 string 、布尔值 boolean),值就保存在变量指向的那个内存地址,因此 const 声明的简单类型变量等同于常量。而复杂类型(对象 object,数组 array,函数 function),变量指向的内存地址其实是保存了一个指向实际数据的指针,所以 const 只能保证指针是固定的,至于指针指向的数据结构变不变就无法控制了,所以使用 const 声明复杂类型对象时要慎重。

4、ES6解构赋值

4.1、解构赋值概述

解构赋值是对赋值运算符的扩展。

它是一种针对数组或者对象进行模式匹配,然后对其中的变量进行赋值。在代码书写上简洁且易读,语义更加清晰明了;也方便了复杂对象中数据字段获取。

4.2、解构模型

ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。

在解构中,有下面两部分参与:

解构的源,解构赋值表达式的右边部分;

解构目标,解构赋值表达式的左边部分;

在ES5中,为变量赋值只能直接指定变量的值:

let a = 1;
let b = 2;

在ES6中,变量赋值允许写成:

let [a,b,c] = [1,2,3];
console.log(a); // 输出1
console.log(b); // 输出2
console.log(c); // 输出3

面代码表示,可以从数组中提取值,按照对应位置,对变量赋值。

本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。

4.3、数组的解构赋值

基本用法

let [a, b, c] = [1, 2, 3];
// a = 1
// b = 2
// c = 3

可嵌套

let [a, b, c] = [1, 2, 3];
// a = 1
// b = 2
// c = 3

可忽略

let [a, , b] = [1, 2, 3];
// a = 1
// b = 3

不完全解构

let [a = 1, b] = []; // a = 1, b = undefined

如果解构不成功,变量的值就等于undefined。

let [foo] = [];
let [bar, foo] = [1];

以上两种情况都属于解构不成功,foo的值都会等于undefined。

另一种情况是不完全解构,即等号左边的模式,只匹配一部分的等号右边的数组。这种情况下,解构依然可以成功。

let [x, y] = [1, 2, 3];
x // 1
y // 2

let [a, [b], d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4

上面两个例子,都属于不完全解构,但是可以成功。

如果等号的右边不是数组(或者严格地说,不是可遍历的结构),那么将会报错。

// 报错
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};

上面的语句都会报错,因为等号右边的值,要么转为对象以后不具备 Iterator 接口(前五个表达式),要么本身就不具备 Iterator 接口(最后一个表达式)。

剩余运算符

let [a, ...b] = [1, 2, 3];
//a = 1
//b = [2, 3]

字符串

在数组的解构中,解构的目标若为可遍历对象,皆可进行解构赋值。可遍历对象即实现 Iterator 接口的数据。

let [a, b, c, d, e] = 'hello';
// a = 'h'
// b = 'e'
// c = 'l'
// d = 'l'
// e = 'o'

解构默认值

解构赋值允许指定默认值。

let [foo = true] = [];
foo // true

let [x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'

当解构模式有匹配结果,且匹配结果是 undefined 时,会触发默认值作为返回结果。

let [a = 2] = [undefined]; // a = 2

注意,ES6 内部使用严格相等运算符(===),判断一个位置是否有值。所以,只有当一个数组成员严格等于undefined,默认值才会生效。

let [x = 1] = [undefined];
x // 1

let [x = 1] = [null];
x // null

上面代码中,如果一个数组成员是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 [a = 3, b = a] = []; // a = 3, b = 3
let [a = 3, b = a] = [1];// a = 1, b = 1
let [a = 3, b = a] = [1, 2]; // a = 1, b = 2
let [a = b, b = 1] = []; // ReferenceError: y is not defined

上述代码解释:

  • a 与 b 匹配结果为 undefined ,触发默认值:a = 3; b = a =3;
  • a 正常解构赋值,匹配结果:a = 1,b 匹配结果 undefined ,触发默认值:b = a =1;
  • a 与 b 正常解构赋值,匹配结果:a = 1,b = 2;
  • 上面最后一个表达式之所以会报错,是因为x用y做默认值时,y还没有声明。
4.4、对象的解构赋值

(1)基本用法

解构不仅可以用于数组,还可以用于对象。

let { foo, bar } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"

对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。

let { bar, foo } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"

let { baz } = { foo: 'aaa', bar: 'bbb' };
baz // undefined

上面代码的第一个例子,等号左边的两个变量的次序,与等号右边两个同名属性的次序不一致,但是对取值完全没有影响。第二个例子的变量没有对应的同名属性,导致取不到值,最后等于undefined。

如果解构失败,变量的值等于undefined。

let {foo} = {bar: 'baz'};
foo // undefined

上面代码中,等号右边的对象没有foo属性,所以变量foo取不到值,所以等于undefined。

对象的解构赋值,可以很方便地将现有对象的方法,赋值到某个变量。

// 例一
let { log, sin, cos } = Math;

// 例二
const { log } = console;
log('hello') // hello

上面代码的例一将Math对象的对数、正弦、余弦三个方法,赋值到对应的变量上,使用起来就会方便很多。例二将console.log赋值到log变量。

如果变量名与属性名不一致,必须写成下面这样。

let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"

let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj;
f // 'hello'
l // 'world'

这实际上说明,对象的解构赋值是下面形式的简写。

let { foo: foo, bar: bar } = { foo: 'aaa', bar: 'bbb' };

也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。

let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
foo // error: foo is not defined

上面代码中,foo是匹配的模式,baz才是变量。真正被赋值的是变量baz,而不是模式foo。

(2)嵌套对象的解构赋值

与数组一样,解构也可以用于嵌套结构的对象。

let obj = {
  p: [
    'Hello',
    { y: 'World' }
  ]
};

let { p: [x, { y }] } = obj;
x // "Hello"
y // "World"

注意,这时p是模式,不是变量,因此不会被赋值。如果p也要作为变量赋值,可以写成下面这样。

let obj = {
  p: [
    'Hello',
    { y: 'World' }
  ]
};

let { p, p: [x, { y }] } = obj;
x // "Hello"
y // "World"
p // ["Hello", {y: "World"}]

下面是另一个例子。

4.5、解构赋值注意事项

(1)如果要将一个已经声明的变量用于解构赋值,必须非常小心。

// 错误的写法
let x;
{x} = {x: 1};
// SyntaxError: syntax error

上面代码的写法会报错,因为 JavaScript 引擎会将{x}理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免 JavaScript 将其解释为代码块,才能解决这个问题。

// 正确的写法
let x;
({x} = {x: 1});

上面代码将整个解构赋值语句,放在一个圆括号里面,就可以正确执行。关于圆括号与解构赋值的关系,参见下文。

(2)解构赋值允许等号左边的模式之中,不放置任何变量名。因此,可以写出非常古怪的赋值表达式。

({} = [true, false]);
({} = 'abc');
({} = []);

上面的表达式虽然毫无意义,但是语法是合法的,可以执行。

(3)由于数组本质是特殊的对象,因此可以对数组进行对象属性的解构。

let arr = [1, 2, 3];
let {0 : first, [arr.length - 1] : last} = arr;
first // 1
last // 3

上面代码对数组进行对象解构。数组arr的0键对应的值是1,[arr.length - 1]就是2键,对应的值是3。方括号这种写法,属于“属性名表达式”。

4.6、解构赋值的用途

变量的解构赋值用途很多。

(1)交换变量的值

let x = 1;
let y = 2;

[x, y] = [y, x];

上面代码交换变量x和y的值,这样的写法不仅简洁,而且易读,语义非常清晰。

(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数据

解构赋值对提取 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]

上面代码可以快速提取 JSON 数据的值。

(5)函数参数的默认值

jQuery.ajax = function (url, {
  async = true,
  beforeSend = function () {},
  cache = true,
  complete = function () {},
  crossDomain = false,
  global = true,
  // ... more config
} = {}) {
  // ... do stuff
};

指定参数的默认值,就避免了在函数体内部再写var foo = config.foo || ‘default foo’;这样的语句。

(6)遍历Map结构

任何部署了 Iterator 接口的对象,都可以用for…of循环遍历。Map 结构原生支持 Iterator 接口,配合变量的解构赋值,获取键名和键值就非常方便。

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

如果只想获取键名,或者只想获取键值,可以写成下面这样。

// 获取键名
for (let [key] of map) {
  // ...
}

// 获取键值
for (let [,value] of map) {
  // ...
}

(7)输入模块的指定方法

加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰。

const { SourceMapConsumer, SourceNode } = require("source-map");

5、字符串、函数、数组、对象的扩展

5.1、模板字符串

传统的 JavaScript 语言,输出模板通常是这样写的(下面使用了 jQuery 的方法)。

$('#result').append(
  'There are <b>' + basket.count + '</b> ' +
  'items in your basket, ' +
  '<em>' + basket.onSale +
  '</em> are on sale!'
);

上面这种写法相当繁琐不方便,ES6 引入了模板字符串解决这个问题。

$('#result').append(`
  There are <b>${basket.count}</b> items
   in your basket, <em>${basket.onSale}</em>
  are on sale!
`);

模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。

// 普通字符串
`In JavaScript '\n' is a line-feed.`

// 多行字符串
`In JavaScript this is
 not legal.`

console.log(`string text line 1
string text line 2`);

// 字符串中嵌入变量
let name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`

上面代码中的模板字符串,都是用反引号表示。

转义符号

如果在模板字符串中需要使用反引号,则前面要用反斜杠转义。

let greeting = `\`Yo\` World!`;

多行字符串

如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中。

$('#list').html(`
<ul>
  <li>first</li>
  <li>second</li>
</ul>
`);

上面代码中,所有模板字符串的空格和换行,都是被保留的,比如 <ul> 标签前面会有一个换行。如果你不想要这个换行,可以使用 trim 方法消除它。

$('#list').html(`
<ul>
  <li>first</li>
  <li>second</li>
</ul>
`.trim());

插入变量

模板字符串中嵌入变量,需要将变量名写在${}之中。

function authorize(user, action) {
  if (!user.hasPrivilege(action)) {
    throw new Error(
      // 传统写法为
      // 'User '
      // + user.name
      // + ' is not authorized to do '
      // + action
      // + '.'
      `User ${user.name} is not authorized to do ${action}.`);
  }
}

插入表达式

大括号内部可以放入任意的 JavaScript 表达式,可以进行运算,以及引用对象属性。

let x = 1;
let y = 2;

`${x} + ${y} = ${x + y}`
// "1 + 2 = 3"

`${x} + ${y * 2} = ${x + y * 2}`
// "1 + 4 = 5"

let obj = {x: 1, y: 2};
`${obj.x + obj.y}`
// "3"

调用函数

模板字符串之中还能调用函数。

function fn() {
  return "Hello World";
}

`foo ${fn()} bar`
// foo Hello World bar

如果大括号中的值不是字符串,将按照一般的规则转为字符串。比如,大括号中是一个对象,将默认调用对象的 toString 方法。

如果模板字符串中的变量没有声明,将报错。

// 变量place没有声明
let msg = `Hello, ${place}`;
// 报错

由于模板字符串的大括号内部,就是执行 JavaScript 代码,因此如果大括号内部是一个字符串,将会原样输出。

`Hello ${'World'}`
// "Hello World"

注意要点

模板字符串中的换行和空格都是会被保留的

innerHtml = `<ul>
  <li>menu</li>
  <li>mine</li>
</ul>
`;
console.log(innerHtml);
// 输出
<ul>
 <li>menu</li>
 <li>mine</li>
</ul>
5.2、字符串扩展方法

(1)子串的识别

ES6 之前判断字符串是否包含子串,用 indexOf 方法,ES6 新增了子串的识别方法。

  • includes():返回布尔值,判断是否找到参数字符串。
  • startsWith():返回布尔值,判断参数字符串是否在原字符串的头部。
  • endsWith():返回布尔值,判断参数字符串是否在原字符串的尾部。

以上三个方法都可以接受两个参数,需要搜索的字符串,和可选的搜索起始位置索引。

let s = 'Hello world!';

s.startsWith('Hello') // true
s.endsWith('!') // true
s.includes('o') // true


这三个方法都支持第二个参数,表示开始搜索的位置。

let s = 'Hello world!';

s.startsWith('world', 6) // true
s.endsWith('Hello', 5) // true
s.includes('Hello', 6) // false


上面代码表示,使用第二个参数 n 时, endsWith 的行为与其他两个方法有所不同。它针对前 n 个字符,而其他两个方法针对从第 n 个位置直到字符串结束。

注意点:

  • 这三个方法只返回布尔值,如果需要知道子串的位置,还是得用 indexOf 和 lastIndexOf 。
  • 这三个方法如果传入了正则表达式而不是字符串,会抛出错误。而 indexOf 和 lastIndexOf 这两个方法,它们会将正则表达式转换为字符串并搜索它。

(2)字符串重复

repeat():返回新的字符串,表示将字符串重复指定次数返回。

'x'.repeat(3) // "xxx"
'hello'.repeat(2) // "hellohello"
'na'.repeat(0) // ""

参数如果是小数,会被向下取整。

'na'.repeat(2.9) // "nana"

如果 repeat 的参数是负数或者 Infinity ,会报错。

'na'.repeat(Infinity)
// RangeError
'na'.repeat(-1)
// RangeError

但是,如果参数是 0 到-1 之间的小数,则等同于 0,这是因为会先进行取整运算。0 到-1 之间的小数,取整以后等于 -0repeat 视同为 0。

'na'.repeat(-0.9) // ""

参数 NaN 等同于 0。

'na'.repeat(NaN) // ""

如果 repeat 的参数是字符串,则会先转换成数字。

'na'.repeat('na') // ""
'na'.repeat('3') // "nanana"

(3)字符串补全

ES2017 引入了字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。

  • padStart:返回新的字符串,表示用参数字符串从头部(左侧)补全原字符串。
  • padEnd:返回新的字符串,表示用参数字符串从尾部(右侧)补全原字符串。

以上两个方法接受两个参数,第一个参数是指定生成的字符串的最小长度,第二个参数是用来补全的字符串。如果没有指定第二个参数,默认用空格填充。

console.log("h".padStart(5,"o"));  // "ooooh"
console.log("h".padEnd(5,"o"));    // "hoooo"
console.log("h".padStart(5));      // "    h"

console.log('x'.padStart(5, 'ab')); // 'ababx'
console.log('x'.padStart(4, 'ab')); // 'abax'

console.log('x'.padEnd(5, 'ab')); // 'xabab'
console.log('x'.padEnd(4, 'ab')); // 'xaba'

上面代码中, padStart()padEnd() 一共接受两个参数,第一个参数是字符串补全生效的最大长度,第二个参数是用来补全的字符串。

如果指定的长度小于或者等于原字符串的长度,则返回原字符串:

console.log("hello".padStart(5,"A"));  // "hello"

如果原字符串加上补全字符串长度大于指定长度,则截去超出位数的补全字符串:

console.log("hello".padEnd(10,",world!"));  // "hello,worl"

如果省略第二个参数,默认使用空格补全长度。

console.log('x'.padStart(4)); // '   x'
console.log('x'.padEnd(4)); // 'x   '

padStart()的常见用途是为数值补全指定位数。下面代码生成 10 位的数值字符串。

console.log('1'.padStart(10, '0')); // "0000000001"
console.log('12'.padStart(10, '0')); // "0000000012"
console.log('123456'.padStart(10, '0')); // "0000123456"

另一个用途是提示字符串格式。

console.log('12'.padStart(10, 'YYYY-MM-DD')); // "YYYY-MM-12"
console.log('09-12'.padStart(10, 'YYYY-MM-DD')); // "YYYY-09-12"

(4)消除空格

ES6对字符串实例新增了 trimStart()trimEnd() 这两个方法。它们的行为与 trim() 一致,trimStart() 消除字符串头部的空格,trimEnd() 消除尾部的空格。它们返回的都是新字符串,不会修改原始字符串。

const s = '  abc  ';

s.trim() // "abc"
s.trimStart() // "abc  "
s.trimEnd() // "  abc"

上面代码中,trimStart() 只消除头部的空格,保留尾部的空格。trimEnd() 也是类似行为。

除了空格键,这两个方法对字符串头部(或尾部)的 tab 键、换行符等不可见的空白符号也有效。

浏览器还部署了额外的两个方法,trimLeft()trimStart() 的别名,trimRight()trimEnd() 的别名。

5.3、函数的扩展

(1)默认值

ES6 之前,不能直接为函数的参数指定默认值,只能采用变通的方法。

function log(x, y) {
  y = y || 'World';
  console.log(x, y);
}

log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello World

上面代码检查函数log的参数y有没有赋值,如果没有,则指定默认值为World。这种写法的缺点在于,如果参数y赋值了,但是对应的布尔值为false,则该赋值不起作用。就像上面代码的最后一行,参数y等于空字符,结果被改为默认值。

为了避免这个问题,通常需要先判断一下参数y是否被赋值,如果没有,再等于默认值。

if (typeof y === 'undefined') {
  y = 'World';
}

ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。

function log(x, y = 'World') {
  console.log(x, y);
}

log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello

可以看到,ES6 的写法比 ES5 简洁许多,而且非常自然。下面是另一个例子。

function Point(x = 0, y = 0) {
  this.x = x;
  this.y = y;
}

const p = new Point();
p // { x: 0, y: 0 }

除了简洁,ES6 的写法还有两个好处:首先,阅读代码的人,可以立刻意识到哪些参数是可以省略的,不用查看函数体或文档;其次,有利于将来的代码优化,即使未来的版本在对外接口中,彻底拿掉这个参数,也不会导致以前的代码无法运行。

参数变量是默认声明的,所以不能用letconst再次声明。

function foo(x = 5) {
  let x = 1; // error
  const x = 2; // error
}

上面代码中,参数变量x是默认声明的,在函数体中,不能用letconst再次声明,否则会报错。

使用参数默认值时,函数不能有同名参数。

// 不报错
function foo(x, x, y) {
  // ...
}

// 报错
function foo(x, x, y = 1) {
  // ...
}
// SyntaxError: Duplicate parameter name not allowed in this context

另外,一个容易忽略的地方是,参数默认值不是传值的,而是每次都重新计算默认值表达式的值。也就是说,参数默认值是惰性求值的。

let x = 99;
function foo(p = x + 1) {
  console.log(p);
}

foo() // 100

x = 100;
foo() // 101

上面代码中,参数p的默认值是x + 1。这时,每次调用函数foo,都会重新计算x + 1,而不是默认p等于 100。

(2)不定参数

不定参数用来表示不确定参数个数,形如,…变量名,由…加上一个具名参数标识符组成。具名参数只能放在参数组的最后,并且有且只有一个不定参数。

基本用法

function f(...values){
    console.log(values.length);
}
f(1,2);      //2
f(1,2,3,4);  //4

(3)箭头函数

箭头函数提供了一种更加简洁的函数书写方式。基本语法是:

参数 => 函数体

基本用法:

var f = v => v;
//等价于
var f = function(a){
 return a;
}
f(1);  //1

当箭头函数没有参数或者有多个参数,要用 () 括起来。

var f = (a,b) => a+b;
f(6,2);  //8

当箭头函数函数体有多行语句,用 {} 包裹起来,表示代码块,当只有一行语句,并且需要返回结果时,可以省略 {} , 结果会自动返回。

var f = (a,b) => {
 let result = a+b;
 return result;
}
f(6,2);  // 8

当箭头函数要返回对象的时候,为了区分于代码块,要用 () 将对象包裹起来

// 报错
var f = (id,name) => {id: id, name: name};
f(6,2);  // SyntaxError: Unexpected token :
 
// 不报错
var f = (id,name) => ({id: id, name: name});
f(6,2);  // {id: 6, name: 2}

注意点:没有 this、super、arguments 和 new.target 绑定。

var func = () => {
  // 箭头函数里面没有 this 对象,
  // 此时的 this 是外层的 this 对象,即 Window 
  console.log(this)
}
func(55)  // Window 
 
var func = () => {    
  console.log(arguments)
}
func(55);  // ReferenceError: arguments is not defined

箭头函数体中的 this 对象,是定义函数时的对象,而不是使用函数时的对象。

function fn(){
  setTimeout(()=>{
    // 定义时,this 绑定的是 fn 中的 this 对象
    console.log(this.a);
  },0)
}
var a = 20;
// fn 的 this 对象为 {a: 19}
fn.call({a: 18});  // 18

不可以作为构造函数,也就是不能使用 new 命令,否则会报错

5.4、数组的扩展

(1)扩展运算符

扩展运算符(spread)是三个点(...)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。

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 push(array, ...items) {
  array.push(...items);
}

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

const numbers = [4, 38];
add(...numbers) // 42

上面代码中,array.push(...items)add(...numbers)这两行,都是函数的调用,它们都使用了扩展运算符。该运算符将一个数组,变为参数序列。

(2)扩展运算符的应用

复制数组

数组是复合的数据类型,直接复制的话,只是复制了指向底层数据结构的指针,而不是克隆一个全新的数组。

const a1 = [1, 2];
const a2 = a1;

a2[0] = 2;
a1 // [2, 2]

上面代码中,a2并不是a1的克隆,而是指向同一份数据的另一个指针。修改a2,会直接导致a1的变化。

ES5 只能用变通方法来复制数组。

const a1 = [1, 2];
const a2 = a1.concat();

a2[0] = 2;
a1 // [1, 2]

上面代码中,a1会返回原数组的克隆,再修改a2就不会对a1产生影响。

扩展运算符提供了复制数组的简便写法。

const a1 = [1, 2];
// 写法一
const a2 = [...a1];
// 写法二
const [...a2] = a1;

上面的两种写法,a2都是a1的克隆。

合并数组

扩展运算符提供了数组合并的新写法。

const arr1 = ['a', 'b'];
const arr2 = ['c'];
const arr3 = ['d', 'e'];

// ES5 的合并数组
arr1.concat(arr2, arr3);
// [ 'a', 'b', 'c', 'd', 'e' ]

// ES6 的合并数组
[...arr1, ...arr2, ...arr3]
// [ 'a', 'b', 'c', 'd', 'e' ]

不过,这两种方法都是浅拷贝,使用的时候需要注意。

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

上面代码中,a3a4是用两种不同方法合并而成的新数组,但是它们的成员都是对原数组成员的引用,这就是浅拷贝。如果修改了原数组的成员,会同步反映到新数组。

(3)数组实例的find()和findIndex()

数组实例的find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined

[1, 4, -5, 10].find((n) => n < 0)
// -5

上面代码找出数组中第一个小于 0 的成员。

[1, 5, 10, 15].find(function(value, index, arr) {
  return value > 9;
}) // 10

上面代码中,find方法的回调函数可以接受三个参数,依次为当前的值、当前的位置和原数组。

数组实例的findIndex方法的用法与find方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1

[1, 5, 10, 15].findIndex(function(value, index, arr) {
  return value > 9;
}) // 2

5.5、对象的扩展

ES6 允许在大括号里面,直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁。

const foo = 'bar';

const baz = {foo};

baz // {foo: "bar"}

// 等同于const baz = {foo: foo};

除了属性简写,方法也可以简写。

const o = {

 	method() {

 	 	return "Hello!";

 	}
};

// 等同于

const o = {

 	method: function() {

  		return "Hello!";

 	}
};

对象的新方法

Object.assign(target, source_1, ···)

用于将源对象的所有可枚举属性复制到目标对象中。

基本用法

let target = {a: 1}; 

let object2 = {b: 2}; 

let object3 = {c: 3}; 

Object.assign(target,object2,object3); // 第一个参数是目标对象,后面的参数是源对象 
target; // {a: 1, b: 2, c: 3}

6、Class基本使用和继承

6.1、类的由来

JavaScript 语言中,生成实例对象的传统方法是通过构造函数。下面是一个例子。

function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.toString = function () {
  return '(' + this.x + ', ' + this.y + ')';
};

var p = new Point(1, 2);

上面这种写法跟传统的面向对象语言(比如 C++ 和 Java)差异很大,很容易让新学习这门语言的程序员感到困惑。

ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。

基本上,ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。上面的代码用 ES6 的class改写,就是下面这样。

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}

上面代码定义了一个“类”,可以看到里面有一个constructor方法,这就是构造方法,而this关键字则代表实例对象。也就是说,ES5 的构造函数Point,对应 ES6 的Point类的构造方法。

Point类除了构造方法,还定义了一个toString方法。注意,定义“类”的方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。另外,方法之间不需要逗号分隔,加了会报错。

6.2、constructor方法

constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。

class Point {
}

// 等同于
class Point {
  constructor() {}
}

上面代码中,定义了一个空的类Point,JavaScript 引擎会自动为它添加一个空的constructor方法。

6.3、类的实例

生成类的实例的写法,与 ES5 完全一样,也是使用new命令。前面说过,如果忘记加上new,像函数那样调用Class,将会报错。

class Point {
  // ...
}

// 报错
var point = Point(2, 3);

// 正确
var point = new Point(2, 3);

6.4、类的继承

Class 可以通过extends关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

class Point {
}

class ColorPoint extends Point {
}

super关键字

super这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。

第一种情况,super作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次super函数。

class A {}

class B extends A {
  constructor() {
    super();
  }
}

上面代码中,子类B的构造函数之中的super(),代表调用父类的构造函数。这是必须的,否则 JavaScript 引擎会报错。

第二种情况,super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。

class A {
  p() {
    return 2;
  }
}

class B extends A {
  constructor() {
    super();
    console.log(super.p()); // 2
  }
}

let b = new B();

上面代码中,子类B当中的super.p(),就是将super当作一个对象使用。这时,super在普通方法之中,指向A.prototype,所以super.p()就相当于A.prototype.p()

6.5、静态方法

类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。

class Foo {
  static classMethod() {
    return 'hello';
  }
}

Foo.classMethod() // 'hello'

var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function

上面代码中,Foo类的classMethod方法前有static关键字,表明该方法是一个静态方法,可以直接在Foo类上调用(Foo.classMethod()),而不是在Foo类的实例上调用。如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法。

6.6、静态属性

静态属性指的是 Class 本身的属性,即Class.propName,而不是定义在实例对象(this)上的属性。

class Foo {
}

Foo.prop = 1;
Foo.prop // 1

上面的写法为Foo类定义了一个静态属性prop

目前,只有这种写法可行,因为 ES6 明确规定,Class 内部只有静态方法,没有静态属性。现在有一个提案提供了类的静态属性,写法是在实例属性的前面,加上static关键字。

class MyClass {
  static myStaticProp = 42;

  constructor() {
    console.log(MyClass.myStaticProp); // 42
  }
}

这个新写法大大方便了静态属性的表达。

// 老写法
class Foo {
  // ...
}
Foo.prop = 1;

// 新写法
class Foo {
  static prop = 1;
}

上面代码中,老写法的静态属性定义在类的外部。整个类生成以后,再生成静态属性。这样让人很容易忽略这个静态属性,也不符合相关代码应该放在一起的代码组织原则。另外,新写法是显式声明(declarative),而不是赋值处理,语义更好。

7、Set和Map数据结构

7.1、Set

ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。

基础用法:

let mySet = new Set();
 
mySet.add(1); // Set(1) {1}
mySet.add(5); // Set(2) {1, 5}
mySet.add(5); // Set(2) {1, 5} 这里体现了值的唯一性
mySet.add("some text"); 
// Set(3) {1, 5, "some text"} 这里体现了类型的多样性
var o = {a: 1, b: 2}; 
mySet.add(o);
mySet.add({a: 1, b: 2}); 
// Set(5) {1, 5, "some text", {…}, {…}} 
// 这里体现了对象之间引用不同不恒等,即使值相同,Set 也能存储

上面代码通过add()方法向 Set 结构加入成员,结果表明 Set 结构不会添加重复的值。

Set函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。

// 例一
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

数据类型转换

Array与Set类型转换

// Array 转 Set
var mySet = new Set(["value1", "value2", "value3"]);
// 用...操作符,将 Set 转 Array
var myArray = [...mySet];

//Array.from方法可以将 Set 结构转为数组。
const items = new Set([1, 2, 3, 4, 5]);
const array = Array.from(items);

String与Set类型转换

// String 转 Set
var mySet = new Set('hello');  // Set(4) {"h", "e", "l", "o"}
// 注:Set 中 toString 方法是不能将 Set 转换成 String

Set实例的属性

  • Set.prototype.constructor:构造函数,默认就是Set函数。
  • Set.prototype.size:返回Set实例的成员总数。

Set实例的操作方法

  • Set.prototype.add(value):添加某个值,返回 Set 结构本身。
  • Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
  • Set.prototype.has(value):返回一个布尔值,表示该值是否为Set的成员。
  • Set.prototype.clear():清除所有成员,没有返回值。

代码示例:

s.add(1).add(2).add(2);
// 注意2被加入了两次

s.size // 2

s.has(1) // true
s.has(2) // true
s.has(3) // false

s.delete(2);
s.has(2) // false

Set实例的遍历方法

  • Set.prototype.keys():返回键名的遍历器
  • Set.prototype.values():返回键值的遍历器
  • Set.prototype.entries():返回键值对的遍历器
  • Set.prototype.forEach():使用回调函数遍历每个成员

需要特别指出的是,Set的遍历顺序就是插入顺序。这个特性有时非常有用,比如使用 Set 保存一个回调函数列表,调用时就能保证按照添加顺序调用。

代码示例:

keys方法、values方法、entries方法返回的都是遍历器对象(详见《Iterator 对象》一章)。由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致。

let set = new Set(['red', 'green', 'blue']);

for (let item of set.keys()) {
  console.log(item);
}
// red
// green
// blue

for (let item of set.values()) {
  console.log(item);
}
// red
// green
// blue

for (let item of set.entries()) {
  console.log(item);
}
// ["red", "red"]
// ["green", "green"]
// ["blue", "blue"]

forEach()代码示例:

let set = new Set([1, 4, 9]);
set.forEach((value, key) => console.log(key + ' : ' + value))
// 1 : 1
// 4 : 4
// 9 : 9

遍历的应用

(1)数组去重

var mySet = new Set([1, 2, 3, 4, 4]);
[...mySet]; // [1, 2, 3, 4]

(2)并集

var a = new Set([1, 2, 3]);
var b = new Set([4, 3, 2]);
var union = new Set([...a, ...b]); // {1, 2, 3, 4}

(3)交集

var a = new Set([1, 2, 3]);
var b = new Set([4, 3, 2]);
var intersect = new Set([...a].filter(x => b.has(x))); // {2, 3}

(4)差集

var a = new Set([1, 2, 3]);
var b = new Set([4, 3, 2]);
var difference = new Set([...a].filter(x => !b.has(x))); // {1}
7.2、Map

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中的key

key是字符串

var myMap = new Map();
var keyString = "a string"; 
 
myMap.set(keyString, "和键'a string'关联的值");
 
myMap.get(keyString);    // "和键'a string'关联的值"
myMap.get("a string");   // "和键'a string'关联的值"
                         // 因为 keyString === 'a string'

key是对象

var myMap = new Map();
var keyObj = {}, 
 
myMap.set(keyObj, "和键 keyObj 关联的值");

myMap.get(keyObj); // "和键 keyObj 关联的值"
myMap.get({}); // undefined, 因为 keyObj !== {}

key是函数

var myMap = new Map();
var keyFunc = function () {}, // 函数
 
myMap.set(keyFunc, "和键 keyFunc 关联的值");
 
myMap.get(keyFunc); // "和键 keyFunc 关联的值"
myMap.get(function() {}) // undefined, 因为 keyFunc !== function () {}

Map的遍历

对 Map 进行遍历,以下两个最高级。

(1)for…of

var myMap = new Map();
myMap.set(0, "zero");
myMap.set(1, "one");
 
// 将会显示两个 log。 一个是 "0 = zero" 另一个是 "1 = one"
for (var [key, value] of myMap) {
  console.log(key + " = " + value);
}
for (var [key, value] of myMap.entries()) {
  console.log(key + " = " + value);
}
/* 这个 entries 方法返回一个新的 Iterator 对象,它按插入顺序包含了 Map 对象中每个元素的 [key, value] 数组。 */
 
// 将会显示两个log。 一个是 "0" 另一个是 "1"
for (var key of myMap.keys()) {
  console.log(key);
}
/* 这个 keys 方法返回一个新的 Iterator 对象, 它按插入顺序包含了 Map 对象中每个元素的键。 */
 
// 将会显示两个log。 一个是 "zero" 另一个是 "one"
for (var value of myMap.values()) {
  console.log(value);
}
/* 这个 values 方法返回一个新的 Iterator 对象,它按插入顺序包含了 Map 对象中每个元素的值。 */

(2)forEach()

var myMap = new Map();
myMap.set(0, "zero");
myMap.set(1, "one");
 
// 将会显示两个 logs。 一个是 "0 = zero" 另一个是 "1 = one"
myMap.forEach(function(value, key) {
  console.log(key + " = " + value);
}, myMap)

8、Promise的对象

8.1、Promise概述

是异步编程的一种解决方案。

从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。

基本用法:

const promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。

resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。

then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。其中,第二个函数是可选的,不一定要提供。这两个函数都接受Promise对象传出的值作为参数。

8.2、Promise状态的特点

Promise 异步操作有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。除了异步操作的结果,任何其他操作都无法改变这个状态。

Promise 对象只有:从 pending 变为 fulfilled 和从 pending 变为 rejected 的状态改变。只要处于 fulfilled 和 rejected ,状态就不会再变了即 resolved(已定型)。

const p1 = new Promise(function(resolve,reject){
    resolve('success1');
    resolve('success2');
}); 
const p2 = new Promise(function(resolve,reject){  
    resolve('success3'); 
    reject('reject');
});
p1.then(function(value){  
    console.log(value); // success1
});
p2.then(function(value){ 
    console.log(value); // success3
});

状态的缺点

无法取消 Promise ,一旦新建它就会立即执行,无法中途取消。

如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。

当处于 pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

8.3、Promise的方法

then()方法

then 方法接收两个函数作为参数,第一个参数是 Promise 执行成功时的回调,第二个参数是 Promise 执行失败时的回调,两个函数只会有一个被调用。

在 JavaScript 事件队列的当前运行完成之前,回调函数永远不会被调用。

const p = new Promise(function(resolve,reject){
  resolve('success');
});
 
p.then(function(value){
  console.log(value);
});
 
console.log('first');
// first
// success

catch()方法

Promise.prototype.catch方法是.then(null, rejection).then(undefined, rejection)的别名,用于指定发生错误时的回调函数。

getJSON('/posts.json').then(function(posts) {
  // ...
}).catch(function(error) {
  // 处理 getJSON 和 前一个回调函数运行时发生的错误
  console.log('发生错误!', error);
});

上面代码中,getJSON方法返回一个 Promise 对象,如果该对象状态变为resolved,则会调用then方法指定的回调函数;如果异步操作抛出错误,状态就会变为rejected,就会调用catch方法指定的回调函数,处理这个错误。另外,then方法指定的回调函数,如果运行中抛出错误,也会被catch方法捕获。

p.then((val) => console.log('fulfilled:', val))
  .catch((err) => console.log('rejected', err));

// 等同于
p.then((val) => console.log('fulfilled:', val))
  .then(null, (err) => console.log("rejected:", err));

all()方法

Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

const p = Promise.all([p1, p2, p3]);

上面代码中,Promise.all()方法接受一个数组作为参数,p1p2p3都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。另外,Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。

p的状态由p1p2p3决定,分成两种情况。

(1)只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数。

(2)只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

下面是一个具体的例子。

// 生成一个Promise对象的数组
const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
  return getJSON('/post/' + id + ".json");
});

Promise.all(promises).then(function (posts) {
  // ...
}).catch(function(reason){
  // ...
});

上面代码中,promises是包含 6 个 Promise 实例的数组,只有这 6 个实例的状态都变成fulfilled,或者其中有一个变为rejected,才会调用Promise.all方法后面的回调函数。

9、async函数

9.1、概念

ES2017 标准引入了 async 函数,使得异步操作变得更加方便。

async 函数是什么?一句话,它就是 Generator 函数的语法糖

9.2、基本用法

async函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。

下面是一个例子。

async function getStockPriceByName(name) {
  const symbol = await getStockSymbol(name);
  const stockPrice = await getStockPrice(symbol);
  return stockPrice;
}

getStockPriceByName('goog').then(function (result) {
  console.log(result);
});

上面代码是一个获取股票报价的函数,函数前面的async关键字,表明该函数内部有异步操作。调用该函数时,会立即返回一个Promise对象。

9.3、语法

async函数返回一个 Promise 对象。

async函数内部return语句返回的值,会成为then方法回调函数的参数。

async function f() {
  return 'hello world';
}

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: 出错了

React基础入门

1、React简介

1.1、React简介

React起源于Facebook的内部项目,因为该公司对市场上所有JavaScript MVC框架都不满意,就决定自己写一套,用来架设 Instagram 的网站。做出来以后,发现这套东西很好用,于是就在2013年5月开源了。

1.2、使用React脚手架安装

1.全局命令cnpm install -g create-react-app

2.更换源 npm config set registry https://registry.npm.taobao.org

npm config set registry https://registry.npm.taobao.org
-- 配置后可通过下面方式来验证是否成功
npm config get registry
-- 显示出上述地址的话就是更换成功

3.创建todolist create-react-app todolist

4.进入目录 cd todolist

5.启动项目 npm start

1.3、脚手架代码精简

项目目录说明:

node_modules ===> npm 下载的相关依赖
package.json ===> node包文件 包括项目的一些介绍 以及 相关文件版本

public文件夹:

index.html ===> 项目的首页

favico.ico ===> 项目图标

mainfest.json ===> 定义网页快捷方式

src 文件夹:

index.js ===> 整个项目的入口文件
registerServiceWorker.js ===> 离线缓存

代码工程精简

src 目录下仅保存 App.jsindex.js 文件

index.js 是入口文件,精简代码如下:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));

App.js 文件精简代码如下:

import React from 'react';

function App() {
  return (
    <div>
      hello React...
    </div>
  );
}

export default App;

2、React基础语法

2.1、 JSX 语法基础

React 使用 JSX 来替代常规的JavaScript

JSX 是按照 XML 语法规范 的 JavaScript 语法扩展。

JSX 有以下优点:

  • JSX 执行更快,因为它在编译为 JavaScript 代码后进行了优化;
  • 它是类型安全的,在编译过程中就能发现错误;
  • 使用 JSX 编写模板更加简单快速。

**JSX 语法的本质:**并不是直接把 JSX 渲染到页面上,而是内部先转换成了 createElement 形式,再渲染的。

**JSX 注释:**推荐使用 {/* 这是注释 */}

**JSX中添加class类名:**需要使用 className 来替代 classhtmlFor 替代 labelfor 属性;

JSX创建DOM的时候,所有节点必须有唯一的根元素进行包裹;

JSX语法中,标签必须成对出现,如果是单标签,则必须自闭和;

代码示例:

const mydiv = <div>这是一个Div标签</div>;
ReactDOM.render(mydiv, document.getElementById('root'));

使用组件化开发代码示例:

App.js 组件文件代码

import React from 'react';

class App extends React.Component{
  render(){
    return (
      <div>
        {1+1}
        <hr/>
        Hello,Reactjs!!
      </div>
    );
  }
}

export default App;

在其他文件中使用 JSX 语法引用组件:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));

渲染数字

import React from 'react';
import ReactDOM from 'react-dom';

let a = 10;

ReactDOM.render(
    <div>{a}</div>
, document.getElementById('root'));

渲染字符串

import React from 'react';
import ReactDOM from 'react-dom';

let str = 'hello react';

ReactDOM.render(
    <div>{str}</div>
, document.getElementById('root'));

渲染布尔类型

import React from 'react';
import ReactDOM from 'react-dom';

let rel = true;

ReactDOM.render(
    <div>{rel ? '结果为真' : '结果为假'}</div>
, document.getElementById('root'));

渲染属性值

import React from 'react';
import ReactDOM from 'react-dom';

let title = "this is a div";

ReactDOM.render(
    <div title={title}>Hello React</div>
, document.getElementById('root'));

渲染标签对象

import React from 'react';
import ReactDOM from 'react-dom';

const h1 = <h1>Hello React!</h1>;

ReactDOM.render(
    <div>
        {h1}
    </div>
, document.getElementById('root'));

渲染数组

import React from 'react';
import ReactDOM from 'react-dom';

const arr = [
    <h1>1</h1>,
    <h2>2</h2>,
];

ReactDOM.render(
    <div>
        {arr}
    </div>
, document.getElementById('root'));

将普通数组转为 JSX 数组,并渲染到页面中

解决 Warning: Each child in a list should have a unique “key” prop.

方法一:

import React from 'react';
import ReactDOM from 'react-dom';

//原数组
const arr = ['html','css','vue'];
//新数组
const newArr = [];
//forEach()方法没有返回值
arr.forEach((item,index) => {
    const temp = <h2 key={index}>{item}</h2>
    newArr.push(temp);
});

ReactDOM.render(
    <div>
        {newArr}
    </div>
, document.getElementById('root'));

方法二:

import React from 'react';
import ReactDOM from 'react-dom';
import TodoList from './TodoList';
import './style.css';

//原数组
const arr = ['html','css','vue'];

ReactDOM.render(
    <div>
    	{/* map()方法有返回值 */}
        {arr.map((item,index) => {
        return <h2 key={index}>{item}</h2>
        })}
    </div>
, document.getElementById('root'));

2.2、React组件

组件的概念

React 应用都是构建在组件之上。

组件作为React的核心内容,是View的重要组成部分,每一个View页面都由一个或多个组件构成,可以说组件是React应用程序的基石。在React的组件构成中,按照状态来分可以分为有状态组件和无状态组件。

组件的特点

组件化的概念在后端早已存在多年,只不过近几年随着前端的发展,这个概念在前端开始被频繁提及,特别是在MV*的框架中。

前端中的“组件化”这个词,在UI这一层通常指“标签化”,也就是把大块的业务界面,拆分成若干小块,然后进行组装。

狭义的组件化一般是指标签化,也就是以自定义标签(自定义属性)为核心的机制。

广义的组件化包括对数据逻辑层业务梳理,形成不同层级的能力封装。

  • 标准性

    任何一个组件都应该遵守一套标准,可以使得不同区域的开发人员据此标准开发出一套标准统一的组件。

  • 组合性

    组件之前应该是可以组合的。我们知道前端页面的展示都是一些HTML DOM的组合,而组件在最终形态上也可以理解为一个个的HTML片段。那么组成一个完整的界面展示,肯定是要依赖不同组件之间的组合,嵌套以及通信。

  • 重用性

    任何一个组件应该都是一个可以独立的个体,可以使其应用在不同的场景中。

  • 可维护性

    任何一个组件应该都具有一套自己的完整的稳定的功能,仅包含自身的,与其它组件无关的逻辑,使其更加的容易理解,使其更加的容易理解,同时大大减少发生bug的几率。

函数组件

无状态函数式组件形式上表现为一个只带有一个 render() 方法的组件类,通过函数形式或者 ES6 箭头 function的形式在创建,并且该组件是无state状态的。具体的创建形式如下

import React from 'react';

//定义一个React组件
function App() {
  return (
    <div>
      hello React...
    </div>
  );
}

export default App;

class组件

React.Component 是以 ES6 的形式来创建react的组件的,是React目前极为推荐的创建有状态组件的方式,形式改为 React.Component 形式如下

import React from 'react';

//定义一个React组件
class App extends React.Component{
  render(){
    return (
      <div>
        Hello,Reactjs!!
      </div>
    );
  }
}

export default App;

在其他文件中引用组件

import React from 'react';
import ReactDOM from 'react-dom';
//App组件,组件要求大写字母开头
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));

函数组件和class组件的区别

用构造函数创建出来的组件,叫做“无状态组件”;

用class关键字创建出来的组件,叫做“有状态组件”;

有状态组件和无状态组件之间的本质区别是有无state属性

注意:

使用class 关键字创建的组件,有自己的私有数据(this.state)和生命周期函数;

使用function创建的组件,只有props,没有自己的私有数据和生命周期函数;

函数组件和类组件当然是有区别的,而且函数组件的性能比类组件的性能要高,因为类组件使用的时候要实例化,而函数组件直接执行函数取返回结果即可。为了提高性能,尽量使用函数组件

区别函数组件class组件
是否有 this没有
是否有生命周期没有
是否有状态 state没有

3、React核心属性

3.1.1、props属性

react中说的单向数据流值说的就是props,根据这一特点它还有一个作用:组件之间的通信。props本身是不可变的,但是有一种情形它貌似可变,即是将父组件的state作为子组件的props,当父组件的state改变,子组件的props也跟着改变,其实它仍旧遵循了这一定律:props是不可更改的。

props属性的特点:

1.每个组件对象都会有props(properties的简写)属性

2.组件标签的所有属性都保存在props中

3.内部读取某个属性值:this.props.propertyName

4.作用:通过标签属性从组件外 向组件内传递数据(只读 read only)

5.对props中的属性值进行类型限制和必要性限制

代码示例

使用函数组件:

import React from 'react';
import ReactDOM from 'react-dom';

//使用函数组件
function User(props){
    //在组件中获取props属性值
	return <div>{props.name}{props.age}</div>
}

//定义数据
const person ={
    name:'张三',
    age:20,
    sex:'男'
}

ReactDOM.render(
    <User {...person}></User>
, document.getElementById('root'));

使用类组件:

import React from 'react';
import ReactDOM from 'react-dom';

//使用class组件
class User extends React.Component{
    render(){
        return (
    <div>{this.props.name}--{this.props.age}</div>
        );
    }
}

//数据
const person ={
    name:'张三',
    age:20,
    sex:'男'
}

ReactDOM.render(
    <User {...person}></User>
, document.getElementById('root'));

3.1.2、state 属性

React 把组件看成是一个状态机(State Machines)。通过与用户的交互,实现不同状态,然后渲染 UI,让用户界面和数据保持一致。

React 里,只需更新组件的 state,然后根据新的 state 重新渲染用户界面(不要操作 DOM)。

代码示例:

import React from 'react';
import ReactDOM from 'react-dom';

class Person extends React.Component{
	//构造方法
    constructor(){
        super();
        this.state = {
            name: 'tom'
        }
    }

    render(){
        //state属性是可修改的
        this.state.name = 'jack';
        return (
        <h1>{this.state.name}</h1>
        );
    }
}

ReactDOM.render(<Person />, document.getElementById('root'));

设置状态:setState

setState(object nextState[, function callback])

不能在组件内部通过this.state修改状态,因为该状态会在调用setState()后被替换。

setState()并不会立即改变this.state,而是创建一个即将处理的state。setState()并不一定是同步的,为了提升性能React会批量执行state和DOM渲染。

setState()总是会触发一次组件重绘,除非在shouldComponentUpdate()中实现了一些条件渲染逻辑

3.1.3、propsstate属性的区别

  • props中的数据都是外界传递过来的;
  • state中的数据都是组件私有的;(通过Ajax获取回来的数据,一般都是私有数据)
  • props中的数据都是只读的,不能重新赋值;
  • state中的数据,都是可读可写的;
  • 子组件只能通过props传递数据;

3.1.4、refs 属性

在React数据流中,父子组件唯一的交流方式是通过props属性;如果要修改子组件,通过修改父组件的属性,更新达到子组件props属性的值,重新渲染组件以达到视图的更新。但是,有些场景需要获取某一个真实的DOM元素来交互,比如文本框的聚焦、触发强制动画等

  • 给DOM元素添加ref属性
  • 给类组件添加ref属性

代码示例:

import React from 'react';
import ReactDOM from 'react-dom';

class Person extends React.Component{

    constructor(){
        super();
        this.handleClick = this.handleClick.bind(this);
    }

    //点击事件
    handleClick(){
        // 使用原生的 DOM API 获取焦点
        this.refs.myInput.focus();
    }

    render(){
        return (
            <div>
                <input type="text" ref="myInput" />
                <input
                    type="button"
                    value="点我输入框获取焦点"
                    onClick={this.handleClick}/>
            </div>
        );    
    }
}

ReactDOM.render(<Person />, document.getElementById('root'));

4、组件的生命周期

4.1、组件生命周期的三个阶段

  1. Mounting(加载阶段)
  2. Updating(更新阶段)
  3. Unmounting(卸载阶段)

4.2、旧的生命周期

图片描述

Mounting(加载阶段:涉及6个钩子函数)

constructor()

加载的时候调用一次,可以初始化state

getDefaultProps()

设置默认的props,也可以用dufaultProps设置组件的默认属性。

getInitialState()

初始化state,可以直接在constructor中定义this.state

componentWillMount()

组件加载时只调用,以后组件更新不调用,整个生命周期只调用一次,此时可以修改state

render()

react最重要的步骤,创建虚拟dom,进行diff算法,更新dom树都在此进行

componentDidMount()

组件渲染之后调用,只调用一次
Updating(更新阶段:涉及5个钩子函数)

componentWillReceivePorps(nextProps)

组件加载时不调用,组件接受新的props时调用

shouldComponentUpdate(nextProps, nextState)

组件接收到新的props或者state时调用,return true就会更新dom(使用diff算法更新),return false能阻止更新(不调用render)

componentWillUpdata(nextProps, nextState)

组件加载时不调用,只有在组件将要更新时才调用,此时可以修改state

render()

react最重要的步骤,创建虚拟dom,进行diff算法,更新dom树都在此进行

componentDidUpdate()

组件加载时不调用,组件更新完成后调用
Unmounting(卸载阶段:涉及1个钩子函数)

componentWillUnmount()

组件渲染之后调用,只调用一次
组件的基本写法
import React, { Component } from 'react'

export default class OldReactComponent extends Component {
    constructor(props) {
        super(props)
        // getDefaultProps:接收初始props
        // getInitialState:初始化state
    }
    state = {

    }
    componentWillMount() { // 组件挂载前触发

    }
    render() {
        return (
            <h2>Old React.Component</h2>
        )
    }
    componentDidMount() { // 组件挂载后触发

    }
    componentWillReceivePorps(nextProps) { // 接收到新的props时触发

    }
    shouldComponentUpdate(nextProps, nextState) { // 组件Props或者state改变时触发,true:更新,false:不更新
        return true
    }
    componentWillUpdate(nextProps, nextState) { // 组件更新前触发

    }
    componentDidUpdate() { // 组件更新后触发

    }
    componentWillUnmount() { // 组件卸载时触发

    }
}

4.3、新的生命周期

Mounting(加载阶段:涉及4个钩子函数)

constructor()

加载的时候调用一次,可以初始化state

static getDerivedStateFromProps(props, state)

组件每次被rerender的时候,包括在组件构建之后(虚拟dom之后,实际dom挂载之前),每次获取新的props或state之后;每次接收新的props之后都会返回一个对象作为新的state,返回null则说明不需要更新state;配合componentDidUpdate,可以覆盖componentWillReceiveProps的所有用法

render()

react最重要的步骤,创建虚拟dom,进行diff算法,更新dom树都在此进行

componentDidMount()

组件渲染之后调用,只调用一次
Updating(更新阶段:涉及5个钩子函数)

static getDerivedStateFromProps(props, state)

组件每次被rerender的时候,包括在组件构建之后(虚拟dom之后,实际dom挂载之前),每次获取新的props或state之后;每次接收新的props之后都会返回一个对象作为新的state,返回null则说明不需要更新state;配合componentDidUpdate,可以覆盖componentWillReceiveProps的所有用法

shouldComponentUpdate(nextProps, nextState)

组件接收到新的props或者state时调用,return true就会更新dom(使用diff算法更新),return false能阻止更新(不调用render)

render()

react最重要的步骤,创建虚拟dom,进行diff算法,更新dom树都在此进行

getSnapshotBeforeUpdate(prevProps, prevState)

触发时间: update发生的时候,在render之后,在组件dom渲染之前;返回一个值,作为componentDidUpdate的第三个参数;配合componentDidUpdate, 可以覆盖componentWillUpdate的所有用法

componentDidUpdate()

组件加载时不调用,组件更新完成后调用
Unmounting(卸载阶段:涉及1个钩子函数)
组件渲染之后调用,只调用一次
Error Handling(错误处理)

componentDidCatch(error,info)

任何一处的javascript报错会触发
组件的基本写法
import React, { Component } from 'react'

export default class NewReactComponent extends Component {
    constructor(props) {
        super(props)
        // getDefaultProps:接收初始props
        // getInitialState:初始化state
    }
    state = {

    }
    static getDerivedStateFromProps(props, state) { // 组件每次被rerender的时候,包括在组件构建之后(虚拟dom之后,实际dom挂载之前),每次获取新的props或state之后;;每次接收新的props之后都会返回一个对象作为新的state,返回null则说明不需要更新state
        return state
    }
    componentDidCatch(error, info) { // 获取到javascript错误

    }
    render() {
        return (
            <h2>New React.Component</h2>
        )
    }
    componentDidMount() { // 挂载后
        
    }   
    shouldComponentUpdate(nextProps, nextState) { // 组件Props或者state改变时触发,true:更新,false:不更新
        return true
    }
    getSnapshotBeforeUpdate(prevProps, prevState) { // 组件更新前触发

    }
    componentDidUpdate() { // 组件更新后触发

    }
    componentWillUnmount() { // 组件卸载时触发

    }
}

4.4、总结

旧的生命周期
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5C8PX7hH-1626080394699)(https://segmentfault.com/img/bVbhRvE?w=720&h=333)]
新的生命周期
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yVgSVuDI-1626080394706)(https://segmentfault.com/img/bVbhRvx?w=720&h=394)]

  1. React16新的生命周期弃用了componentWillMount、componentWillReceivePorps,componentWillUpdate
  2. 新增了getDerivedStateFromProps、getSnapshotBeforeUpdate来代替弃用的三个钩子函数(componentWillMount、componentWillReceivePorps,componentWillUpdate)
  3. React16并没有删除这三个钩子函数,但是不能和新增的钩子函数(getDerivedStateFromProps、getSnapshotBeforeUpdate)混用,React17将会删除componentWillMount、componentWillReceivePorps,componentWillUpdate
  4. 新增了对错误的处理(componentDidCatch)

5、父子组件传值

5.1.1、父组件传值子组件

在引用子组件的时候传递,相当于一个属性,例如:在子组件内通过porps.param获取到这个param的值。

父组件向子组件传值,通过props,将父组件的state传递给了子组件。

父组件代码片段:

constructor(props){
    super(props)
    this.state={
      message:"i am from parent"
    }
  }
  render(){
    return(
          <Child txt={this.state.message}/>
    )
  }
}

子组件代码片段:

render(){
    return(
          <p>{this.props.txt}</p>
    )
}

完整示例

创建父组件 index.js

import React from 'react';
import ReactDOM from 'react-dom';
import User from './User';//引入子组件

//定义数据
const person = {
    name: 'Tom',
    age:20
}

ReactDOM.render(
    //渲染子组件,并向子组件传递name,age属性
    <User name={person.name} age={person.age}></User>
    , document.getElementById('root'));

创建子组件 User.js

import React from 'react';

class User extends React.Component{
    render(){
        return (
            // 使用props属性接收父组件传递过来的参数
            <div>{this.props.name}{this.props.age}</div>
        );
    }
}

export default User;

在父组件中可以使用展开运算符 ... 传递对象

index.js文件

ReactDOM.render(
    //渲染子组件,并向子组件传递name,age属性
    <User {...person}></User>
    , document.getElementById('root'));

User.js文件

render(){
   return (
       // 使用props属性接收父组件传递过来的参数
       <div>{this.props.name}{this.props.age}</div>
   );
}

5.1.2、子组件传值父组件

子组件通过调用父组件传递到子组件的方法向父组件传递消息的。

完整案例

子组件 Son.js 文件代码示例:

import React from 'react';

class Son extends React.Component {
    //构造方法
    constructor(){
        super();
        this.state = {
            inputValue:''
        }
    }
    //按钮点击事件
    handleClick(){
        //通过props属性获取父组件的getdata方法,并将this.state值传递过去
        this.props.getdata(this.state.inputValue);
    }
    //输入框事件,用于为this.state赋值
    handleChange(e){
        this.setState({
            inputValue: e.target.value
        });
    }

    render(){
        return (
            <React.Fragment>
                <input onChange={this.handleChange.bind(this)}></input>
                <button onClick={this.handleClick.bind(this)}>点击获取数据</button>
            </React.Fragment>
        );
    }

}

export default Son;

父组件 Parent.js 文件代码示例:

import React from 'react';
import Son from './Son';

class Parent extends React.Component {
    //构造方法
    constructor(){
        super();
        this.state = {
            mess: '' //初始化mess属性
        }
    }
    //用于接收子组件的传值方法,参数为子组件传递过来的值
    getDatas(msg){
        //把子组件传递过来的值赋给this.state中的属性
        this.setState({
            mess: msg
        });
    }

    render(){
        return (
            <React.Fragment>
                {/* 渲染子组件,设置子组件访问的方法,
                getdata属性名为子组件中调用的父组件方法名 */}
                <Son getdata={this.getDatas.bind(this)}></Son>
                <div>展示数据:{this.state.mess}</div>
            </React.Fragment>
        );
    }

}

export default Parent;

入口文件 index.js示例代码:

import React from 'react';
import ReactDOM from 'react-dom';
import Parent from './Parent';

ReactDOM.render(<Parent></Parent>, document.getElementById('root'));

5.1.3、兄弟组件传值

兄弟组件之间的传值,是通过父组件做的中转 ,流程为:

组件A传值 --> 父组件传值 --> 组件B

代码示例:

创建 Acls.js 组件,用于提供数据

import React from 'react';

class Acls extends React.Component {
	//按钮点击事件,向父组件Pcls.js传值
    handleClick(){
        this.props.data("hello...React...");
    }

    render(){
        return (
            <button onClick={this.handleClick.bind(this)}>Acls组件中获取数据</button>
        );
    }
}

export default Acls;

创建父组件 Pcls.js 用于中转数据

import React from 'react';
import Acls from './Acls';
import Bcls from './Bcls';

class Pcls extends React.Component {
	//构造函数
    constructor(){
        super();
        this.state = {
            mess: ''
        }
    }
	//向子组件Acls.js提供的传值方法,参数为获取的子组件传过来的值
    getDatas(data){
        this.setState({
            mess: data
        });
    }

    render(){
        return (
            <React.Fragment>
                Pcls组件中显示按钮并传值:
                <Acls data={this.getDatas.bind(this)}></Acls>
                <Bcls mess={this.state.mess}></Bcls>
            </React.Fragment>
        );
    }
}

export default Pcls;

创建子组件 Bcls.js 用于展示从 Acls.js 组件中生成的数据

import React from 'react';

class Bcls extends React.Component {

    render(){
        return (
            <div>在Bcls组件中展示数据:{this.props.mess}</div>
        );
    }
}

export default Bcls;

6、TodoList 案例

6.1.创建 Todolist.js 组件

import React from 'react';

class TodoList extends React.Component{

  //构造方法
  constructor(props){
    super(props);
    this.state = {
      list: [
        'learn html',
        'learn css',
        'learn react',
        'learn vue'
      ]
    }
  }

  //按钮点击事件方法
  handleBtnClick(){
    this.setState({
      list: [...this.state.list,'hello world'] //...为展开运算符,将this.state.list内容放到当前的list中
    });
  }

  render(){
    return (
      <div>
        <div>
          <input type="text"></input>
          <button onClick={this.handleBtnClick.bind(this)}>添加</button>
        </div>
        <ul>
          {/* key属性为唯一值,没有该属性,浏览器会报警告信息,在做添加操作时会出bug */}
          {this.state.list.map((item,index) => <li key={index}>{item}</li>)}
        </ul>

      </div>
    );
  }
}

export default TodoList;

引用组件

import React from 'react';
import ReactDOM from 'react-dom';
import TodoList from './TodoList';

ReactDOM.render(<TodoList />, document.getElementById('root'));

6.2.添加列表项功能

代码示例:

import React from 'react';

class TodoList extends React.Component{

  //构造方法
  constructor(props){
    super(props);
    this.state = {
      list: [],  //展示列表
      inputValue:'' //记录输入框的值
    }
  }

  按钮点击事件方法
  handleBtnClick(){
    this.setState({
      list: [...this.state.list,this.state.inputValue], //...为展开运算符,将this.state.list内容放到当前的list中
      inputValue: '' //点击添加按钮,清空输入框
    });
  }

  //输入框输入事件方法
  handleInputChange(e){
    this.setState({
      inputValue: e.target.value
    });
  }

  render(){
    return (
      <div>
        <div>
          <input value={this.state.inputValue} onChange={this.handleInputChange.bind(this)}></input>
          <button onClick={this.handleBtnClick.bind(this)}>添加</button>
        </div>
        <ul>
          {this.state.list.map((item,index) => <li key={index}>{item}</li>)}
        </ul>

      </div>
    );
  }
}

export default TodoList;

6.3.删除列表元素

删除列表元素代码:

import React from 'react';

class TodoList extends React.Component{

  //构造方法
  constructor(props){
    super(props);
    this.state = {
      list: [],  //展示列表
      inputValue:'' //记录输入框的值
    }
  }

  按钮点击事件方法
  handleBtnClick(){
    this.setState({
      list: [...this.state.list,this.state.inputValue], //...为展开运算符,将this.state.list内容放到当前的list中
      inputValue: '' //点击添加按钮,清空输入框
    });
  }

  //输入框输入事件方法
  handleInputChange(e){
    this.setState({
      inputValue: e.target.value
    });
  }

  //点击展示列表事件方法,用于删除展示元素
  handleItemClick(index){
    const list = [...this.state.list];
    list.splice(index,1);
    this.setState({
      list: list
    });
  }

  render(){
    return (
      <div>
        <div>
          <input value={this.state.inputValue} onChange={this.handleInputChange.bind(this)}></input>
          <button onClick={this.handleBtnClick.bind(this)}>添加</button>
        </div>
        <ul>
          {this.state.list.map((item,index) => {
            return <li onClick={this.handleItemClick.bind(this,index)} key={index}>
                    {item}
                  </li>
          })}
        </ul>

      </div>
    );
  }
}

export default TodoList;

6.4.使用组件化实现删除功能

创建子组件 TodoItem.js

import React from 'react';

class TodoItem extends React.Component{

    //点击元素删除的方法
    //子组件如果想和父组件通信,子组件要调用父组件传递过来的方法
    handleDelete(){
        //调用父组件的方法,向父组件传值
        this.props.delete(this.props.index);
    }

    render(){
        return (
            <div onClick={this.handleDelete.bind(this)}>
                {/* 子组件通过props接收父组件传递过来的参数 */}
                {this.props.content}
            </div>
        );
    }
}

export default TodoItem;

在父组件 TodoList.js 中调用子组件

import React from 'react';
import TodoItem from './TodoItem';

class TodoList extends React.Component{

  //构造方法
  constructor(props){
    super(props);
    this.state = {
      list: [],  //展示列表
      inputValue:'' //记录输入框的值
    }
  }

  按钮点击事件方法
  handleBtnClick(){
    this.setState({
      list: [...this.state.list,this.state.inputValue], //...为展开运算符,将this.state.list内容放到当前的list中
      inputValue: '' //点击添加按钮,清空输入框
    });
  }

  //输入框输入事件方法
  handleInputChange(e){
    this.setState({
      inputValue: e.target.value
    });
  }

  //点击元素删除的方法,该方法是用来接收子组件的传值
  handelDeleteItem(index){
    const list = [...this.state.list];
    list.splice(index,1);
    this.setState({
      list: list
    });
  }

  render(){
    return (
      <div>
        <div>
          <input value={this.state.inputValue} onChange={this.handleInputChange.bind(this)}></input>
          <button onClick={this.handleBtnClick.bind(this)}>添加</button>
        </div>
        <ul>
          {this.state.list.map((item,index) => {
            //父组件通过属性的形式向子组件传递参数
            return <TodoItem 
            			delete={this.handelDeleteItem.bind(this)} 
            			key={index} 
            			content={item} 
            			index={index}>
            		</TodoItem>
          })}
        </ul>

      </div>
    );
  }
}

export default TodoList;

6.5.代码优化

TodoItem.js 代码优化:

import React from 'react';

class TodoItem extends React.Component{

    //构造方法
    constructor(props){
        super(props);
        this.handleDelete = this.handleDelete.bind(this);
    }

    //点击元素删除的方法
    //子组件如果想和父组件通信,子组件要调用父组件传递过来的方法
    handleDelete(){
        const { handelDelete , index } = this.props;
        //调用父组件的方法,向父组件传值
        handelDelete(index);
    }

    render(){
        const { content } = this.props;
        return (
            <div onClick={this.handleDelete}>{content}</div>
        );
    }
}

export default TodoItem;

TodoList.js 代码优化:

import React from 'react';
import TodoItem from './TodoItem';

class TodoList extends React.Component{

  //构造方法
  constructor(props){
    super(props);
    this.state = {
      list: [],  //展示列表
      inputValue:'' //记录输入框的值
    }
    
    this.handleInputChange = this.handleInputChange.bind(this);
    this.handleBtnClick = this.handleBtnClick.bind(this);
    this.handelDeleteItem = this.handelDeleteItem.bind(this);
  }

  按钮点击事件方法
  handleBtnClick(){
    this.setState({
      list: [...this.state.list,this.state.inputValue], //...为展开运算符,将this.state.list内容放到当前的list中
      inputValue: '' //点击添加按钮,清空输入框
    });
  }

  //输入框输入事件方法
  handleInputChange(e){
    this.setState({
      inputValue: e.target.value
    });
  }

  //点击元素删除的方法,该方法是用来接收子组件的传值
  handelDeleteItem(index){
    const list = [...this.state.list];
    list.splice(index,1);
    this.setState({
      list: list
    });
  }

  //遍历方法
  getTodoItems(){
    return (
      this.state.list.map((item,index) => {
        //父组件通过属性的形式向子组件传递参数
        return <TodoItem 
                  handelDelete={this.handelDeleteItem} 
                  key={index} 
                  content={item} 
                  index={index} />
      })
    );
  }

  render(){
    return (
      <div>
        <div>
          <input value={this.state.inputValue} onChange={this.handleInputChange}></input>
          <button onClick={this.handleBtnClick}>添加</button>
        </div>
        <ul>
          {this.getTodoItems()}
        </ul>

      </div>
    );
  }
}

export default TodoList;

6.6.使用CSS样式修饰

方法一:使用style属性

代码示例:

<button style={{background:'blue',color:'#fff'}} onClick={this.handleBtnClick}>
	添加
</button>

方法二:使用className属性

创建 style.css 文件

.red-btn{
    background-color: red;
    color: #ffffff;
}

index.js 入口文件引入 css

import './style.css';

在组件中使用 className 属性

 <button className='red-btn' onClick={this.handleBtnClick}>添加</button>

6.7.JSX代码优化

render() 方法 returnJSX 代码需要在最外层使用一个标签包裹,如果不想在页面中显示最外层的这个标签,可以使用 <React.Fragment> 标签替代,代码示例:

render(){
    return (
      <React.Fragment>
        <div>
          <input />
          <button>添加</button>
        </div>
        <ul>
          {this.getTodoItems()}
        </ul>
      </React.Fragment>
    );
  }

也可以在引入组件时直接引入类,在使用时就不需要用 React 调用

引入

import React,{Component,Fragment} from 'react';
class TodoList extends Component{
render(){
    return (
      <Fragment>
        <div>
          <input />
          <button>添加</button>
        </div>
        <ul>
          {this.getTodoItems()}
        </ul>
      </Fragment>
    );
  }
}
export default TodoList;

React路由

1、React路由介绍

1.1、单页面应用

单页面得特点:只需要加载一次主页面,通过局部刷新,就可以实现跳转或者切换页面

优点:加载速度快,用户体验比较好

缺点:

  • 第一次加载比传统要慢一点
  • 不利seo
  • 页面相对复杂
  • 返回键

1.2、安装react-router-dom

在项目命令行中,执行

cnpm install react-router-dom -S

下载到生产环境的依赖中。

在组件中通过对象的解构方式去获取到react-router-dom内置组件,在组件中,按需引入内置组件,在页面中进行使用:

  • HashRouter表示一个路由的根容器,将来所有的路由相关的东西,都要包裹在HashRouter里面,而且一个网站中,只需要使用一次HashRouter就好了;
  • Route表示一个路由规则,在Route上,有两个比较重要的属性,path,component
  • Link表示一个路由的链接,属性to
import {HashRouter,Route,Link} from 'react-router-dom'

代码示例:

render(){
        return (
            <HashRouter>
                <div>
                    <h1>这是网站的根目录</h1>
                    <hr />
                    <Link to="/home">首页</Link>&nbsp;&nbsp;
                    <Link to="/movie/">电影</Link>&nbsp;&nbsp;
                    <Link to="/about">关于</Link>
                    <hr />
                    <Route path="/home" component={Home} ></Route><hr/>
                    <Route path="/movie" component={Movie} exact></Route><hr/>
                    <Route path="/about" component={About}></Route><hr/>
                </div>
            </HashRouter>
        );
    }

当使用HashRouter把APP根组件的元素包裹起来之后,网站就已经启用路由了,在一个HashRouter中,只能有唯一的一个根元素。 在一个网站中,只需要使用唯一的一次<HashRouter></HashRouter>就行了。

Route创建的标签,就是路由规则,其中path表示要匹配的路由,component表示要展示的组件。Route具有两种身份:1.它是一个路由匹配规则;2.它是一个占位符,表示将来匹配到的组件都放到这个位置

需要注意的地方

  • Route 组件path地址是以/开头 ,配置component属性,是显示的组件,这个属性不可以大写
  • Route组件可以单双标签使用,单标签需要/结尾,双标签不可以在中间写入别的东西
  • Link to属性的地址也是/开头,Link在页面渲染的是a标签

2、路由传值

2.1、React-router-dom路由传值

通过配置路由的地址,在Link跳转时

  • Route path路径后面 /:id (key)

  • Link to 路径后面 /top/10 (value)

    接收传值:

  • class类组件,this.props.match.params.属性名

  • 函数组件:形参.match.params.属性名

代码示例:

render(){
        return (
            <HashRouter>
                <div>
                    <h1>这是网站的根目录</h1>
                    <hr />
                    <Link to="/movie/top/10">电影</Link>&nbsp;&nbsp;
                    <hr />
                    <Route path="/movie/:type/:id" component={Movie} exact></Route>
                </div>
            </HashRouter>
        );
    }

在Route内置组件中,配置path地址:

<Route path="/movie/:type/:id" component={Movie} exact></Route>

在Link内置组件中,配置to属性,进行跳转:

<Link to="/movie/top/10">电影</Link>

类组件中通过生命周期进行接收,this.props携带路由传递过来的数据:

render(){
        console.log(this);
        return (
            <div>
                电影--{this.props.match.params.type}--{this.props.match.params.id}
            </div>
        );
    }

代码优化后:

class Movie extends React.Component{

    constructor(props){
        super();
        this.state = {
            routeParams:props.match.params
        }
    }

    render(){
        console.log(this);
        return (
            <div>
                电影--{this.state.routeParams.type}--{this.state.routeParams.id}
            </div>
        );
    }
}

函数组件中通过形参接收传递过来的值,props形参,函数组件作为路由组件,props就是传递过来的对象,里面携带着路由传递过来的数据

import React from 'react'

export default function home(props) {
	return (
		<div>
			{props.match.params.id}
		</div>
	)
}

2.2、嵌套路由

嵌套路由:在路由组件中,使用Link, Route,配置子路由,实现跳转,切换;

下面为一级路由,在一级路由Home为路由组件

<Route path="/home" component={ Home }></Route>

在Home组件中继续使用Link,Route进行路由的嵌套,需要注意的就是路由地址,前部分为一级路由地址,后面接一个二级路由相应的路径

render() {
    return (
        <div>
            <ul>
            	<li><Link to="/home/a">推荐</Link></li>
            	<li><Link to="/home/b">新时代</Link></li>
                <li><Link to="/home/c">动漫</Link></li>
            </ul>
            <Route path="/home/a" component={ A }></Route>
            <Route path="/home/b" component={ B }></Route>
            <Route path="/home/c" component={ C }></Route>
        </div>
    )
}

2.3、JS实现路由跳转

引入BrowserRouter模块

import {BrowserRouter,HashRouter,Route,Link} from 'react-router-dom'

使用BrowserRouter作为根容器

//跳转点击事件
jump(){
    window.location.href = "/news"
}

render(){
    return (
        <BrowserRouter>
            <div>
                <h1>这是网站的根目录</h1>
                <hr />
                <button onClick={()=>{this.jump()}}>新闻</button>
                <hr />
                <Route path="/news" component={News}></Route>
            </div>
        </BrowserRouter>
    );
}

在render方法中,写一个按钮,按钮名称为js跳转路由,定义一个onClick方法,箭头函数解决this指向问题,与render同级,定义一个jump方法,在jump方法中执行一句代码进行路由跳转,使用window.location.href = “路由的地址”实现路由跳转。

3、react-router-dom内置组件

首先按需引入,使用什么内置组件,就需要引入

import { BrowserRouter, Link, Route,Redirect,NavLink,Switch } from 'react-router-dom'

3.1、在组件中使用NavLink

NavLink 带有选中activeClassName ,如果路由处于激活状态,显示激活class样式。

在我们之前案例的基础上,找到Link组件,我们已经学到Link组件的作用,可以进行路由的跳转,通过to属性,跳转相应的path地址。

<ul>
    <li>
    	<Link to="/home">首页</Link>
    </li>
    <li>
    	<Link to="/video">好看视频</Link>
    </li>
</ul>

将组件中的Link全部换成NavLink组件

<li>
	<NavLink activeClassName="red" to="/home">首页</NavLink>
</li>
<li>
	<NavLink activeClassName="red" to="/video">好看视频</NavLink>
</li>

我们会发现,之前可以正常进行路由跳转,换成NavLink,还依然可以正常跳转,证明组件得跳转使用NavLink也可以实现,那么问题来了,NavLink有什么用,为什么封装了NavLink,将每一个NavLink加入一个activeClassName属性绑定一个class类样式,这时在触发NavLink时,会触发相应得样式,这样有一个切换效果。

3.2、在组件中使用Redirect内置组件

Redirect 重定向 具备to属性,可以直接跳转到指定路由。

在render方法中,使用内置组件,Redirect内置组件使用to属性,当执行到内置标签时,会进行to跳转路由,to后面接的地址是什么,就可以匹配到相应得路由组件。

<Redirect to="/home/c"></Redirect>

3.3、在组件使用Switch内置组件

Switch 具有排他性

在组件中使用Switch标签包裹所有得Route,这时,Route相当于每一个path都是精准的匹配,展示相应的组件,最后一个Route是一个不具备地址的路由路径,如果当Link指向一个不存在的地址时,没有精准的匹配到地址,那么会显示到C404路由,使用Switch只会显示一个组件,匹配到为止。

<Switch>
    <Route path="/home" component={ Home }></Route>
    <Route path="/video" component={ Home1 }></Route>
    <Route path="/book" component={ Home2 }></Route>
    <Route path="/renwu" component={ Home3 }></Route>
    <Route path="/user" component={ Home4 }></Route>
    <Route component={ C404 }></Route>
</Switch>

Redux

1、什么是Redux

Redux 是React生态中重要的组成部分。很多人都说,简单的应用可以不用此工具。但是我个人认为,中小型应用使用的话,可以使文件结构更加规范,代码可读性更强。因为React提出将展示组件与容器组件分离的思想,所以降低了React 与Redux之间的耦合度。

img

Redux是一个流行的JavaScript框架,为应用程序提供一个可预测的状态容器。Redux基于简化版本的Flux框架,Flux是Facebook开发的一个框架。在标准的MVC框架中,数据可以在UI组件和存储之间双向流动,而Redux严格限制了数据只能在一个方向上流动。 见下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XDmWiwUE-1626080394748)(https://segmentfault.com/img/bVWi0h?w=687&h=330)]

在Redux中,所有的数据(比如state)被保存在一个被称为store的容器中 → 在一个应用程序中只能有一个。store本质上是一个状态树,保存了所有对象的状态。任何UI组件都可以直接从store访问特定对象的状态。要通过本地或远程组件更改状态,需要分发一个action分发在这里意味着将可执行信息发送到store。当一个store接收到一个action,它将把这个action代理给相关的reducerreducer是一个纯函数,它可以查看之前的状态,执行一个action并且返回一个新的状态。

Redux的运行原理

img

store中重要的方法

img

2、配置Redux

配置Redux开发环境的最快方法是使用create-react-app工具。在开始之前,确保已经安装并更新了nodejsnpm

npm install redux

创建store目录,在store目录下新建index.js文件,键入以下内容:

import {createStore} from 'redux';
import reducer from './reducer';

const store = createStore(reducer);

export default store;

在store目录下创建reducer.js文件,键入以下内容:

const defaultState = {
    inputValue:'',
    list:[]
}

//reducer可以接收state,但是不能修改state
export default (state = defaultState,action) => {
    return state;
}

在组件中就可以使用store的数据

import React,{Component} from 'react';
import store from './store/index';

class TodoList extends Component{

    constructor(props){
        super(props);
        this.state = store.getState();
        this.handleStoreChange = this.handleStoreChange.bind(this);
        store.subscribe(this.handleStoreChange);
    }
    
    handleStoreChange(){
        this.setState(store.getState());
    }

    render(){
        return (
            <div>
                <input type='text' value={this.state.inputValue}/>
                <button onClick={this.handleClick}>提交</button>
                <ul>
                    {this.state.list.map((item,index)=>{
                        return (
                            <li key={index}>{item}</li>
                        );
                    })}
                </ul>
            </div>
        );
    }
}

export default TodoList;

3、TodoList案例

TodoList.js

import React,{Component} from 'react';
import store from './store/index';
import {DELETE_TODO_ITEM,CHANGE_INPUT_VALUE,ADD_TODO_ITEM} from './store/actionType'

class TodoList extends Component{

    constructor(props){
        super(props);
        this.state = store.getState();
        this.handleChange = this.handleChange.bind(this);
        this.handleStoreChange = this.handleStoreChange.bind(this);
        this.handleClick = this.handleClick.bind(this);
        store.subscribe(this.handleStoreChange);

    }

    handleChange(e){
        const action = {
            type:CHANGE_INPUT_VALUE,
            value:e.target.value
        }
        store.dispatch(action);
    }

    handleStoreChange(){
        this.setState(store.getState());
    }

    handleClick(){
        const action = {
            type:ADD_TODO_ITEM
        }
        store.dispatch(action);
    }

    handleClickItem(index){
        const action = {
            type:DELETE_TODO_ITEM,
            index:index
        }
        store.dispatch(action);
    }

    render(){
        return (
            <div>
                <input type='text' value={this.state.inputValue} onChange={this.handleChange} />
                <button onClick={this.handleClick}>提交</button>
                <ul>
                    {this.state.list.map((item,index)=>{
                        return (
                            <li key={index} onClick={this.handleClickItem.bind(this,index)}>{item}</li>
                        );
                    })}
                </ul>
            </div>
        );
    }
}

export default TodoList;

store/index.js

import {createStore} from 'redux';
import reducer from './reducer';

const store = createStore(reducer);

export default store;

store/reducer.js

import {DELETE_TODO_ITEM,CHANGE_INPUT_VALUE,ADD_TODO_ITEM} from './actionType'

const defaultState = {
    inputValue:'',
    list:[]
}

//reducer可以接收state,但是不能修改state
export default (state = defaultState,action) => {
    console.log(state,action);
    if(action.type === CHANGE_INPUT_VALUE){
        const newState = state;
        newState.inputValue = action.value;
        return newState;
    }
    if(action.type === ADD_TODO_ITEM){
        const newState = state;
        newState.list.push(newState.inputValue);
        newState.inputValue = '';
        return newState;
    }
    if(action.type === DELETE_TODO_ITEM){
        const newState = state;
        newState.list.splice(action.index,1);
        return newState;
    }
    
    return state;
}

store/actionType.js

export const CHANGE_INPUT_VALUE = 'change_input_value'
export const ADD_TODO_ITEM = 'add_todo_item'
export const DELETE_TODO_ITEM = 'delete_todo_item'

**参考文章: https://segmentfault.com/a/1190000011474522 **

核心API:

  • createStore 创建store
  • store.dispatch 派发action,把数据上传到Store中
  • store.getState 获取store中所有的数据内容,但是Store中的数据发生变化时,组件不会知道
  • store.subscribe 监听Store中的数据变化,Store中的数据一旦发生变化,该方法就会被执行

4、Redux-thunk中间件

第一步 安装redux-thunk中间件

npm install redux-thunk

第二步 在store中引入thunk组件

import {createStore,applyMiddleware } from 'redux';
import Reducer from './Reducer';
import thunk from 'redux-thunk';

const store = createStore(Reducer,applyMiddleware(thunk));

export default store;

第三步 封装异步请求方法

在TodoList.js组件中,封装异步获取请求的方法:

import React, { Component } from 'react'
import Store from './Store'
import axios from 'axios'

export class TodoList extends Component {

    constructor(props){
        super(props);
        this.state = Store.getState();
        this.handleStoreChange = this.handleStoreChange.bind(this);
        Store.subscribe(this.handleStoreChange);
    }

    //在生命周期函数中调用异步方法
    componentDidMount(){
        Store.dispatch(this.getTodoListDatas());
    }

    //异步获取请求的方法
    getTodoListDatas(){
        return (dispatch)=>{
            axios.get("/data.json")
            .then(resp => {
                const action = {
                    type:'axios_getdata',
                    data:resp.data
                }
                dispatch(action)
            })
        }
    }

    handleStoreChange(){
        this.setState(Store.getState());
    }

    render() {
        return (
            <div>
                <input type='text' value={this.state.inputValue}></input>
                <button>添加</button>
                <hr></hr>
                <ul>
                    {this.state.list.map((item,index)=>{
                        return (
                            <li key={index}>{item}</li>
                        );
                    })}
                </ul>
            </div>
        )
    }
}

export default TodoList


第四步 在reducer中接收action信息

const defaultState = {
    inputValue:'',
    list:[]
}

export default (state = defaultState,action) => {
    if(action.type === 'axios_getdata'){
        const newState = state;
        newState.list = action.data;
        return newState;
    }

    return state
}

5、Redux中间件执行原理

redux-thunk这个中间件可以使我们把这样的异步请求或者说复杂的逻辑可以放到action里面去处理,redux-thunk使redux的一个中间件,为什么叫做中间件

我们说中间件,那么肯定是谁和谁的中间,那么redux的中间件指的是谁和谁的中间呢?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3VkAE87P-1626080394760)(C:\Users\mac\AppData\Roaming\Typora\typora-user-images\1581401374679.png)]

如图。view在redux中会派发一个action,action通过store的dispatch方法派发给store,store接收到action,连同之前到state,一起传给reducer,reducer返回一个新到数据给store,store去改变自己到state。这是redux的一个标准流程,那么我们说redux的中间件指的是谁和谁的之间,大家一定要记住,**redux的中间件指的是action和store之间。之前我们说action只能是一个对象,所以action是一个对象直接派发给了store。**但是现在,当我们使用了redux-thunk之后,action可以是函数了。

为什么可以是函数呢,看这张图。action通过dispatch方法被传递给store,那么action和store之间是谁,是不是就是dispatch这个方法,**实际上我们指的中间件指的是什么呢,就是对dispatch方法的一个封装,或者说是dispatch方法的一个升级,最原始的dispatch方法,他接收到一个对象之后,会把这个对象传递给store,这就是view中间件的一个情况。**当我们对dispath做了一个升级之后,比如说我们使用了redux-thunk这个中间件,对dispath做了一个升级,这个时候当你调用dispatch方法,给dispatch传递的参数是一个对象的话,那么这个dispatch就会把这个对象直接传给store。跟之前写一个对象,调用dispatch传给store没有任何的区别。但是这个时候假设传给dispatch方法是一个函数的话,这个时候dispatch方法已经升级了。他知道如果你传递过来是一个函数的话,他就不会把这个函数直接传递给store。他会怎么办呢?

他会让你这个函数先执行,然后执行完了之后,需要调用store的时候,这个函数再去调用store。所以dispatch做了一个事情,他会根据参数的不同,执行不同的事情,如果你参数是对象,那我直接传给store。如果你参数是函数,那就把这个函数执行结束。所以讲到这大家应该知道

**redux中间件,他的原理是非常简单的,他就是对store对dispatch方法做一个升级,之前这个dispatch方法只能接收一个对象,现在升级之后,就可以接收对象,也可以接收函数了。**当然这里用什么对他进行一个升级呢?用redux-thunk对他进行了升级。当然中间件不只redux-thunk这一个,实际上redux中间件非常多,比如说我们说的redux-log,可以记录action每次派发的日志,那他怎么记录呢?其实也很简单,每次调用 action的时候,都会通过dispatch把这个action传递给store,那么我可以对dispatch做一个升级,dispatch不仅仅把action传递给store,而且在每次传递之前呢,还通过console.log,把这个action打印出来。这样就写了一个redux-log的中间件。他可以在每次派发action的时候,把这个action打印在控制台里面。

最近用的比较火的redux中间件,除了redux-thunk,redux-log这样的东西,还有一个中间件,叫做redux-saga。他的应用范围也非常广,redux-saga也是解决redux中间异步问题的中间件。不同于redux-thunk。redux-thunk是把异步操作放在action里面操作。而redux-saga采用的设计思想是,单独的把一个异步逻辑拆分出来,放在一个异步文件里面管理,基本上掌握了redux-thunk和redux-saga这两者的设计思路之后呢,再去做redux里面的异步逻辑,或者说复杂的逻辑,如何去拆分,就比较明白了。

6、react-redux

第一步 安装react-redux

npm install react-redux

第二步 创建store和reducer

store.js文件

import {createStore } from 'redux';
import reducer from './reducer';

const store = createStore(reducer);

export default store;

reducer.js文件

const defaultState = {
    inputValue:'hello',
    list:['a','b','c']
}
export default (state = defaultState,action) => {
    return state
}

第三步 在index.js入口文件引入Provider组件

import React from 'react';
import ReactDOM from 'react-dom';
import TodoList from './reactredux/TodoList'
import {Provider} from 'react-redux'
import store from './reactredux/store'

const APP = (
    <Provider store={store}>
        <TodoList></TodoList>
    </Provider>
);

ReactDOM.render(APP , document.getElementById('root'));

第四步 在TodoList.js组件中引入connect组件

import React, { Component } from 'react'
import {connect} from 'react-redux'

export class TodoList extends Component {

    render() {
        return (
            <div>
                <input type='text' value={this.props.inputValue} onChange={this.props.changeInputValue}></input>
                <button onClick={this.props.addClick.bind(this)}>添加</button>
                <hr></hr>
                <ul>
                    {this.props.list.map((item,index)=>{
                        return (
                            <li key={index}>{item}</li>
                        );
                    })}
                </ul>
            </div>
        )
    }
}

const mapStateToProps = (state) =>{
    return {
        inputValue: state.inputValue,
        list:state.list
    }
}

const mapDispatchToProps = (dispatch) =>{
    return {
        changeInputValue(e){
            const action = {
                type:"change_inputvalue",
                inputValue:e.target.value
            }
            dispatch(action);
        },
        addClick(){
            const action = {
                type:"add_list",
                value:this.props.inputValue
            }
            dispatch(action);
        }
    }
}

export default connect(mapStateToProps,mapDispatchToProps)(TodoList);


第五步 在reducer中接收action的值

const defaultState = {
    inputValue:'hello',
    list:['a','b','c']
}
export default (state = defaultState,action) => {
    if(action.type === 'change_inputvalue'){
        const newState = JSON.parse(JSON.stringify(state));
        newState.inputValue = action.inputValue;
        return newState;
    }

    if(action.type === 'add_list'){
        const newState = JSON.parse(JSON.stringify(state));
        newState.list.push(action.value);
        newState.inputValue = "";
        return newState;
    }
    
    return state
}

React-UI组件库

1、常用的React UI组件

1.1、React Material-UI介绍

React 组件用于更快速、更简便的 web 开发。你也可以建立你自己的设计系统,或者从 Material Design 开始。

Material-UI 是一个使用 MIT 许可证的开源项目。 可能完全因为这些超棒的 backers,这个项目不断的在开发

1.2、React-Bootstrap介绍

React-Bootstrap是可重用的前端组件库。 样式组件依赖于bootstrap。与 Twitter Bootstrap 一致外观与感受,但通过 Facebook 的 React.js 框架获得更清爽的代码

1.3、ant-design介绍

Ant Design 蚂蚁框架 (蚂蚁设计)

antd 是基于 Ant Design 设计体系的 React UI 组件库,主要用于研发企业级中后台产品

特点:开箱即用的高质量 React 组件。

2、ant-design安装以及按需使用

2.1、ant-design安装

  1. 安装依赖 cnpm install antd -S
  2. 按需引入 import { Button } from ‘antd’,可以使用Button组件
  3. 引入样式 import ‘antd/dist/antd.css’
  4. 在组件render方法中,使用Button
<Button size="small" disabled href="#123" onClick={()=>{this.click()}} type="danger">button</Button>

根据api加入相应的功能,比如点击事件,size大小,href当成a链接使用,通过type类型调节颜色。

2.2、Antd-design 使用图标icon

import { Button,Icon } from 'antd'
<Icon spin twoToneColor="blue" style={{fontSize:"30px",color:"red"}} type="aliwangwang" />

2.3、Tag标签的引入

第一步引入组件样式,以及解构Tag标签

import { Tag } from 'antd';
import 'antd/dist/antd.css'

通过使用的API来认识tag标签的使用方式。

2.4、自动生成不同颜色标签案例

通过api写一个小案例,定义6种颜色,在页面上有一个开始按钮,点击开始按钮时,页面随机生成Tag标签,标签的颜色也是对应的Tag,生成在constructor中定义一个状态,里面写几个随机颜色,定义一个空数组colorarr。

<Button onClick={()=>{this.click()}}>开始</Button>
{
    this.state.colorarr.map((item)=>{
        return (
            <Tag color={item}>{item}</Tag>
        )
    })
}

当触发onClick方法时触发click方法

click() {
    // 创建一个定时器
    this.time = setInterval(()=>{
        // 随机生成一个数字 0-5
        let count = Math.round(Math.random()*5)
        console.log(count)
        // 在数组中取到颜色,通过生成的数字
        let str = this.state.color[count]
        // 将取到的颜色放入colorarr 中
        this.state.colorarr.push(str)
        this.setState({colorarr:this.state.colorarr})
    },100)
}

2.5、Rate评分的基本用法

按需引入antd中的Rate组件,在render方法中,jsx渲染Rate组件

import { Rate } from ‘antd’;

ReactDOM.render(, mountNode);

State状态中定义一个value值,默认为0

constructor() {
    super()
    this.state = {
    	value:0
    }
}

在render方法中,定义一个循环加星的按钮,加一个loop方法,根据Rate评分组件api,加入自动获取交点autoFocus onKeyDown键盘事件 星星的总数,鼠标悬停触发事件,以及当前的星数

render() {
    return (
        <div>
            <h3>rate</h3>
            <button onClick={()=>{this.loop()}}>循环加星</button>
            <Rate autoFocus onKeyDown={(e)=>{this.kd(e)}} count="20" onHoverChange={(e)=>{this.change(e)}} value={this.state.value} allowHalf></Rate>
        </div>
    )
}

当鼠标悬停时,触发事件,页面的星数随着悬停在几颗星,就为几颗星

change(e) {
    console.log(e)
    this.setState({value:e})
}

当点击开始按钮时,触发loop事件,将组件加入一个周期性定时器,将星数每0.2秒中自动加一颗星,当星数为20时,星数变为0,一直循环加星

loop() {
    this.time = setInterval(()=>{
        if(this.state.value == 20){
        	this.state.value = 0
        }
        this.state.value ++
        this.setState({value:this.state.value})
    },200)
}

当触发kd方法时,为鼠标按下事件,鼠标按右键时,触发键盘码39,按下一次星数长半颗星,按下左键时,触发键盘码37,每一次按键减少半颗星

kd(e) {
    if(e.keyCode == 39){
    	this.state.value +=0.5
    }
    if(e.keyCode == 37){
    	this.state.value -=0.5
    }
    this.setState({
    	value:this.state.value
    })
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

半生过往

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值