都 2021 年了还不会连 ES6/ES2015 更新了什么都不知道吧

  • es6 / es2015
    • let & const
    • 块级作用域
    • 解构
      • 数组解构
      • 对象解构
    • 模板字符串
    • Math + Number + String + Object 扩展方法
    • 默认参数
    • 剩余 与 展开 操作符
    • 箭头函数
    • 对象字面量
    • 代理 Proxy
    • 反射 Reflect
    • 期约 Promises
      • 类的继承
        • 内置对象可被继承
      • 类的静态方法
    • Map + Set + WeakMap + WeakSet
    • 迭代器 & 生成器 & for of
      • 迭代器
      • 生成器
      • for of
    • Symbols
    • Modules
    • Unicode
    • 二进制 与 八进制字面量

JavaScript 的历史也算是非常久远了,最早能追溯回 90 年代,网景公司和微软为了能够抢占市场大打出手,分别推出了只有自家产品才能够使用的 JavaScript 版本。这就让开发们非常头疼了——写一份代码已经很难了,为了适配多端兼容还得再整一个一样,又不完全一样的版本出来。

为了解决这个问题,最终由 ECMA International(前身为欧洲计算机制造商协会)统一规范,并且将该规范命名为 ECMAScript,作为 JavaScript 的统一标准。

自此,神仙打架的日子结束了,开发们的日子也稍微好过一些了。

事件继续推进到了 2008 年后,移动端的兴起对 JavaScript 的影响是巨大的。也因此,社区中的大神对于那些功能应该被放入当时的 ES5 而产生了极大的分歧,最终导致了 ES6 的标准的限定也陷入了滞后。

从现在的角度看来,ES6 的变化真的可以说是翻天覆地的变化,也许,这样的变化对于当时的开发来说还是太早了。

时间又向后推进几年,到了 2015 年,ECMAScript6 终于作为正式版本发布,官方名称为 ECMAScript 2015,旨在更频繁的进行少量的迭代。并且,从 ECMAScript6(ES6/ES2015)之后,所有的版本迭代将会以 ECMAScript + 年份 作为正式的官方名称。

历史讲完了,下面就肩带概述一下从 ES2015-ES2021 分别都有什么新特性。

es6 / es2015

最主要的版本迭代,推出了很多对于 JavaScript 来说革命性的新特性。

let & const

在 ES6 之前,JavaScript 想要申明变量只能使用关键字 var。JavaScript 特有的作用域提升让 var 具有一些特殊的行为,如:

for (var i = 0; i < 5; i++) {}console.log(i); // 5

使用 let 和 const 可以很好地规避这个问题,上面同样的代码,以 let 为例:

for (let i = 0; i < 5; i++) {}console.log(i); // Uncaught ReferenceError: i is not defined

const 与 let 最大的区别有两点:

  • let 的值是可变的,而 const 是不可变的,如:
let b = 10; // 10
b = 20; // 20const c = 10; // 10
c = 20; // Uncaught TypeError: Assignment to constant variable.
  • let 初始化可以不用给值,const 不给值会报错
let b; // undefined
const c; // Uncaught SyntaxError: Missing initializer in const declaration

所以,目前的推荐用法是:

  1. 规避使用 var 去声明变量

更多关于作用域提升的内容,可以看之前记得笔记:var, let, const 的区别与相同

块级作用域

即 花括号{},这也是 ES6 新增特性之一,在花括号之中的内容独立作为一个作用域,外部无法访问。

在块级作用域搭配 let 和 const 可以有效地避免变量名污染的问题,如:

for (let i = 0; i < 3; i++) {for (let i = 0; i < 3; i++) {console.log(i);}
}
// 0
// 1
// 2
// 0
// 1
// 2
// 0
// 1
// 2

因为外部作用域无法访问到内部的变量,而内部的 i 会就近寻找作用域上的变量。为了方便理解,还是建议将内外循环体重的变量取不同的名字。

解构

快速提取变量的方法,可以用于数组和对象。

数组解构

数组解构与根据 index 取值有些像:

const testArr = [1, 2, 3];
const [val1, val2, val3] = testArr;
console.log(val1, val2, val3); // 1, 2, 3// 等同于
const val1 = testArr[0],val2 = testArr[1],val3 = testArr[2];
console.log(val1, val2, val3); // 1, 2, 3

在学习算法的过程中就会听经常的用到数组解构:

// 用数组解构
function swap(arr, i, j) {[arr[i], arr[j]] = [arr[j], arr[i]];
}// 不用数组解构
function swap2(arr, i, j) {const temp = arr[i];arr[i] = arr[j];arr[j] = temp;
}

在工作中,在拆解固定值也挺方便的,例如说经常会出现有些字符串使用 - 进行分割:

const [base, str2] = someStr.split('-');// 不用数组解构
const splittedStr = someStr.split('-');
const base = splittedStr[0],str2 = splittedStr[1];

对象解构

根据属性名进行解构,解构中的变量名一定是匹配类中的属性名的,使用方法如下:

const getUserAddress = (metaData) => {return {zipcode: 123456,addr1: 'some addr',addr2: 'some addr2',state: 'some state',// 其他属性...};
};const { zipcode, addr1, addr2, state } = getUserAddress('dummy data');// 或提取常用的函数出来,假设做数学运算,经常会用到一些数学常量和函数,就可以先提取出来
const { PI, abs, sin } = Math;
const { log } = console;
log(sin(PI));

对于解决变量名重复,ES6 也已经有了自己的解决方法,依旧以上面的函数为例:

const state = 'placeholder';const getUserAddress = (metaData) => {return {zipcode: 123456,addr1: 'some addr',addr2: 'some addr2',state: 'some state',// 其他属性...};
};const {zipcode,addr1,addr2,state: currentState,
} = getUserAddress('dummy data');

模板字符串

非常好用的特性之一,它最大的特点就在于能够拼接变量以及保证原有的格式。如果说有的时候需要写一点 HTML 的东西,就非常的有用了:

// 拼接字符串
const fullName = `${firstName} ${lastName}`;// 保证原有格式增强可读性
const tempHtml = `
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body></body>
</html>
`;console.log(tempHtml);
/*
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body></body>
</html>
*/

效果图如下:

二者混用其实是最能够体现它效果强大的地方:

// 准备要发送出去的html
const tempHtml = `
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body>${bodyEl}
</body>
</html>
`;

Math + Number + String + Object 扩展方法

  • Math

    增加了一些常见的变量,如 Number.EPSILON,最后用的就是增加了两个非常有用的静态方法:

    和一些数学中常用的计算方法,如 acosh,sin 等,除非特定项目,否则用的比较少。

  • Number

    新增了对 NaN 和无穷大的处理的静态函数。

    Number.isInteger(Infinity); // false
    Number.isNaN('NaN'); // false
    
  • String

    • includes,是否包含指定字符串

    • startsWith,是否以该字符串开头

    • endsWith,是否以该字符串结尾

    • repeat,重复字符串

    使用方法:

    const msg = 'Error: some error message';
    msg.includes('Error'); // true
    msg.startsWith('Error'); // true
    msg.endsWith('Error'); // false'6'.repeat(3); // '666s'
    
  • Object

    Object.assign(target, ...sources),将所有可枚举属性的值从衣蛾或多个源对象分配到目标对象。

    是一个浅复制的好方法。

默认参数

现在 JavaScript 可以为形参提供默认值。这样的好处就在于可以省略掉一些空值的判断,如:

function func(id, name, age, interests = []) {// 进行操作
}// 以前可能会写
function func(id, name, age, interests) {if (interest && interest.length > 0) {// 进行操作}
}

现在就算再调用函数时没有提供参数,也可以直接进行循环操作了。

注:需要提供默认值的参数一定是放在最后的参数,所以才能做到在没有传值的时候设置默认值。

剩余 与 展开 操作符

... 操作符,可以用来传递剩余的参数,也可以用来展开数组或是对象。

  • 当用来传递剩余参数时

    可以用来展开所有的参数,并且以数组的形式在函数内被调用。

    这要哪个的优点在于可以避免使用 arguments 来获取所有的参数,同时,因为 ... 会获取剩余所有的参数,因此只能放置在参数的最后,并且只能用 1 次。

    function func(...args) {console.log(args); // [1,2,3,4,]
    }func(1, 2, 3, 4);
    
  • 当用来展开时

    可以用来展开数据,同样也可以用来做浅拷贝:

    const test = [1, 2, 3, 4, 5];
    const copy = [...test];
    console.log(copy); // [1, 2, 3, 4, 5]const test2 = { name: 'test', age: 18 };
    const copy2 = { ...test2 };
    console.log(copy2); // { name: 'test', age: 18 }// 当需要输出一些值,就不需要用 test[0] test[1] 这样扣值
    console.log(...test); // 1 2 3 4 5
    

箭头函数

现在比较推荐的写法,可以简化定义函数的代码,语法为:

const/let/var 函数名 = (参数) => {}

// 使用传统函数
function foo(val) {return val + 1;
}// 使用箭头函数
// 当只有一行时,可以忽略花括号直接在 => 后写要返回的值
const foo = (val) => val + 1;// 返回多行时,可以用圆括号包起来
const foo = (val) => (<div><div>paragraph1</div><div>paragraph2</div></div>
);// 当操作有多行时,需要拥花括号包起来,并在 return 后申明返回值
const foo = (val) => {console.log(val);return val + 1;
};

与普通的函数相比,并且没有自己的 thisargumentssupernew.target

箭头函数可以用来取代以前使用匿名函数的地方。

对象字面量

有了以下几个有用的语法糖

  • 当变量名一致时,可以省略变量名

    const data = { name: 'name', age: 18 };const testData = {'Content-Type': 'text/plain',data,
    };console.log(testData); // { 'Content-Type': 'text/plain', data: { name: 'name', age: 18 } }
    
  • 计算属性名,也就是用 [] 动态传递属性名

    这个操作在循环调用时,或者需要动态传递参数时非常有用

    const data = { name: 'name', age: 18 };
    for (const key in data) {console.log(data[key]);
    }
    // name
    // 18
    

代理 Proxy

ES6 新推出的代理功能,Proxy 是对于目标对象的抽象,在实际操作目标对象时,可以通过 Proxy 实现对基本操作的拦截和自定义。

语法为:

const p = new Proxy(target, handler)

我觉得 MDN 中,通过 Proxy 绑定验证的案例挺好的:

let validator = {set: function (obj, prop, value) {if (prop === 'age') {if (!Number.isInteger(value)) {throw new TypeError('The age is not an integer');}if (value > 200) {throw new RangeError('The age seems invalid');}}// The default behavior to store the valueobj[prop] = value;// 表示成功return true;},
};let person = new Proxy({}, validator);person.age = 100;console.log(person.age);
// 100person.age = 'young';
// 抛出异常: Uncaught TypeError: The age is not an integerperson.age = 300;
// 抛出异常: Uncaught RangeError: The age seems invalid

通过将验证抽离出来,能够对验证方法进行有效复用,在做数据验证时,也能够省去很多功夫。

反射 Reflect

Reflect 是一个内置的静态对象,它提供拦截 JavaScript 操作的方法。它的方法与 proxy handlers 的方法相同。

mdn 上的用法也很全:

const duck = {name: 'Maurice',color: 'white',greeting: function () {console.log(`Quaaaack! My name is ${this.name}`);},
};Reflect.has(duck, 'color');
// true
Reflect.has(duck, 'haircut');
// false

可能是说 Reflect 主要的作用还是为了能够提供一个标准的操作方法,如:

// 判断是否有某个属性
if (property in obj) {}
// 删除某个属性
delete obj.proprety;
// 获取所有的属性名
Object.keys(obj);// 使用 Reflect 进行同样的操作
Reflect.has(obj, property);
Reflect.deleteProperty(obj, property);
Reflect.ownKesy(obj);

相比较而言,Reflect 能够提供一个更加统一的接口调用标准,对于新手而言,查文档也会方便一些。

期约 Promises

提供了一种更好地解决异步编程的方案,通过链式调用扁平化函数调用,解决回调地狱的问题。

传统的写法可能会这么写:

function fetchData(callback) {fetchData2((err, data) => {if (err) return handleRejected2;fetchData3((err, data) => {if (err) return handleRejected3;fetchData4((err, data) => {// ...});});});
}

但是使用了 Promiise 之后,就可以使用 then 去链式调用:

const promise = new Promise(resolve, reject);
promise.then(fetchData, handleRejected1).then(fetchData2, handleRejected2).then(fetchData3, handleRejected3).then(fetchData4, handleRejected4);

从结构上来说也会更加的清晰一些。

以前是采用 function 的原型链继承法去实现的继承,如:

function Person(name) {this.name = name;
}
// 添加新的原型链继承方法
Person.prototype.work = function () {console.log('996是福报……?');
};

而使用 class 的结构会更加的清晰:

class Person {constructor(name) {this.name = name;}work() {console.log('996是福报……?');}
}

其实之前有其他的语言基础的话,使用 class 理解起来应该非常的轻松——和其他的语言非常的相似。

类的继承

而类的继承的方法,也与其他的语言看起来非常相似,下面是来自 MDN 的例子:

class Polygon {constructor(height, width) {this.name = 'Polygon';this.height = height;this.width = width;}sayName() {ChromeSamples.log('Hi, I am a ', this.name + '.');}sayHistory() {ChromeSamples.log('"Polygon" is derived from the Greek polus (many) ' + 'and gonia (angle).');}
}class Square extends Polygon {constructor(length) {super(length, length);this.name = 'Square';}getArea() {return this.height * this.width;}
}
内置对象可被继承

内置对象,如 Array, Date 等可被用来继承:

// User code of Array subclass
class MyArray extends Array {constructor(...args) {super(...args);}
}var arr = new MyArray();
arr[1] = 12;
arr.length == 2;

类的静态方法

注意,静态方法是直接挂载在类上,而不是类的实例上。

目前感觉主要的调用方法就是返回一个新的对象:

class Square extends Polygon {constructor(length) {super(length, length);this.name = 'Square';}static create(name) {return new Square(name);}
}const square = Square.create('square');

Map + Set + WeakMap + WeakSet

JavaScript 中的 Map, Set, WeakMap, WeakSet 之前的笔记写的还挺详细的。

这里放几个关键点:

  • Map 存储的是键值对

  • 对于多数 Web 开发来说,使用 Object 还是 Map 只是个人偏好问题,影响不大。不过对于在乎性能和内存的用户来说,二者还是有区别的

    Object Map
    内存占用 浏览器实现不同 浏览器实现不同 \newline 但是给定固定内存大小,Map 能比 Object 多存储 50%的键值对
    插入性能 大致相当 大致相当 \newline 一般来说性能会稍微好一些
    插入顺序 不会维护 会维护
    查找速度 大型数据源中,Object 和 Map 的性能差异极小 \newline 但是数据量比较小的情况下,使用 Object 更快 \newline 所以涉及大量查找的情况使用 Object 会比较好 大型数据源中,Object 和 Map 的性能差异极小
    删除性能 使用 delete 删除对象的属性一直为人所诟病 \newline 在 JavaScript 高级程序设计第四章学习笔记 中也提到过: \newline 另外,使用 delete 操作也会动态更新属性,从而使得两个类无法共享同样的隐藏类 \newline 很多时候都会使用将属性值设置为 null 或 undefined 作为折中 相比较而言,Map 的 delete 方法都比插入和查找更快 \newline 如果代码涉及到大量删除的操作,那么毋庸置疑的应该使用 Map
  • WeakMap 可以拟态私有变量

  • WeakMap 很适合关联 DOM 节点元数据

  • Set 的很多 API 与行为与 Map 是共有的,因此操作上会有一些相似性

  • WeakSet 基本 API 与 Set 相似,基本特性与 WeakMap 相似

迭代器 & 生成器 & for of

又是一篇写过笔记的内容:JavaScript 高级程序设计第 7 章 迭代器和生成器 学习笔记

迭代器

迭代器主要是抽象了内部的迭代方法,传统用法中,如果要迭代某个对象,例如说数组,那么就一定需要对它的结构有所了解。

但是在实际开发中,下游的开发人员并不可能了解所有封装后对象的结构,因此,对封装后对象中的内容进行迭代就成了一个比较苦恼的事情。

这里举一个简单的例子:

// 商品列表
class ProductList {constructor() {// 初始化所有的产品this._productList = [];}addItem(name, price) {// 这里偷懒就新建一个类,而是直接用对象存储了const product = {id: this._productList.length + 1,name,price,date: new Date(),};this._productList.push(product);}// 实现获得所有商品的迭代[Symbol.iterator]() {// 注意 this 的指向问题,return中的next是一个闭包,指向会变let count = 0;const length = this._productList.length,productList = this._productList;return {next() {if (count < length) {return { done: false, value: productList[count++] };} else {return { done: true, value: undefined };}},};}
}const productList = new ProductList();
productList.addItem('apple', 10);
productList.addItem('pear', 5);for (product of productList) {console.log(product);// { id: 1, name: 'apple', price: 10, date: 2021-06-07T14:15:42.994Z }// { id: 2, name: 'pear', price: 5, date: 2021-06-07T14:15:42.994Z }
}

在这个案例之中就能够看出来,当我们需要迭代整个商品列表去获得单独的一个商品时,并不需要关注商品列表的结构是什么,只需要调用 for of 即可。

如果明天需求变了,产品的储存方式从数组变成了 Map,或是其他的自定义结构了,那么只要实现了迭代器的接口,就一直可以使用 for of 去获得商品。

也因此,只要修改 迭代器 中的代码就可以了,而不需要牵一发动全身,上下寻找引用到处修改。

生成器

生成器 是实现了 迭代器接口的 自定义迭代器。

比起迭代器来说它更加的灵活,具有通过 yield,在函数块内实现暂停和恢复执行代码的能力。

语法为:

function* func(){}

通常来说生成器的用法更多,因为它的实现方法简单,并且实现了迭代器接口。但是生成器是一个函数,因此,当处理的情况比较复杂(很少见)时,就不得不用迭代器去实现了。

for of

for of 是 ES6 新增的迭代方法,它可以用在任何实现了迭代器接口的集合上。

比起 forEach 而言,for of 具有可以使用 break 去中断操作的属性,而非强制性的使用 return 去返回整个函数。

Symbols

Symbol 是 ES6 新增的一个基本类型,可以接受字符串作为自身的描述。

它具有不可复制性,因此可以被用来实现其他类的扩展功能——使用 Symbol 作为对象属性,就不用担心会重写已有的函数了。

另外,因为 Symbol 具有不可复制性,所以就算在作用域之外重新定义一个具有相同字符串描述的 Symbol 也不代表它们的引用地址相同,因此,也可以用来模拟私有变量。

Modules

ES6 自带的模块属性,算是对 import/export 实现的一个标准吧,不需要再借用社区的规范了,自带严格模式。

使用方式如下:

// lib/math.js
export function sum(x, y) {return x + y;
}
export var pi = 3.141593;// app.js
import * as math from 'lib/math';
console.log('2π = ' + math.sum(math.pi, math.pi));

Unicode

JavaScript 完全支持 unicode:

// same as ES5.1
'												

都 2021 年了还不会连 ES6/ES2015 更新了什么都不知道吧相关推荐

  1. 什么?都2021年了还不会ajax嘛,来这里让您快速学会Ajax

    文章目录 学习网站 Ajax 为什么要学习Ajax Ajax概述及应用场景 Ajax的运行环境 Ajax运行原理 Ajax实现步骤 服务器端响应的数据格式 请求参数格式 获取服务端响应的另一种方式(了 ...

  2. 这都2021年了还不懂Linux?一张思维导图帮你理清思路!【建议收藏!】

    Linux思维导图 Linux常用命令 Linux网络配置 Linux进程管理 Linux服务管理 只用一张图即可理清思路!!! 长图加载有点慢,双击即可看到! 持续更新中~~~~需要xmind文件评 ...

  3. 都2021年了,再不学ES6你就out了 —— 一文搞懂ES6

    JS干货分享 -- 一文搞懂ES6 导语:ES6是什么?用来做什么? 1. let 与 const 2. 解构赋值 3. 模板字符串 4. ES6 函数(升级后更爽) 5. Class类 6. Map ...

  4. 都2021年了,不会还有人连深度学习还不了解吧(六)-- Padding篇

    导读 本篇文章主要介绍CNN中常见的填充方式Padding,Padding在CNN中用的很多,是CNN必不可少的组成部分,使用Padding的目的主要是为了调整输出的大小,是必须搞清楚的知识点.如果你 ...

  5. 都2021年了,不会还有人连深度学习都不了解吧(五)-- 下采样篇

    导读 该篇文章重点介绍CNN中下采样方式,下采样是CNN中必不可少的阶段之一,CNN中常用的下采样方式有平均池化和最大池化,同时平均池化和最大池化也是注意力机制的重要组件. 目前深度学习系列已经更新了 ...

  6. 都2021年了,不会还有人连深度学习都不了解吧(三)- 损失函数篇

    一.前言 深度学习系列文章陆陆续续已经发了两篇,分别是激活函数篇和卷积篇,纯干货分享,想要入门深度学习的童鞋不容错过噢!书接上文,该篇文章来给大家介绍" 选择对象的标准 "-- 损 ...

  7. 都2021年了,不会还有人连深度学习都不了解吧(一)- 激活函数篇

    一.前言 本人目前研一,研究方向为基于深度学习的医学图像分割,转眼间已接触深度学习快1年,研一生活也即将结束,期间看了大量的英文文献,做了大量的实验,也算是对深度学习有了一个初步的了解吧.接下来的一段 ...

  8. 都2021年了,你还在考虑电赛飞行器赛题,备赛是否有必要用基于TI处理芯片的飞控问题?

    无名创新售后群问题节选 @无名小哥 能问下今年国赛会不会指定某一款飞控呀? 答:都2021年了,你还在考虑电赛飞行器赛题备赛是否有必要用基于TI处理芯片的飞控问题? 暂不论官方是否会限定TI芯片的飞控 ...

  9. 都2021年了,不会还有人连深度学习都不了解吧(二)- 卷积篇

    一.前言 上篇文章详细阐述了激活函数是什么.常用的激活函数有哪些以及为什么要使用激活函数,相信大家对此有了一定的了解.在此基础上,我们趁热打铁,继续学习深度学习其它必须的知识.该篇文章讲述卷积操作及其 ...

最新文章

  1. List复制:深拷贝和浅拷贝用法及区别
  2. yolov3模型识别不出训练图片_YOLOv3训练自己的模型
  3. Flutter 基础Widgets之AppBar详解
  4. ES中如何使用逗号来分词
  5. 初学Java ssh之Spring 第二篇
  6. php join a.id b.id,mysql求助 请问where a.id=b.id 和join on a.id=b.id 在效率上的区别
  7. oracle虚拟机字符集,更改虚拟机上的oracle字符集
  8. python django前端重构_django修改models重建数据库的操作
  9. mac文件丢失,苹果电脑有没有好用的恢复软件?
  10. 【微信小程序】支付过程详解
  11. python基于二维数据矩阵随机生成图像文件
  12. php 汽车品牌三级联动,车辆品牌型号的三级联动菜单怎么做的
  13. 2022最新第四方聚合支付系统源码+详细搭建教程
  14. 关于oracle误删数据如何进行恢复
  15. 漫画:什么是加密算法?
  16. 数据仓库--事实表和维度表
  17. 关于机器人创业:学术界vs工业界及中国机器人企业的机会
  18. i7处理器好吗_英特尔酷睿i5处理器和i7有什么区别
  19. 【NLP】OpenAI GPT算法理解
  20. deep deepfm wide 区别_FM算法和DeepFM算法

热门文章

  1. 中国电信计算机通信笔试题,中国电信入职考试题 求大神解答!
  2. 自适应+两栏三栏布局
  3. Command python setup.py egg_info failed with error code 1 in C:\Users\ADMINI~1\AppData\Local\Temp\
  4. HTML——HTML 简介
  5. mapstruct 使用与问题解决
  6. [转]ACM-ICPC比赛随想——刘汝佳
  7. 微信无法打开网页下载链接的解决方案,微信跳转外部浏览器
  8. Linux和Windows命令行中使用命令的输出(删除几天前的日志)
  9. JavaScript-狂神说笔记
  10. c++打印心形_C语言控制台打印3D爱心图案