JavaScript 进阶学习

函数

函数定义

三种定义函数的方式:

  1. 使用 function 关键字 函数声明方式(命名函数):

    1
    function fn(){};
  2. 函数表达式(匿名函数)

    1
    var fn = function(){};
  3. 调用 Function('参数1','参数2'..., '函数体') 构造函数:

    1
    2
    var f = new Function('a', 'b', 'console.log(a + b)');
    f(1, 2);

第三种方式执行效率低,也不方便书写,因此较少使用,但是要知道所有函数本质上都是 Function 的实例对象,可以通过函数对象的原型 __proto__ 属性来获取到 Function 原型对象。

image-20210913164629767

函数调用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 1. 普通函数 */
function fn() {
console.log('人生的巅峰');
}
fn(); // 直接加小括号调用
/* 2. 对象的方法 */
var o = {
sayHi: function() {
console.log('人生的巅峰');
}
}
o.sayHi(); // 通过对象实例调用
/* 3. 构造函数*/
function Star() {};
new Star(); // 使用 new 关键字调用
/* 4. 绑定事件函数*/
btn.onclick = function() {}; // 点击了按钮就可以调用这个回调函数
/* 5. 定时器函数*/
setInterval(function() {}, 1000); 这个函数是定时器自动1秒钟调用一次
/* 6. 立即执行函数(自调用函数)*/
(function() {
console.log('人生的巅峰');
})();

函数中的 this 指向

调用函数的时候确定了 this 的指向,调用方式的不同决定了 this 的指向不同
一般来说 this 指向调用者,谁调用了函数 this 指向谁。.

image-20210913165516840

JavaScript 提供了一些函数方法处理函数内部 this 的指向问题,常用的有 bind()call()apply() 三种方法。

  • call(thisArg, arg1, arg2, ...):执行这个函数对象,并且使函数运行时的 this 指向 thisArgarg1arg2 是向函数传递的其他参数,该方法的返回值就是函数对象执行得到的返回值。call() 常用于组合继承中子类调用父类的构造函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var o = {
    name: 'andy'
    }
    function fn(a, b) {
    console.log(this);
    console.log(a+b)
    };
    fn(1,2)// 此时的this指向的是window
    fn.call(o,1,2)//此时的this指向的是对象o,参数使用逗号隔开
  • apply(thisArg, [argsArray]):执行这个函数对象,并且使函数运行时的 this 指向 thisArgargsArray 是向函数传递的参数形成的数组。apply() 多用在数组相关的方法上,通过参数数组一次传入所有参数,不用再需要一个一个传参。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var o = {
    name: 'andy'
    }
    function fn(a, b) {
    console.log(this);
    console.log(a+b);
    };
    fn() // 此时的 this 指向的是 window
    fn.apply(o,[1,2]); // 此时的 this 指向的是对象 o,参数使用数组传递

    var arr = [1, 2, 3];
    Math.max.apply(null,arr); // 等效 Math.max(1,2,3);
  • bind(thisArg, arg1, arg2, ...):该方法不会立刻执行函数对象,但是能改变将来函数运行时内部 this 的指向,返回的是改变 this 指向并传递完参数产生的新函数。bind() 多用于改变回调函数中 this 的指向。

高阶函数

高阶函数就是对其他函数进行操作的函数,它接收函数作为参数或将函数作为返回值输出。

在 JavaScript 中,函数也是一种数据类型,函数对象可以作为参数传递给另外一个参数使用或者作为返回值传递回来。 最典型的就是一个函数对象作为参数传入,等待着被回调。

面向对象编程

在 JavaScript 中,对象是由属性和方法组成的,具体表现为一组无序键值对的集合。

在典型的 OOP 语言中(如 Java ),都存在类的概念,类就是对象的模板,对象就是类的实例,但在 ES6 标准之前,JavaScript 中并没用引入类的概念,对象不是基于类创建的,而是用一类称为构造函数的特殊函数来定义对象们的共同特征。

构造函数是一种特殊的函数,主要用来初始化对象,即为对象成员变量赋初始值,构造函数的调用与 new 一起使用。可以把对象中一些公共的属性和方法抽取出来,然使用 this 关键字封装到这个函数里面。

1
2
3
4
5
function Person(name,age){
this.name = name;
this.age = age;
}
var obj = new Person('张三',12);

通常构造函数的首字母要大写,以和普通函数相区别,但这是一种约定,而不是强制规定。

实际上构造函数本质上和普通函数一样,都是 Function 对象实例,但是 thisnew 让它的用法与普通函数有所区别。

使用 new 调用函数,那么这个函数就是构造函数,就是类模板,如果直接调用就是一个普通函数,其中的 this 指向全局对象。

使用 new 调用执行构造函数时会做四件事情:

  1. 在内存中创建一个新的空对象;
  2. 让构造函数中的 this 指向这个新的对象;
  3. 执行构造函数里面的代码,给这个新对象添加属性和方法;
  4. 函数体执行完毕,返回这个新对象(所以构造函数里面不需要 return 关键字手动返回生成的对象)。

JavaScript 的构造函数中可以添加一些成员,可以在构造函数对象本身上添加,也可以在构造函数内部的 this 上添加。通过这两种方式添加的成员,就分别称为静态成员和实例成员。

  • 静态成员:构造函数本身也是一个对象,所以可以为函数添加属性以及方法。在构造函数自身上添加的成员称为静态成员,也称为类成员,只能通过构造函数对象来访问,不能通过实例对象来访问,这一点和 Java 不一样。
  • 实例成员:在构造函数内部使用 this 关键字创建的对象成员称为实例成员,只能由实例化的对象来访问。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Star(name, age) {
this.name = name; // 实例变量
this.sing = function () { // 实例方法
console.log("唱歌");
}
}
Star.count = 2; // 静态变量,或称类变量
Star.act = function () { // 静态方法,或称类方法,类方法中不能访问实例成员中的变量
console.log("演戏");
}
Star.act();
var ldh = new Star("刘德华", 33);
console.log(ldh.count); // undefined,不能通过实例对象来访问静态成员
console.log(Star.count); // 静态成员只能通过构造函数来访问

原型对象 prototype

由于 JavaScript 中函数也是对象,每次使用构造函数实例化一个新对象时,实例方法也会被分配相应的内存,那么同样的方法在内存中存在多处副本,比较浪费内存。

image-20210912200401689

JavaScript 规定,每个函数都有一个 prototype 属性,指向一个对象,也称为构造函数的原型对象。对于普通函数来说,该属性基本无用。但是,对于构造函数来说,生成实例的时候,该属性会自动成为实例对象的 __proto__ 属性。

把实例方法直接定义在构造函数的 prototype 属性上,这样所有对象实例就可以共享同一个方法以节省内存,通过 prototype 原型对象添加的方法也叫原型方法。

1
2
3
4
5
6
7
8
9
10
11
12
function Star(uname, age) {
this.uname = uname;
this.age = age;
}
Star.prototype.sing = function() {
console.log('我会唱歌');
}
var ldh = new Star('刘德华', 18);
var zxy = new Star('张学友', 19);
// 共享原型对象上的同一个方法
ldh.sing();
zxy.sing();

通过原型对象,可以对 JavaScript 的内置对象添加自定义的方法。但不要给内置对象的原型对象整体赋值,这样原型对象原有的一些属性被覆盖或者丢失,只能为原型对象添加单个属性。比如给数组增加自定义求偶数和的功能:

1
2
3
4
5
6
7
8
Array.prototype = { sum: function() {}} // 这样会使 Array 类原有的功能丢失
Array.prototype.sum = function() {
var sum = 0;
for (var i = 0; i < this.length; i++) {
sum += this[i];
}
return sum;
};

对象原型 __proto__

实例对象都会有一个属性 __proto__ 指向构造函数的 prototype 原型对象,之所以实例对象可以使用构造函数 prototype 原型对象的属性和方法,就是因为对象有 __proto__ 原型的存在。

image-20210912170021936

对象原型 __proto__ 和原型对象 prototype 是等价的,指向的是同一个对象。

对象原型 __proto__ 的意义就在于为对象的查找机制提供一个方向,或者说一条路线,但是它是一个非标准属性,因此实际开发中,避免使用该属性。

__proto__ 并不是语言本身的特性,这是各大厂商具体实现时添加的私有属性,虽然目前很多现代浏览器的 JS 引擎中都提供了这个私有属性,但依旧不建议在生产中使用该属性,避免对环境产生依赖。生产环境中,我们可以使用 Object.getPrototypeOf 方法来获取实例对象的原型,然后再来为原型添加方法/属性。

构造函数 constructor

对象原型 __proto__ 和构造函数原型对象 prototype 里面都有一个 constructor 属性指回构造函数本身,也称之为构造函数属性。constructor 主要用于记录该对象引用于哪个构造函数,它可以让原型对象重新指向原来的构造函数。

image-20210912170138634

一般情况下,对象的实例方法都在构造函数的原型对象 prototype 中设置。如果要通过原型对象一次添加多个实例方法,可以给原型对象采取对象形式赋值。但是这样赋值后,原型对象中原来的一些属性被覆盖或者丢失,其中 constructor 属性就不再指向当前构造函数了。因此应该在修改后的原型对象中,添加一个 constructor 属性指向原来的构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Star(uname, age) {
this.uname = uname;
this.age = age;
}
// 给原型对象赋值的一个对象一次添加了两个实例方法,但必须手动的利用 constructor 指回原来的构造函数
Star.prototype = {
constructor: Star, // 手动设置指回原来的构造函数
sing: function() {
console.log('我会唱歌');
},
movie: function() {
console.log('我会演电影');
}
}

原型链

对象原型 __proto__ 的意义就在于为对象成员查找机制提供一个方向,或者说一条路线。 每一个实例对象有一个 __proto__ 属性,指向构造函数的原型对象 prototype,构造函数的原型对象也是一个对象,也有 __proto__ 属性指向原型对象的原型,这样一层一层就形成了原型链。

image-20210912212344413

JavaScript 的原型链查找机制(规则):

  1. 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。
  2. 如果没有就查找它的原型(也就是对象的 __proto__ 属性指向的原型对象 prototype )有没有该属性。
  3. 如果还没有就查找原型对象的原型,依此类推一直找到 Object 的原型对象为止(Object 对象实例的 __proto__ 属性值为 null)。

类的继承

大部分面向对象的编程语言,都是通过 extends 实现类的继承。在 ES6 之前,JavaScript 没有提供 extends 继承关键字,但可以通过构造函数和原型对象模拟实现继承,这种继承被称为组合继承。

借用构造函数继承父类属性的核心原理: 通过 call() 调用父类的构造函数,并把父类型的 this 指向子类型的 this ,这样就可以实现子类继承父类中的属性。

一般情况下,对象的方法都在构造函数的原型对象中设置,通过构造函数无法继承父类的原型方法,需要使用到原型对象。

借用原型对象继承父类方法的核心原理: 让子类的原型指向父类的原型,这样子类对象就可以通过原型链找到父类的方法,即继承了父类的方法。

组合继承步骤:

  1. 先定义父构造函数
  2. 再定义子构造函数
  3. 在子类的构造函数中通过 call() 调用父类的构造函数,并把父类型的 this 指向子类型的 this ,这样就可以实现子类继承父类中的属性。
  4. 让子类的原型对象 prototype 指向一个父类对象实例,这样子类就可以继承父类的方法。
  5. 将子类的 constructor 属性重新指向子类的构造函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 1.先定义父构造函数
function Father(uname, age) {
// this 指向父构造函数的对象实例
this.uname = uname;
this.age = age;
}
// 父类原型方法
Father.prototype.money = function () {
console.log("大人要挣钱");
};
// 2.再定义子构造函数
function Son(uname, age, score) {
// 3.使用 call() 调用父类的构造函数,并且让父类的 this 指向子类的 this,同时调用这个函数
Father.call(this, uname, age);
// this 指向子构造函数的对象实例
this.score = score;
}
// Son.prototype = Father.prototype 会让父原型对象会跟着子原型对象一起变化。
// 4.让子类的原型对象 prototype 指向一个父类对象实例
Son.prototype = new Father();
// 5.将子类的 constructor 属性重新指向子类的构造函数。
Son.prototype.constructor = Son;
// 子类原型方法
Son.prototype.exam = function () {
console.log('孩子要考试');
}
var son = new Son('刘德华', 18, 100);
console.log(son.uname);
son.money();

image-20210912232338262

让子类的原型对象 prototype 指向一个新创建的父类对象实例,子类对象可以通过 __proto__ 属性接找到父类的对象原型 __proto__ 中的方法。

ES5 新特性

Array 新增方法

下面的遍历数组的方法都接受一个函数作为参数,所有数组成员都会被依次传入该函数执行,其中 currentValue 是当前传入的数组成员的值,index 是传入成员项在数组中的索引,arr 是数组对象本身。

  • map(function(currentValue, index, arr)):将数组的所有成员依次传入参数函数,然后把每一次的执行返回结果组成一个新数组返回。
  • forEach(function(currentValue, index, arr))forEach()方法与map() 方法很相似,也是对数组的所有成员依次执行参数函数。但是,forEach() 方法不返回值,只用来操作数据,在函数里面 return 也不会终止迭代。这就是说,如果数组遍历的目的是为了得到返回值,那么使用 map() 方法,否则使用 forEach() 方法。
  • filter(function(currentValue, index, arr)):用于筛选数组用于过滤数组成员,它的参数是一个函数,所有数组成员依次执行该函数,函数返回为 true 的成员组成一个新数组返回。该方法不会改变原数组。
  • some(function(currentValue, index, arr)):用于检测数组中的是否存在元素满足指定条件,只要一个数组成员的执行该函数的返回值是 true,则整个 some 方法的返回值就是 true,否则返回 false。 如果找到第一个满足条件的元素,则终止循环不在继续查找。
  • every(function(currentValue, index, arr)):用于检测数组中的所有元素,所有成员传入函数执行的返回值都是 true,整个 every方法才返回 true,否则返回false

String 新增方法

  • trim():方法用于去除字符串两端的空格,返回一个新字符串,不改变原字符串。该方法去除的不仅是空格,还包括制表符(\t\v)、换行符(\n)和回车符(\r)。

Object 方法

  • Object.keys(obj):获取到对象中的属性名,返回值是一个数组,该数组的成员都是该对象自身的(不包括继承的)所有属性名。返回的数组配合数组 foeEach() 方法可以遍历原对象。
  • Object.defineProperty(obj, prop, descriptor):定义新属性或修改原有的属性,obj 是待修改的目标对象,prop 是需新增或修改的属性的名字,descriptor 描述对象用来说明目标属性所拥有的特性,描述对象有四个子属性,value 是要赋给目标属性的值。 writable 决定目标属性值是否可以重写。如果该属性值为 false,则不允许再修改对象中的这个属性值。 enumerable 决定目标属性是否可以被枚举遍历,如果值为 false 则不允许遍历。configurable 决定目标属性是否可以被删除或是否可以再次修改特性,如果为 false 则不允许使用 delete 删除这个属性。

严格模式

ES5 提供了严格模式(strict mode)即在严格的条件下运行 JavaScript 代码。严格模式在 IE 10 以上版本的浏览器中才会被支持,旧版本浏览器中会被忽略。严格模式对正常的 JavaScript 语义做了一些更改:

  1. 消除了 Javascript 语法的一些不合理、不严谨之处,减少了一些怪异行为,例如未声明就使用变量。
  2. 消除代码运行的一些不安全之处,保证代码运行的安全。
  3. 提高编译器效率,增加运行速度。
  4. 禁用了在 ECMAScript 的未来版本中可能会重新定义的一些语法,为未来新版本的 Javascript 做好铺垫。比如一些保留字如:classenumexportextendsimportsuper 不能做变量名。

在正常模式中,如果一个变量没有声明就赋值,默认是全局变量。严格模式禁止这种用法,变量都必须先用声明,然后再使用,并且禁止使用 delete 删除已经声明变量。

严格模式中函数形参列表中不能有重名的参数,并且函数必须声明在顶层,不允许在非函数的代码块内声明函数。为了与新版本( ES6 中已引入)即将引入的块级作用域。

严格模式下 this 指向问题:

  1. 以前在全局作用域函数中的 this 指向全局对象(浏览器中是 window 对象),严格模式下全局作用域中函数中的 thisundefined
  2. 以前构造函数时也可以当作普通函数不加 new 调用,其中this 指向全局对象。严格模式下,如果构造函数不加 new 调用,由于 this 指向的是 undefined 构造函数汇中给 this 添加属性的操作则会报错
  3. 其他类型函数中 this 和普通模式一样,new 调用构造函数中 this 指向创建的对象实例,定时器函数 this 指向全局对象(浏览器中是 window 对象),回调函数中 this 指向调用者。

严格模式的更多要求请参考 MDN:严格模式

严格模式可以应用到整个脚本或个别函数中。因此在使用时,我们可以将严格模式分为为脚本开启严格模式和为函数开启严格模式两种情况。

  • 为脚本开启严格模式:在脚本的第一行,所有语句之前添加一行特定语句"use strict";'use strict';,老版本的浏览器会把该行它当作一行普通字符串表达式而忽略。

    1
    2
    3
    4
    <script>
    "use strict";
    console.log("这是严格模式");
    </script>
  • 为函数开启严格模式:把特定语句"use strict";'use strict';放在函数体所有语句之前,整个函数以 “严格模式” 运行。

    1
    2
    3
    4
    function fn(){
    "use strict";
    console.log("这是严格模式");
    }
  • 当有多个脚本文件需要合并时,可能其中有的脚本是严格模式,有的脚本是正常模式。可以将整个脚本文件中的代码放在一个立即执行的匿名函数之中,再在函数体所有语句之前添加严格模式声明,这样就不影响其他未开启严格模式的脚本文件。

    1
    2
    3
    4
    5
    6
    <script>
    (function (){
    "use strict";
    console.log("这是严格模式");
    })();
    </script>

ES6 新特性

ES 的全称是 ECMAScript , 它是由 ECMA 国际标准化组织,制定的一项脚本语言的标准化规范。ES6 实际上是一个泛指,泛指 ES2015 及后续的版本。

image-20210916183052538

let 关键字

let 是 ES6 中新增的用于声明变量的关键字,let 声明的变量只在所处于的块级有效。

使用 let 关键字声明的变量才具有块级作用域,使用var声明的变量不具备块级作用域特性。

1
2
3
4
5
6
{
var a = 10;
let b = 20;
}
console.log(a) // 10
console.log(b) // b is not defined

使用 let 声明的变量不存在变量提升,必须先声明再使用,声明语句前访问会出现引用错误。

1
2
3
4
console.log(a) // undefined
console.log(b) // ReferenceError: Cannot access 'b' before initialization
var a = 10;
let b = 20;

使用 let 声明的变量可能在代码块中存在一段临时性死区,全局作用域的同名变量也不能在死区之中访问。

1
2
3
4
5
6
7
8
var a = 10;
var b = 20
{
console.log(a); // 10
console.log(b); // ReferenceError: Cannot access 'b' before initialization 尽管全局作用域的 b
var a = 100;
let b = 200;
}

const 关键字

const 关键字用来声明常量,constlet 一样,具有块级作用域。

1
2
3
4
5
6
{
var a = 10;
const b = 20;
}
console.log(a) // 10
console.log(b) // b is not defined

由于 const 常量赋值后不可再更改,因此声明常量时就必须赋值,初始化常量。

1
const PI; // Missing initializer in const declaration

常量就是值在第一次赋值初始化后不能再变化的量。具体来说,如果是基本数据类型,其存储的值不能更改,如果是复杂数据类型,由于其存储的是对象的引用地址,则不可再指向其他对象,但是对象中的值可以改变。

1
2
3
4
5
6
7
8
const PI = 3.14;
PI = 100; // Assignment to constant variable.

const ary = [100, 200];
ary[0] = 'a';
ary[1] = 'b';
console.log(ary); // ['a', 'b'];
ary = ['a', 'b']; // Assignment to constant variable.

解构赋值

ES6 中允许在数组、对象中按照对应位置提取值,然后对变量赋值。如果变量跟数组元素个数或者对象属性个数不匹配的时候,则部分结构不成功,未匹配变量的值为 undefined

数组解构用中括号包裹,对象解构用花括号包裹,多个变量用逗号隔开。利用解构赋值能够很方便的提取对象中的属性跟方法。

数组解构赋值:

1
2
3
4
let [a, b, c] = [1, 2];
console.log(a) // 1
console.log(b) // 2
console.log(c) // 如果解构不成功,变量的值为undefined

对象解构赋值:

1
2
3
4
5
6
7
8
let person = { name: 'zhangsan', age: 20 }; 
let { name, age } = person;
console.log(name); // 'zhangsan'
console.log(age); // 20

let {name: myName, age: myAge} = person; // myName myAge 属于别名
console.log(myName); // 'zhangsan'
console.log(myAge); // 20

箭头函数

箭头函数是 ES6 新增的定义匿名函数的方式,箭头函数表达式的语法比函数表达式更简洁。

小括号中是形参列表,大括号是函数体。

1
2
() => {}
const fn = () => {}

如果函数体中只有一句代码,且代码的执行结果就是返回值,可以省略大括号。

1
2
3
4
5
function sum (num1, num2) { 
return num1 + num2;
}
// ES6 箭头函数省略写法
const sum = (num1, num2) => num1 + num2;

如果形参只有一个,可以省略小括号。

1
2
3
4
5
function fn (v) {
return v;
}
// ES6 箭头函数省略写法
const fn = v => v;

箭头函数不绑定 thisargumentssuper 关键字,因此箭头函数只能作为普通函数不能作为类的构造函数使用。

箭头函数中的 this,指向的是函数定义位置的上下文中的 this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
this.age = 100;
var obj1 = {
age: 20,
say: () => {
console.log(this.age);
}
}
obj1.say(); //100,箭头函数 this 指向的是被声明的作用域里面,而对象没有作用域的,所以箭头函数虽然在对象中被定义,但是 this 指向的是全局作用域中的 this

var obj2 = {
name: "张三",
fn: function () {
console.log(this); // this 指向 是 obj 对象
return () => {
console.log(this); // 箭头函数中没有自己的 this,根据作用域链向上查找,即是 fn 的 this
}
}
}
obj2.fn()();

使用箭头函数可以解决匿名函数 this 指向的问题,例如定时器中的回调函数中的 this 往往默认指向全局对象,此前只能手动更改其中 this 的指向或者使用额外的变量来传递 tihs,现在可以直接使用箭头函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Person() {
var that = this;
that.age = 0;

setInterval(function growUp() {
// 回调引用的是 that 变量, 其值是预期的对象.
that.age++;
}, 1000);
}

// ES6 箭头函数写法
function Person(){
this.age = 0;

setInterval(() => {
this.age++; // this 指向构造函数创新的新对象
}, 1000);
}

var p = new Person();

剩余参数

剩余参数语法允许将一些不定数量的参数表示为一个数组,这种方式很方便的去声明不知道参数情况下的一个函数,ES 6 之前要通过 arguments 对象来获取实参,比较麻烦。

1
2
3
4
5
function sum (first, ...args) {
console.log(first); // 10
console.log(args); // [20, 30]
}
sum(10, 20, 30);

剩余参数还可以和解构赋值一起使用:

1
2
3
4
let students = ['wangwu', 'zhangsan', 'lisi'];
let [s1, ...s2] = students;
console.log(s1); // 'wangwu'
console.log(s2); // ['zhangsan', 'lisi']

模板字符串

模板字符串是 ES6 新增的创建字符串的方式,模板字符串使用反引号来代替普通字符串中的用双引号和单引号。

1
2
3
4
let name = `zhangsan`;
let name = `zhangsan`;
console.log(typeof name); // string
console.log(name === "zhangsan"); // true

模板字符串中可以直接换行,不需要转义:

1
2
3
4
console.log(`string text line 1
string text line 2`);
// "string text line 1
// string text line 2"

模板字符串中可以使用 ${ } 占位符嵌入表达式,表达式可以是一个简单的变量、运算操作、函数调用,

1
2
3
4
5
6
7
8
9
10
11
let name = '张三'; 
let sayHello = `my name is ${name}`; // my name is zhangsan

var a = 5;
var b = 10;
console.log(`a + b = ${a + b}`); // a + b = 15

const sayHello = function () {
return 'hello';
};
console.log(`${sayHello()}`); // hello

扩展运算符

扩展运算符可以将数组或者对象转为用逗号分隔的参数序列,又称为展开语法。

1
2
3
4
5
6
7
8
 let ary = [1, 2, 3];
...ary // 1, 2, 3
console.log(...ary); // 1 2 3,相当于下面的代码
console.log(1,2,3);

function myFunction(x, y, z) { }
var args = [0, 1, 2];
myFunction(...args);

使用展开语法合并两个数组:

1
2
3
4
5
6
7
8
let ary1 = [1, 2, 3];
let ary2 = [3, 4, 5];
// 方法一
ary1 = [...ary1, ...ary2];
// 方法二
ary1.push(...ary2);
// 方法三 apply() 方法
ary1.push.apply(ary1, ary2)

将伪数组或者可迭代对象转换为真正的数组:

1
2
let oDivs = document.getElementsByTagName('div'); 
oDivs = [...oDivs];

Array 新增方法

  • Array.isArray() :可以判断一个对象是不是真正的数组,真正的数组对象在生成对象的时候就必须是一个数组。将一个伪数组的 __proto__ 直接或间接指向 Array.prototye,就可以调用数组相关方法,但严格来说并不是一个真正的数组。obj instanceof Arrayture 不能保证就是真正的数组。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 定义一个伪数组对象
    let arrayLike = {
    '0': 'a',
    '1': 'b',
    '2': 'c',
    length: 3
    };
    arrayLike.__proto__ = Array.prototype;
    console.log(arrayLike instanceof Array); // true
    console.log(Array.isArray(arrayLike)); // false
    arrayLike.push(5);
    console.log(arrayLike); // Array { '0': 'a', '1': 'b', '2': 'c', '3': 5, length: 4 }
  • Array.from():将伪数组对象或可迭代对象转换为真正的数组,伪数组对象的属性名必须是对应的索引值,并且必须要有 length 属性。将伪数组转换为真正数组之后就可以调用数组专属方法例如 push()pop() 等。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 定义一个伪数组对象
    let arrayLike = {
    '0': 'a',
    1: 'b',
    '3': 'd',
    length: 5
    };
    let arr = Array.from(arrayLike); // 将伪数组转成数组
    console.log(arrayLike instanceof Array); // false
    console.log(Array.isArray(arrayLike)); // false
    console.log(arr instanceof Array); // ture
    console.log(Array.isArray(arr)); // true
    console.log(arr); // [ 'a', 'b', undefined, 'd', undefined ]

    该方法还可以接受第二个参数,作用类似于数组的 map() 方法,用来对伪数组中每个元素进行处理,将处理后的值放入返回的数组。

    1
    2
    3
    4
    5
    6
    let arrayLike = { 
    "0": 1,
    "1": 2,
    "length": 2
    }
    let newAry = Array.from(arrayLike, item => item *2) // [2,4]
  • find():用于找出第一个符合条件的数组成员,如果没有找到返回 undefined。通过传入函数对象对数组元素进行遍历,如果找到第一个满足条件的元素,则终止循环不在继续查找,类似数组对象的 some() 方法,区别是 some() 返回布尔值,find() 返回查找到的元素。

    1
    2
    3
    4
    5
    6
    7
    8
    let ary = [{
    id: 1,
    name: '张三'
    }, {
    id: 2,
    name: '李四'
    }];
    let target = ary.find((item, index) => item.id == 2); // 找数组里面符合条件的值,当数组中元素id等于2的查找出来,注意,只会匹配第一个
  • findIndex():用于找出第一个符合条件的数组成员的位置,如果没有找到返回 -1。

    1
    2
    3
    let ary = [1, 5, 10, 15];
    let index = ary.findIndex((value, index) => value > 9);
    console.log(index); // 2
  • includes():表示某个数组是否包含给定的值,返回布尔值。

    1
    2
    [1, 2, 3].includes(2) 	// true 
    [1, 2, 3].includes(4) // false

String 新增方法

  • startsWith(searchString[, position]):用来判断当前字符串是否以另外一个给定的子字符串开头,返回布尔值。第一个参数是要搜索的子字符串。第二个参数搜索的开始位置,默认值为 0。

    1
    2
    3
    4
    var str = "To be, or not to be, that is the question.";
    alert(str.startsWith("To be")); // true
    alert(str.startsWith("not to be")); // false
    alert(str.startsWith("not to be", 10)); // true
  • endsWith(searchString[, length]):用来判断当前字符串是否以另外一个给定的子字符串结尾,返回布尔值。第一个参数是要搜索的子字符串。第二个参数作为字符串的长度以判断字符串是否在结尾,默认值为被搜索字符串的长度。

    1
    2
    3
    4
    var str = "To be, or not to be, that is the question.";
    alert( str.endsWith("question.") ); // true
    alert( str.endsWith("to be") ); // false
    alert( str.endsWith("to be", 19) ); // true
  • repeat(n):方法表示将原字符串重复 n 次,返回一个新字符串。

    1
    2
    console.log('x'.repeat(3))      // "xxx" 
    console.log('hello'.repeat(2)) // "hellohello"

Set 集合

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

使用 Set 构造函数用来生成 Set 对象:

1
const set = new Set();

Set() 构造函数可以接受一个数组作为参数,用来初始化集合,并且自动去掉数组汇总重复的元素:

1
2
const set = new Set([1, 2, 3, 4, 4]);
console.log(set); // Set(4) { 1, 2, 3, 4 }
  • add(value):在 Set 对象尾部添加一个元素。返回该 Set 对象。
  • delete(value):移除 Set 中与这个值相等的元素,返回一个布尔值,表示删除是否成功。根据值相等删除而不是索引。
  • has(value):返回一个布尔值,表示该元素是否为 Set 对象的成员。
  • clear():清除所有成员,没有返回值。
  • forEach(callback[, thisArg]):根据集合中元素的插入顺序,对所有元素依次执行提供的回调函数,用来遍历集合元素。
1
2
3
4
5
6
const s = new Set();
s.add(1).add(2).add(3); // 向 Set 集合中添加元素,可以使用链式编程
s.delete(2); // 删除 Set 集合中值为 2 的元素,根据值来删除
s.has(1); // 判断 Set 集合中是否有值为 1 的元素
s.forEach(value => console.log(value)); // 遍历输出 Set 集合中所有的元素
s.clear(); // 清除 Set 集合中的所有元素

正则表达式

正则表达式(Regular Expression)是用于匹配字符串中字符组合的模式。

前端中的正则表达式主要用来:

  • 对表单中用户输入的内容进行合法性验证;
  • 提取大段文本中特定的字符串;
  • 对文本内容的敏感内容进行查找替换。

在 JavaScript 中,正则表达式也是一种对象,可以通过两种方式创建一个正则表达式。

  • 直接通过用斜杠 / 包裹起来的的字面量创建正则表达式:

    1
    var re = /123/;
  • 通过调用 RegExp 对象的构造函数创建:

    1
    2
    var rg = new RegExp(/123/);
    var rg = new RegExp("123");

正则表达式对象的 test() 方法用来检测字符串是否符合该正则表达式规范,该方法会返回 truefalse,其参数是测试字符串,如果是其他类型会自动转换为字符串类型。

1
2
3
var re = /123/;
console.log(re.test(123)); // 自动转换为字符串类型,true
console.log(re.test('abc'));// false

特殊字符

特殊字符也被称为元字符,在正则表达式中是具有特殊意义的专用符号,更多特殊字符请参考 MDN:正则表达式

  • 边界符(位置符):用来提示字符所处的位置,主要有两个字符:

    • ^:表示匹配行首的文本(以谁开始)

    • $:表示匹配行尾的文本(以谁结束)

      如果 ^$ 在一起使用,表示必须是精确匹配。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      var rg = /abc/; 	// 正则表达式里面不需要加引号 不管是数字型还是字符串型
      // /abc/ 只要包含有abc这个字符串返回的都是true
      console.log(rg.test('abc'));
      console.log(rg.test('abcd'));
      console.log(rg.test('aabcd'));
      console.log('---------------------------');
      var reg = /^abc/;
      console.log(reg.test('abc')); // true
      console.log(reg.test('abcd')); // true
      console.log(reg.test('aabcd')); // false
      console.log('---------------------------');
      var reg1 = /^abc$/; // 精确匹配 要求必须是 abc字符串才符合规范
      console.log(reg1.test('abc')); // true
      console.log(reg1.test('abcd')); // false
      console.log(reg1.test('aabcd')); // false
      console.log(reg1.test('abcabc')); // false
  • []:字符组合,所有可供选择的字符都放在方括号内,包括方括号中的任意一个字符即匹配成功。方括号内部加上 - 表示范围,例如,/[abcd]//[a-d]/ 是一样的,方括号内部头上的 ^ 表示取反,只要包含方括号内的任一字符,都匹配失败。特殊符号在字符组合中没有特殊的意义,不需要再转义,/[a|b]/ 相当于 /a|\||b/

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    var rg = /[abc]/; // 只要包含有a 或者 包含有b 或者包含有c 都返回为true
    console.log(rg.test('andy'));//true
    console.log(rg.test('baby'));//true
    console.log(rg.test('color'));//true
    console.log(rg.test('red'));//false

    var rg1 = /^[abc]$/; // 三选一 只有是a 或者是 b 或者是c 这三个字母才返回 true
    console.log(rg1.test('aa'));//false
    console.log(rg1.test('a'));//true
    console.log(rg1.test('b'));//true
    console.log(rg1.test('c'));//true
    console.log(rg1.test('abc'));//true

    var reg = /^[a-z]$/ //26个英文字母任何一个字母返回 true - 表示的是a 到z 的范围
    console.log(reg.test('a'));//true
    console.log(reg.test('z'));//true
    console.log(reg.test('A'));//false

    //字符组合
    var reg1 = /^[a-zA-Z0-9]$/; // 26个英文字母(大写和小写都可以)任何一个字母返回 true
    //取反 方括号内部加上 ^ 表示取反,只要包含方括号内的字符,都返回 false 。
    var reg2 = /^[^a-zA-Z0-9]$/;
    console.log(reg2.test('a'));//false
    console.log(reg2.test('B'));//false
    console.log(reg2.test(8));//false
    console.log(reg2.test('!'));//true
  • 量词符:量词符用来设定某个模式出现的次数,
    image-20210916175110033

  • 预定义类:预定义类指的是某些常见模式的简写方式。
    image-20210916175427867

  • ():分组括号,提升了优先级,并且在之后可以提取括号内容所匹配的字符串。

  • |:选择符号,在几项之选择匹配一项,/[ab]/ 等价于 /a|b//(a|b|c)//[abc]/ 在匹配效果上是等价的,但是前者中的小括号具有捕获引用功能,可以提取子匹配,而后者没有。/(?:a|b|c)//[abc]/ 则是完全等价,参考正则表达式中方括号和小括号的区别

在正则表达式结尾可以通过修饰符来指定按照什么样的模式来匹配,常用的有以下三种修饰符组合:

  • g:全局匹配
  • i:忽略大小写
  • gi:全局匹配 + 忽略大小写
1
2
var re = /abc/g;
var re = new RegExp("abc", "g");

正则表达式配合字符串的 replace(regexp/substr, replacement) 方法可以实现替换字符串操作,该方法传入的第一个参数可以是一个字符串或是一个正则表达式,第二个参数是要替代的字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var str = 'andy和red';
var newStr = str.replace('andy', 'baby');
console.log(newStr)//baby和red
//等同于 此处的andy可以写在正则表达式内
var newStr2 = str.replace(/andy/, 'baby');
console.log(newStr2)//baby和red
//全部替换
var str = 'abcabc'
var nStr = str.replace(/a/,'哈哈')
console.log(nStr) //哈哈bcabc
//全部替换g
var nStr = str.replace(/a/a,'哈哈')
console.log(nStr) //哈哈bc哈哈bc
//忽略大小写i
var str = 'aAbcAba';
var newStr = str.replace(/a/gi,'哈哈')//"哈哈哈哈bc哈哈b哈哈"

参考

  1. JavaScript 进阶面向对象ES6
  2. 阮一峰:JavaScript 教程
  3. 方应杭:JS 的 new 到底是干什么的?
  4. 方应杭:「每日一题」什么是 JS 原型链?
  5. 方应杭:this 的值到底是什么?一次说清楚
  6. MDN:箭头函数

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!