开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

JavaScript深入之继承的多种方式和优缺点

发表于 2017-05-12

JavaScript深入系列第十五篇,讲解JavaScript各种继承方式和优缺点。

写在前面

本文讲解JavaScript各种继承方式和优缺点。

但是注意:

这篇文章更像是笔记,哎,再让我感叹一句:《JavaScript高级程序设计》写得真是太好了!

1.原型链继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码function Parent () {
this.name = 'kevin';
}

Parent.prototype.getName = function () {
console.log(this.name);
}

function Child () {

}

Child.prototype = new Parent();

var child1 = new Child();

console.log(child1.getName()) // kevin

问题:

1.引用类型的属性被所有实例共享,举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码function Parent () {
this.names = ['kevin', 'daisy'];
}

function Child () {

}

Child.prototype = new Parent();

var child1 = new Child();

child1.names.push('yayu');

console.log(child1.names); // ["kevin", "daisy", "yayu"]

var child2 = new Child();

console.log(child2.names); // ["kevin", "daisy", "yayu"]

2.在创建 Child 的实例时,不能向Parent传参

2.借用构造函数(经典继承)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码function Parent () {
this.names = ['kevin', 'daisy'];
}

function Child () {
Parent.call(this);
}

var child1 = new Child();

child1.names.push('yayu');

console.log(child1.names); // ["kevin", "daisy", "yayu"]

var child2 = new Child();

console.log(child2.names); // ["kevin", "daisy"]

优点:

1.避免了引用类型的属性被所有实例共享

2.可以在 Child 中向 Parent 传参

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码function Parent (name) {
this.name = name;
}

function Child (name) {
Parent.call(this, name);
}

var child1 = new Child('kevin');

console.log(child1.name); // kevin

var child2 = new Child('daisy');

console.log(child2.name); // daisy

缺点:

方法都在构造函数中定义,每次创建实例都会创建一遍方法。

3.组合继承

原型链继承和经典继承双剑合璧。

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
30
31
32
复制代码function Parent (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function () {
console.log(this.name)
}

function Child (name, age) {

Parent.call(this, name);

this.age = age;

}

Child.prototype = new Parent();

var child1 = new Child('kevin', '18');

child1.colors.push('black');

console.log(child1.name); // kevin
console.log(child1.age); // 18
console.log(child1.colors); // ["red", "blue", "green", "black"]

var child2 = new Child('daisy', '20');

console.log(child2.name); // daisy
console.log(child2.age); // 20
console.log(child2.colors); // ["red", "blue", "green"]

优点:融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式。

4.原型式继承

1
2
3
4
5
复制代码function createObj(o) {
function F(){}
F.prototype = o;
return new F();
}

就是 ES5 Object.create 的模拟实现,将传入的对象作为创建的对象的原型。

缺点:

包含引用类型的属性值始终都会共享相应的值,这点跟原型链继承一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码var person = {
name: 'kevin',
friends: ['daisy', 'kelly']
}

var person1 = createObj(person);
var person2 = createObj(person);

person1.name = 'person1';
console.log(person2.name); // kevin

person1.firends.push('taylor');
console.log(person2.friends); // ["daisy", "kelly", "taylor"]

注意:修改person1.name的值,person2.name的值并未发生改变,并不是因为person1和person2有独立的 name 值,而是因为person1.name = 'person1',给person1添加了 name 值,并非修改了原型上的 name 值。

  1. 寄生式继承

创建一个仅用于封装继承过程的函数,该函数在内部以某种形式来做增强对象,最后返回对象。

1
2
3
4
5
6
7
复制代码function createObj (o) {
var clone = object.create(o);
clone.sayName = function () {
console.log('hi');
}
return clone;
}

缺点:跟借用构造函数模式一样,每次创建对象都会创建一遍方法。

  1. 寄生组合式继承

为了方便大家阅读,在这里重复一下组合继承的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码function Parent (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function () {
console.log(this.name)
}

function Child (name, age) {
Parent.call(this, name);
this.age = age;
}

Child.prototype = new Parent();

var child1 = new Child('kevin', '18');

console.log(child1)

组合继承最大的缺点是会调用两次父构造函数。

一次是设置子类型实例的原型的时候:

1
复制代码Child.prototype = new Parent();

一次在创建子类型实例的时候:

1
复制代码var child1 = new Child('kevin', '18');

回想下 new 的模拟实现,其实在这句中,我们会执行:

1
复制代码Parent.call(this, name);

在这里,我们又会调用了一次 Parent 构造函数。

所以,在这个例子中,如果我们打印 child1 对象,我们会发现 Child.prototype 和 child1 都有一个属性为colors,属性值为['red', 'blue', 'green']。

那么我们该如何精益求精,避免这一次重复调用呢?

如果我们不使用 Child.prototype = new Parent() ,而是间接的让 Child.prototype 访问到 Parent.prototype 呢?

看看如何实现:

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
复制代码function Parent (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function () {
console.log(this.name)
}

function Child (name, age) {
Parent.call(this, name);
this.age = age;
}

// 关键的三步
var F = function () {};

F.prototype = Parent.prototype;

Child.prototype = new F();


var child1 = new Child('kevin', '18');

console.log(child1);

最后我们封装一下这个继承方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码function object(o) {
function F() {}
F.prototype = o;
return new F();
}

function prototype(child, parent) {
var prototype = object(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
}

// 当我们使用的时候:
prototype(Child, Parent);

引用《JavaScript高级程序设计》中对寄生组合式继承的夸赞就是:

这种方式的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。

相关链接

《JavaScript深入之从原型到原型链》

《JavaScript深入之call和apply的模拟实现》

《JavaScript深入之new的模拟实现》

《JavaScript深入之创建对象》

深入系列

JavaScript深入系列目录地址:github.com/mqyqingfeng…。

JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

JavaScript深入之创建对象的多种方式以及优缺点

发表于 2017-05-10

JavaScript深入系列第十四篇,讲解创建对象的各种方式,以及优缺点。

写在前面

这篇文章讲解创建对象的各种方式,以及优缺点。

但是注意:

这篇文章更像是笔记,因为《JavaScript高级程序设计》写得真是太好了!

  1. 工厂模式

1
2
3
4
5
6
7
8
9
10
11
复制代码function createPerson(name) {
var o = new Object();
o.name = name;
o.getName = function () {
console.log(this.name);
};

return o;
}

var person1 = createPerson('kevin');

缺点:对象无法识别,因为所有的实例都指向一个原型

  1. 构造函数模式

1
2
3
4
5
6
7
8
复制代码function Person(name) {
this.name = name;
this.getName = function () {
console.log(this.name);
};
}

var person1 = new Person('kevin');

优点:实例可以识别为一个特定的类型

缺点:每次创建实例时,每个方法都要被创建一次

2.1 构造函数模式优化

1
2
3
4
5
6
7
8
9
10
复制代码function Person(name) {
this.name = name;
this.getName = getName;
}

function getName() {
console.log(this.name);
}

var person1 = new Person('kevin');

优点:解决了每个方法都要被重新创建的问题

缺点:这叫啥封装……

  1. 原型模式

1
2
3
4
5
6
7
8
9
10
复制代码function Person(name) {

}

Person.prototype.name = 'keivn';
Person.prototype.getName = function () {
console.log(this.name);
};

var person1 = new Person();

优点:方法不会重新创建

缺点:1. 所有的属性和方法都共享 2. 不能初始化参数

3.1 原型模式优化

1
2
3
4
5
6
7
8
9
10
11
12
复制代码function Person(name) {

}

Person.prototype = {
name: 'kevin',
getName: function () {
console.log(this.name);
}
};

var person1 = new Person();

优点:封装性好了一点

缺点:重写了原型,丢失了constructor属性

3.2 原型模式优化

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码function Person(name) {

}

Person.prototype = {
constructor: Person,
name: 'kevin',
getName: function () {
console.log(this.name);
}
};

var person1 = new Person();

优点:实例可以通过constructor属性找到所属构造函数

缺点:原型模式该有的缺点还是有

  1. 组合模式

构造函数模式与原型模式双剑合璧。

1
2
3
4
5
6
7
8
9
10
11
12
复制代码function Person(name) {
this.name = name;
}

Person.prototype = {
constructor: Person,
getName: function () {
console.log(this.name);
}
};

var person1 = new Person();

优点:该共享的共享,该私有的私有,使用最广泛的方式

缺点:有的人就是希望全部都写在一起,即更好的封装性

4.1 动态原型模式

1
2
3
4
5
6
7
8
9
10
复制代码function Person(name) {
this.name = name;
if (typeof this.getName != "function") {
Person.prototype.getName = function () {
console.log(this.name);
}
}
}

var person1 = new Person();

注意:使用动态原型模式时,不能用对象字面量重写原型

解释下为什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码function Person(name) {
this.name = name;
if (typeof this.getName != "function") {
Person.prototype = {
constructor: Person,
getName: function () {
console.log(this.name);
}
}
}
}

var person1 = new Person('kevin');
var person2 = new Person('daisy');

// 报错 并没有该方法
person1.getName();

// 注释掉上面的代码,这句是可以执行的。
person2.getName();

为了解释这个问题,假设开始执行var person1 = new Person('kevin')。

如果对 new 和 apply 的底层执行过程不是很熟悉,可以阅读底部相关链接中的文章。

我们回顾下 new 的实现步骤:

  1. 首先新建一个对象
  2. 然后将对象的原型指向 Person.prototype
  3. 然后 Person.apply(obj)
  4. 返回这个对象

注意这个时候,回顾下 apply 的实现步骤,会执行 obj.Person 方法,这个时候就会执行 if 语句里的内容,注意构造函数的 prototype 属性指向了实例的原型,使用字面量方式直接覆盖 Person.prototype,并不会更改实例的原型的值,person1 依然是指向了以前的原型,而不是 Person.prototype。而之前的原型是没有 getName 方法的,所以就报错了!

如果你就是想用字面量方式写代码,可以尝试下这种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码function Person(name) {
this.name = name;
if (typeof this.getName != "function") {
Person.prototype = {
constructor: Person,
getName: function () {
console.log(this.name);
}
}

return new Person(name);
}
}

var person1 = new Person('kevin');
var person2 = new Person('daisy');

person1.getName(); // kevin
person2.getName(); // daisy

5.1 寄生构造函数模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码function Person(name) {

var o = new Object();
o.name = name;
o.getName = function () {
console.log(this.name);
};

return o;

}

var person1 = new Person('kevin');
console.log(person1 instanceof Person) // false
console.log(person1 instanceof Object) // true

寄生构造函数模式,我个人认为应该这样读:

寄生-构造函数-模式,也就是说寄生在构造函数的一种方法。

也就是说打着构造函数的幌子挂羊头卖狗肉,你看创建的实例使用 instanceof 都无法指向构造函数!

这样方法可以在特殊情况下使用。比如我们想创建一个具有额外方法的特殊数组,但是又不想直接修改Array构造函数,我们可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码function SpecialArray() {
var values = new Array();

for (var i = 0, len = arguments.length; i < len; i++) {
values.push(arguments[i]);
}

values.toPipedString = function () {
return this.join("|");
};
return values;
}

var colors = new SpecialArray('red', 'blue', 'green');
var colors2 = SpecialArray('red2', 'blue2', 'green2');


console.log(colors);
console.log(colors.toPipedString()); // red|blue|green

console.log(colors2);
console.log(colors2.toPipedString()); // red2|blue2|green2

你会发现,其实所谓的寄生构造函数模式就是比工厂模式在创建对象的时候,多使用了一个new,实际上两者的结果是一样的。

但是作者可能是希望能像使用普通 Array 一样使用 SpecialArray,虽然把 SpecialArray 当成函数也一样能用,但是这并不是作者的本意,也变得不优雅。

在可以使用其他模式的情况下,不要使用这种模式。

但是值得一提的是,上面例子中的循环:

1
2
3
复制代码for (var i = 0, len = arguments.length; i < len; i++) {
values.push(arguments[i]);
}

可以替换成:

1
复制代码values.push.apply(values, arguments);

5.2 稳妥构造函数模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码function person(name){
var o = new Object();
o.sayName = function(){
console.log(name);
};
return o;
}

var person1 = person('kevin');

person1.sayName(); // kevin

person1.name = "daisy";

person1.sayName(); // kevin

console.log(person1.name); // daisy

所谓稳妥对象,指的是没有公共属性,而且其方法也不引用 this 的对象。

与寄生构造函数模式有两点不同:

  1. 新创建的实例方法不引用 this
  2. 不使用 new 操作符调用构造函数

稳妥对象最适合在一些安全的环境中。

稳妥构造函数模式也跟工厂模式一样,无法识别对象所属类型。

下一篇文章

JavaScript深入之继承的多种方式和优缺点

相关链接

《JavaScript深入之从原型到原型链》

《JavaScript深入之new的模拟实现》

《JavaScript深入之call和apply的模拟实现》

深入系列

JavaScript深入系列目录地址:github.com/mqyqingfeng…。

JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

JavaScript 深入之类数组对象与 arguments

发表于 2017-05-08

JavaScript深入系列第十三篇,讲解类数组对象与对象之间的相似与差异以及arguments的注意要点

类数组对象

所谓的类数组对象:

拥有一个 length 属性和若干索引属性的对象

举个例子:

1
2
3
4
5
6
7
8
复制代码var array = ['name', 'age', 'sex'];

var arrayLike = {
0: 'name',
1: 'age',
2: 'sex',
length: 3
}

即便如此,为什么叫做类数组对象呢?

那让我们从读写、获取长度、遍历三个方面看看这两个对象。

读写

1
2
3
4
5
复制代码console.log(array[0]); // name
console.log(arrayLike[0]); // name

array[0] = 'new name';
arrayLike[0] = 'new name';

长度

1
2
复制代码console.log(array.length); // 3
console.log(arrayLike.length); // 3

遍历

1
2
3
4
5
6
复制代码for(var i = 0, len = array.length; i < len; i++) {
……
}
for(var i = 0, len = arrayLike.length; i < len; i++) {
……
}

是不是很像?

那类数组对象可以使用数组的方法吗?比如:

1
复制代码arrayLike.push('4');

然而上述代码会报错: arrayLike.push is not a function

所以终归还是类数组呐……

调用数组方法

如果类数组就是任性的想用数组的方法怎么办呢?

既然无法直接调用,我们可以用 Function.call 间接调用:

1
2
3
4
5
6
7
8
9
10
11
复制代码var arrayLike = {0: 'name', 1: 'age', 2: 'sex', length: 3 }

Array.prototype.join.call(arrayLike, '&'); // name&age&sex

Array.prototype.slice.call(arrayLike, 0); // ["name", "age", "sex"]
// slice可以做到类数组转数组

Array.prototype.map.call(arrayLike, function(item){
return item.toUpperCase();
});
// ["NAME", "AGE", "SEX"]

类数组转对象

在上面的例子中已经提到了一种类数组转数组的方法,再补充三个:

1
2
3
4
5
6
7
8
9
复制代码var arrayLike = {0: 'name', 1: 'age', 2: 'sex', length: 3 }
// 1. slice
Array.prototype.slice.call(arrayLike); // ["name", "age", "sex"]
// 2. splice
Array.prototype.splice.call(arrayLike, 0); // ["name", "age", "sex"]
// 3. ES6 Array.from
Array.from(arrayLike); // ["name", "age", "sex"]
// 4. apply
Array.prototype.concat.apply([], arrayLike)

那么为什么会讲到类数组对象呢?以及类数组有什么应用吗?

要说到类数组对象,Arguments 对象就是一个类数组对象。在客户端 JavaScript 中,一些 DOM 方法(document.getElementsByTagName()等)也返回类数组对象。

Arguments对象

接下来重点讲讲 Arguments 对象。

Arguments 对象只定义在函数体中,包括了函数的参数和其他属性。在函数体中,arguments 指代该函数的 Arguments 对象。

举个例子:

1
2
3
4
5
复制代码function foo(name, age, sex) {
console.log(arguments);
}

foo('name', 'age', 'sex')

打印结果如下:

arguments

我们可以看到除了类数组的索引属性和length属性之外,还有一个callee属性,接下来我们一个一个介绍。

length属性

Arguments对象的length属性,表示实参的长度,举个例子:

1
2
3
4
5
6
7
8
9
10
复制代码function foo(b, c, d){
console.log("实参的长度为:" + arguments.length)
}

console.log("形参的长度为:" + foo.length)

foo(1)

// 形参的长度为:3
// 实参的长度为:1

callee属性

Arguments 对象的 callee 属性,通过它可以调用函数自身。

讲个闭包经典面试题使用 callee 的解决方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码var data = [];

for (var i = 0; i < 3; i++) {
(data[i] = function () {
console.log(arguments.callee.i)
}).i = i;
}

data[0]();
data[1]();
data[2]();

// 0
// 1
// 2

接下来讲讲 arguments 对象的几个注意要点:

arguments 和对应参数的绑定

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
复制代码function foo(name, age, sex, hobbit) {

console.log(name, arguments[0]); // name name

// 改变形参
name = 'new name';

console.log(name, arguments[0]); // new name new name

// 改变arguments
arguments[1] = 'new age';

console.log(age, arguments[1]); // new age new age

// 测试未传入的是否会绑定
console.log(sex); // undefined

sex = 'new sex';

console.log(sex, arguments[2]); // new sex undefined

arguments[3] = 'new hobbit';

console.log(hobbit, arguments[3]); // undefined new hobbit

}

foo('name', 'age')

传入的参数,实参和 arguments 的值会共享,当没有传入时,实参与 arguments 值不会共享

除此之外,以上是在非严格模式下,如果是在严格模式下,实参和 arguments 是不会共享的。

传递参数

将参数从一个函数传递到另一个函数

1
2
3
4
5
6
7
8
9
复制代码// 使用 apply 将 foo 的参数传递给 bar
function foo() {
bar.apply(this, arguments);
}
function bar(a, b, c) {
console.log(a, b, c);
}

foo(1, 2, 3)

强大的ES6

使用ES6的 … 运算符,我们可以轻松转成数组。

1
2
3
4
5
复制代码function func(...arguments) {
console.log(arguments); // [1, 2, 3]
}

func(1, 2, 3);

应用

arguments的应用其实很多,在下个系列,也就是 JavaScript 专题系列中,我们会在 jQuery 的 extend 实现、函数柯里化、递归等场景看见 arguments 的身影。这篇文章就不具体展开了。

如果要总结这些场景的话,暂时能想到的包括:

  1. 参数不定长
  2. 函数柯里化
  3. 递归调用
  4. 函数重载
    …

欢迎留言回复。

下一篇文章

JavaScript深入之创建对象的多种方式以及优缺点

深入系列

JavaScript深入系列目录地址:github.com/mqyqingfeng…。

JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

JavaScript深入之new的模拟实现

发表于 2017-05-04

JavaScript深入系列第十二篇,通过new的模拟实现,带大家揭开使用new获得构造函数实例的真相

new

一句话介绍 new:

new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象类型之一

也许有点难懂,我们在模拟 new 之前,先看看 new 实现了哪些功能。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码// Otaku 御宅族,简称宅
function Otaku (name, age) {
this.name = name;
this.age = age;

this.habit = 'Games';
}

// 因为缺乏锻炼的缘故,身体强度让人担忧
Otaku.prototype.strength = 60;

Otaku.prototype.sayYourName = function () {
console.log('I am ' + this.name);
}

var person = new Otaku('Kevin', '18');

console.log(person.name) // Kevin
console.log(person.habit) // Games
console.log(person.strength) // 60

person.sayYourName(); // I am Kevin

从这个例子中,我们可以看到,实例 person 可以:

  1. 访问到 Otaku 构造函数里的属性
  2. 访问到 Otaku.prototype 中的属性

接下来,我们可以尝试着模拟一下了。

因为 new 是关键字,所以无法像 bind 函数一样直接覆盖,所以我们写一个函数,命名为 objectFactory,来模拟 new 的效果。用的时候是这样的:

1
2
3
4
5
6
7
8
复制代码function Otaku () {
……
}

// 使用 new
var person = new Otaku(……);
// 使用 objectFactory
var person = objectFactory(Otaku, ……)

初步实现

分析:

因为 new 的结果是一个新对象,所以在模拟实现的时候,我们也要建立一个新对象,假设这个对象叫 obj,因为 obj 会具有 Otaku 构造函数里的属性,想想经典继承的例子,我们可以使用 Otaku.apply(obj, arguments)来给 obj 添加新的属性。

在 JavaScript 深入系列第一篇中,我们便讲了原型与原型链,我们知道实例的 __proto__ 属性会指向构造函数的 prototype,也正是因为建立起这样的关系,实例可以访问原型上的属性。

现在,我们可以尝试着写第一版了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码// 第一版代码
function objectFactory() {

var obj = new Object(),

Constructor = [].shift.call(arguments);

obj.__proto__ = Constructor.prototype;

Constructor.apply(obj, arguments);

return obj;

};

在这一版中,我们:

  1. 用new Object() 的方式新建了一个对象 obj
  2. 取出第一个参数,就是我们要传入的构造函数。此外因为 shift 会修改原数组,所以 arguments 会被去除第一个参数
  3. 将 obj 的原型指向构造函数,这样 obj 就可以访问到构造函数原型中的属性
  4. 使用 apply,改变构造函数 this 的指向到新建的对象,这样 obj 就可以访问到构造函数中的属性
  5. 返回 obj

更多关于:

原型与原型链,可以看《JavaScript深入之从原型到原型链》

apply,可以看《JavaScript深入之call和apply的模拟实现》

经典继承,可以看《JavaScript深入之继承》

复制以下的代码,到浏览器中,我们可以做一下测试:

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
复制代码function Otaku (name, age) {
this.name = name;
this.age = age;

this.habit = 'Games';
}

Otaku.prototype.strength = 60;

Otaku.prototype.sayYourName = function () {
console.log('I am ' + this.name);
}

function objectFactory() {
var obj = new Object(),
Constructor = [].shift.call(arguments);
obj.__proto__ = Constructor.prototype;
Constructor.apply(obj, arguments);
return obj;
};

var person = objectFactory(Otaku, 'Kevin', '18')

console.log(person.name) // Kevin
console.log(person.habit) // Games
console.log(person.strength) // 60

person.sayYourName(); // I am Kevin

[]( ̄▽ ̄)**

返回值效果实现

接下来我们再来看一种情况,假如构造函数有返回值,举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码function Otaku (name, age) {
this.strength = 60;
this.age = age;

return {
name: name,
habit: 'Games'
}
}

var person = new Otaku('Kevin', '18');

console.log(person.name) // Kevin
console.log(person.habit) // Games
console.log(person.strength) // undefined
console.log(person.age) // undefined

在这个例子中,构造函数返回了一个对象,在实例 person 中只能访问返回的对象中的属性。

而且还要注意一点,在这里我们是返回了一个对象,假如我们只是返回一个基本类型的值呢?

再举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码function Otaku (name, age) {
this.strength = 60;
this.age = age;

return 'handsome boy';
}

var person = new Otaku('Kevin', '18');

console.log(person.name) // undefined
console.log(person.habit) // undefined
console.log(person.strength) // 60
console.log(person.age) // 18

结果完全颠倒过来,这次尽管有返回值,但是相当于没有返回值进行处理。

所以我们还需要判断返回的值是不是一个对象,如果是一个对象,我们就返回这个对象,如果没有,我们该返回什么就返回什么。

再来看第二版的代码,也是最后一版的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码// 第二版的代码
function objectFactory() {

var obj = new Object(),

Constructor = [].shift.call(arguments);

obj.__proto__ = Constructor.prototype;

var ret = Constructor.apply(obj, arguments);

return typeof ret === 'object' ? ret : obj;

};

下一篇文章

JavaScript深入之类数组对象与arguments

相关链接

《JavaScript深入之从原型到原型链》

《JavaScript深入之call和apply的模拟实现》

《JavaScript深入之继承》

深入系列

JavaScript深入系列目录地址:github.com/mqyqingfeng…。

JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

JavaScript字符串所有API全解密

发表于 2017-05-03

关于

  • 我的博客:louis blog
  • SF专栏:路易斯前端深度课
  • 原文链接:JavaScript字符串所有API全解密

本文近 6k 字,读完需 10 分钟。

字符串作为基本的信息交流的桥梁,几乎被所有的编程语言所实现(然而c、c++没有提供)。多数开发者几乎每天都在和字符串打交道,语言内置的String模块,极大地提升了开发者的效率。JavaScript通过自动装箱字符串字面量为String对象,自然地继承了String.prototype的所有方法,更加简化了字符串的使用。

截止ES6,字符串共包含31个标准的API方法,其中有些方法出镜率较高,需要摸透原理;有些方法之间相似度较高,需要仔细分辨;甚至有些方法执行效率较低,应当尽量少的使用。下面将从String构造器方法说起,逐步帮助你掌握字符串。

String构造器方法

fromCharCode

fromCharCode() 方法返回使用指定的Unicode序列创建的字符串,也就是说传入Unicode序列,返回基于此创建的字符串。

语法:fromCharCode(num1, num2,…),传入的参数均为数字。

如下这是一个简单的例子,将返回 ABC、abc、*、+、- 和 /:

1
2
3
4
5
6
复制代码String.fromCharCode(65, 66, 67); // "ABC"
String.fromCharCode(97, 98, 99); // "abc"
String.fromCharCode(42); // "*"
String.fromCharCode(43); // "+"
String.fromCharCode(45); // "-"
String.fromCharCode(47); // "/"

看起来fromCharCode像是满足了需求,但实际上由于js语言设计时的先天不足(只能处理UCS-2编码,即所有字符都是2个字节,无法处理4字节字符),通过该方法并不能返回一个4字节的字符,为了弥补这个缺陷,ES6新增了fromCodePoint方法,请往下看。

fromCodePoint(ES6)

fromCodePoint() 方法基于ECMAScript 2015(ES6)规范,作用和语法同fromCharCode方法,该方法主要扩展了对4字节字符的支持。

1
2
3
4
5
6
复制代码// "𝌆" 是一个4字节字符,我们先看下它的数字形式
"𝌆".codePointAt(); // 119558
//调用fromCharCode解析之,返回乱码
String.fromCharCode(119558); // "팆"
//调用fromCodePoint解析之,正常解析
String.fromCodePoint(119558); // "𝌆"

除了扩展对4字节的支持外,fromCodePoint还规范了错误处理,只要是无效的Unicode编码,就会抛出错误RangeError: Invalid code point...,这就意味着,只要不是符合Unicode字符范围的正整数(Unicode最多可容纳1114112个码位),均会抛出错误。

1
2
3
复制代码String.fromCodePoint('abc'); // RangeError: Invalid code point NaN
String.fromCodePoint(Infinity); // RangeError: Invalid code point Infinity
String.fromCodePoint(-1.23); // RangeError: Invalid code point -1.23

注:进一步了解Unicode编码,推荐这篇文章:浅谈文字编码和Unicode(下)。如需在老的浏览器中使用该方法,请参考Polyfill。

raw(ES6)

raw() 方法基于ECMAScript 2015(ES6)规范,它是一个模板字符串的标签函数,作用类似于Python的r和C#的@字符串前缀,都是用来获取一个模板字符串的原始字面量。

语法: String.raw(callSite, …substitutions),callSite即模板字符串的『调用点对象』,…substitutions表示任意个内插表达式对应的值,这里理解起来相当拗口,下面我将通俗的讲解它。

如下是python的字符串前缀r:

1
2
3
4
复制代码# 防止特殊字符串被转义
print r'a\nb\tc' # 打印出来依然是 "a\nb\tc"
# python中常用于正则表达式
regExp = r'(?<=123)[a-z]+'

如下是String.raw作为前缀的用法:

1
2
3
4
5
6
7
复制代码// 防止特殊字符串被转义
String.raw`a\nb\tc`; // 输出为 "a\nb\tc"
// 支持内插表达式
let name = "louis";
String.raw`Hello \n ${name}`; // "Hello \n louis"
// 内插表达式还可以运算
String.raw`1+2=${1+2},2*3=${2*3}`; // "1+2=3,2*3=6"

String.raw作为函数来调用的场景不太多,如下是用法:

1
2
3
4
复制代码// 对象的raw属性值为字符串时,从第二个参数起,它们分别被插入到下标为0,1,2,...n的元素后面
String.raw({raw: 'abcd'}, 1, 2, 3); // "a1b2c3d"
// 对象的raw属性值为数组时,从第二个参数起,它们分别被插入到数组下标为0,1,2,...n的元素后面
String.raw({raw: ['a', 'b', 'c', 'd']}, 1, 2, 3); // "a1b2c3d"

那么怎么解释String.raw函数按照下标挨个去插入的特性呢?MDN上有段描述如下:

In most cases, String.raw() is used with template strings. The first syntax mentioned above is only rarely used, because the JavaScript engine will call this with proper arguments for you, just like with other tag functions.

这意味着,String.raw作为函数调用时,基本与ES6的tag标签模板一样。如下:

1
2
3
4
5
6
7
8
9
复制代码// 如下是tag函数的实现
function tag(){
const array = arguments[0];
return array.reduce((p, v, i) => p + (arguments[i] || '') + v);
}
// 回顾一个simple的tag标签模板
tag`Hello ${ 2 + 3 } world ${ 2 * 3 }`; // "Hello 5 world 6"
// 其实就想当于如下调用
tag(['Hello ', ' world ', ''], 5, 6); // "Hello 5 world 6"

因此String.raw作为函数调用时,不论对象的raw属性值是字符串还是数组,插槽都是天生的,下标为0,1,2,…n的元素后面都是插槽(不包括最后一个元素)。实际上,它相当于是这样的tag函数:

1
2
3
4
5
6
7
复制代码function tag(){
const array = arguments[0].raw;
if(array === undefined || array === null){ // 这里可简写成 array == undefined
throw new TypeError('Cannot convert undefined or null to object');
}
return array.reduce((p, v, i) => p + (arguments[i] || '') + v);
}

实际上,String.raw作为函数调用时,若第一个参数不是一个符合标准格式的对象,执行将抛出TypeError错误。

1
复制代码String.raw({123: 'abcd'}, 1, 2, 3); // TypeError: Cannot convert undefined or null to object

目前只有Chrome v41+和Firefox v34+版本浏览器实现了该方法。

String.prototype

和其他所有对象一样,字符串实例的所有方法均来自String.prototype。以下是它的属性特性:

writable false
enumerable false
configurable false

可见,字符串属性不可编辑,任何试图改变它属性的行为都将抛出错误。

属性

String.prototype共有两个属性,如下:

  • String.prototype.constructor 指向构造器(String())
  • String.prototype.length 表示字符串长度

方法

字符串原型方法分为两种,一种是html无关的方法,一种是html有关的方法。我们先看第一种。但是无论字符串方法如何厉害,都不至于强大到可以改变原字符串。

HTML无关的方法

常用的方法有,charAt、charCodeAt、concat、indexOf、lastIndexOf、localeCompare、match、replace、search、slice、split、substr、substring、toLocaleLowerCase、toLocaleUpperCase、toLowerCase、toString、toUpperCase、trim、valueof 等ES5支持的,以及 codePointAt、contains、endsWith、normalize、repeat、startsWith 等ES6支持的,还包括 quote、toSource、trimLeft、trimRight 等非标准的。

接下来我们将对各个方法分别举例阐述其用法。若没有特别说明,默认该方法兼容所有目前主流浏览器。

charAt

charAt() 方法返回字符串中指定位置的字符。

语法:str.charAt(index)

index 为字符串索引(取值从0至length-1),如果超出该范围,则返回空串。

1
复制代码console.log("Hello, World".charAt(8)); // o, 返回下标为8的字符串o
charCodeAt

charCodeAt() 返回指定索引处字符的 Unicode 数值。

语法:str.charCodeAt(index)

index 为一个从0至length-1的整数。如果不是一个数值,则默认为 0,如果小于0或者大于字符串长度,则返回 NaN。

Unicode 编码单元(code points)的范围从 0 到 1,114,111。开头的 128 个 Unicode 编码单元和 ASCII 字符编码一样.

charCodeAt() 总是返回一个小于 65,536 的值。因为高位编码单元需要由一对字符来表示,为了查看其编码的完成字符,需要查看 charCodeAt(i) 以及 charCodeAt(i+1) 的值。如需更多了解请参考 fixedCharCodeAt。

1
2
复制代码console.log("Hello, World".charCodeAt(8)); // 111
console.log("前端工程师".charCodeAt(2)); // 24037, 可见也可以查看中文Unicode编码
concat

concat() 方法将一个或多个字符串拼接在一起,组成新的字符串并返回。

语法:str.concat(string2, string3, …)

1
复制代码console.log("早".concat("上","好")); // 早上好

但是 concat 的性能表现不佳,强烈推荐使用赋值操作符(+或+=)代替 concat。”+” 操作符大概快了 concat 几十倍。(数据参考 性能测试)。

indexOf
lastIndexOf

indexOf() 方法用于查找子字符串在字符串中首次出现的位置,没有则返回 -1。该方法严格区分大小写,并且从左往右查找。而 lastIndexOf 则从右往左查找,其它与前者一致。

语法:str.indexOf(searchValue [, fromIndex=0]),str.lastIndexOf(searchValue [, fromIndex=0])

searchValue 表示被查找的字符串,fromIndex 表示开始查找的位置,默认为0,如果小于0,则查找整个字符串,若超过字符串长度,则该方法返回-1,除非被查找的是空字符串,此时返回字符串长度。

1
2
3
复制代码console.log("".indexOf("",100)); // 0
console.log("IT改变世界".indexOf("世界")); // 4
console.log("IT改变世界".lastIndexOf("世界")); // 4
localeCompare

localeCompare() 方法用来比较字符串,如果指定字符串在原字符串的前面则返回负数,否则返回正数或0,其中0 表示两个字符串相同。该方法实现依赖具体的本地实现,不同的语言下可能有不同的返回。

语法:str.localeCompare(str2 [, locales [, options]])

1
2
3
4
复制代码var str = "apple";
var str2 = "orange";
console.log(str.localeCompare(str2)); // -1
console.log(str.localeCompare("123")); // 1

目前 Safari 浏览器暂不支持该方法,但Chrome v24+、Firefox v29+,IE11+ 和 Opera v15+ 都已实现了它。

match

match() 方法用于测试字符串是否支持指定正则表达式的规则,即使传入的是非正则表达式对象,它也会隐式地使用new RegExp(obj)将其转换为正则表达式对象。

语法:str.match(regexp)

该方法返回包含匹配结果的数组,如果没有匹配项,则返回 null。

描述

  • 若正则表达式没有 g 标志,则返回同 RegExp.exec(str) 相同的结果。而且返回的数组拥有一个额外的 input 属性,该属性包含原始字符串,另外该数组还拥有一个 index 属性,该属性表示匹配字符串在原字符串中索引(从0开始)。
  • 若正则表达式包含 g 标志,则该方法返回一个包含所有匹配结果的数组,没有匹配到则返回 null。

相关 RegExp 方法

  • 若需测试字符串是否匹配正则,请参考 RegExp.test(str)。
  • 若只需第一个匹配结果,请参考 RegExp.exec(str)。
1
2
3
4
5
6
复制代码var str = "World Internet Conference";
console.log(str.match(/[a-d]/i)); // ["d", index: 4, input: "World Internet Conference"]
console.log(str.match(/[a-d]/gi)); // ["d", "C", "c"]
// RegExp 方法如下
console.log(/[a-d]/gi.test(str)); // true
console.log(/[a-d]/gi.exec(str)); // ["d", index: 4, input: "World Internet Conference"]

由上可知,RegExp.test(str) 方法只要匹配到了一个字符也返回true。而RegExp.exec(str) 方法无论正则中有没有包含 g 标志,RegExp.exec将直接返回第一个匹配结果,且该结果同 str.match(regexp) 方法不包含 g 标志时的返回一致。

replace

该方法在之前已经讲过,详细请参考 String.prototype.replace高阶技能 。

search

search() 方法用于测试字符串对象是否包含某个正则匹配,相当于正则表达式的 test 方法,且该方法比 match() 方法更快。如果匹配成功,search() 返回正则表达式在字符串中首次匹配项的索引,否则返回-1。

注意:search方法与indexOf方法作用基本一致,都是查询到了就返回子串第一次出现的下标,否则返回-1,唯一的区别就在于search默认会将子串转化为正则表达式形式,而indexOf不做此处理,也不能处理正则。

语法:str.search(regexp)

1
2
复制代码var str = "abcdefg";
console.log(str.search(/[d-g]/)); // 3, 匹配到子串"defg",而d在原字符串中的索引为3

search() 方法不支持全局匹配(正则中包含g参数),如下:

1
复制代码console.log(str.search(/[d-g]/g)); // 3, 与无g参数时,返回相同
slice

slice() 方法提取字符串的一部分,并返回新的字符串。该方法有些类似 Array.prototype.slice 方法。

语法:str.slice(start, end)

首先 end 参数可选,start可取正值,也可取负值。

取正值时表示从索引为start的位置截取到end的位置(不包括end所在位置的字符,如果end省略则截取到字符串末尾)。

取负值时表示从索引为 length+start 位置截取到end所在位置的字符。

1
2
3
复制代码var str = "It is our choices that show what we truly are, far more than our abilities.";
console.log(str.slice(0,-30)); // It is our choices that show what we truly are
console.log(str.slice(-30)); // , far more than our abilities.
split

split() 方法把原字符串分割成子字符串组成数组,并返回该数组。

语法:str.split(separator, limit)

两个参数均是可选的,其中 separator 表示分隔符,它可以是字符串也可以是正则表达式。如果忽略 separator,则返回的数组包含一个由原字符串组成的元素。如果 separator 是一个空串,则 str 将会被分割成一个由原字符串中字符组成的数组。limit 表示从返回的数组中截取前 limit 个元素,从而限定返回的数组长度。

1
2
3
4
复制代码var str = "today is a sunny day";
console.log(str.split()); // ["today is a sunny day"]
console.log(str.split("")); // ["t", "o", "d", "a", "y", " ", "i", "s", " ", "a", " ", "s", "u", "n", "n", "y", " ", "d", "a", "y"]
console.log(str.split(" ")); // ["today", "is", "a", "sunny", "day"]

使用limit限定返回的数组大小,如下:

1
复制代码console.log(str.split(" ")); // ["today"]

使用正则分隔符(RegExp separator), 如下:

1
复制代码console.log(str.split(/\s*is\s*/)); // ["today", "a sunny day"]

若正则分隔符里包含捕获括号,则括号匹配的结果将会包含在返回的数组中。

1
复制代码console.log(str.split(/(\s*is\s*)/)); // ["today", " is ", "a sunny day"]
substr

substr() 方法返回字符串指定位置开始的指定数量的字符。

语法:str.substr(start[, length])

start 表示开始截取字符的位置,可取正值或负值。取正值时表示start位置的索引,取负值时表示 length+start位置的索引。

length 表示截取的字符长度。

1
2
3
复制代码var str = "Yesterday is history. Tomorrow is mystery. But today is a gift.";
console.log(str.substr(47)); // today is a gift.
console.log(str.substr(-16)); // today is a gift.

目前 Microsoft’s JScript 不支持 start 参数取负的索引,如需在 IE 下支持,请参考 Polyfill。

substring

substring() 方法返回字符串两个索引之间的子串。

语法:str.substring(indexA[, indexB])

indexA、indexB 表示字符串索引,其中 indexB 可选,如果省略,则表示返回从 indexA 到字符串末尾的子串。

描述

substring 要截取的是从 indexA 到 indexB(不包含)之间的字符,符合以下规律:

  • 若 indexA == indexB,则返回一个空字符串;
  • 若 省略 indexB,则提取字符一直到字符串末尾;
  • 若 任一参数小于 0 或 NaN,则被当作 0;
  • 若 任一参数大于 length,则被当作 length。

而 如果 indexA > indexB,则 substring 的执行效果就像是两个参数调换一般。比如:str.substring(0, 1) == str.substring(1, 0)

1
2
3
4
5
6
复制代码var str = "Get outside every day. Miracles are waiting everywhere.";
console.log(str.substring(1,1)); // ""
console.log(str.substring(0)); // Get outside every day. Miracles are waiting everywhere.
console.log(str.substring(-1)); // Get outside every day. Miracles are waiting everywhere.
console.log(str.substring(0,100)); // Get outside every day. Miracles are waiting everywhere.
console.log(str.substring(22,NaN)); // Get outside every day.
toLocaleLowerCase
toLocaleUpperCase

toLocaleLowerCase() 方法返回调用该方法的字符串被转换成小写的值,转换规则根据本地化的大小写映射。而toLocaleUpperCase() 方法则是转换成大写的值。

语法:str.toLocaleLowerCase(), str.toLocaleUpperCase()

1
2
复制代码console.log('ABCDEFG'.toLocaleLowerCase()); // abcdefg
console.log('abcdefg'.toLocaleUpperCase()); // ABCDEFG
toLowerCase
toUpperCase

这两个方法分别表示将字符串转换为相应的小写,大写形式,并返回。如下:

1
2
复制代码console.log('ABCDEFG'.toLowerCase()); // abcdefg
console.log('abcdefg'.toUpperCase()); // ABCDEFG
toString
valueOf

这两个方法都是返回字符串本身。

语法:str.toString(), str.valueOf()

1
2
3
复制代码var str = "abc";
console.log(str.toString()); // abc
console.log(str.toString()==str.valueOf()); // true

对于对象而言,toString和valueOf也是非常的相似,它们之间有着细微的差别,请尝试运行以下一段代码:

1
2
3
4
5
6
7
8
9
10
11
复制代码var x = {
toString: function () { return "test"; },
valueOf: function () { return 123; }
};

console.log(x); // test
console.log("x=" + x); // "x=123"
console.log(x + "=x"); // "123=x"
console.log(x + "1"); // 1231
console.log(x + 1); // 124
console.log(["x=", x].join("")); // "x=test"

当 “+” 操作符一边为数字时,对象x趋向于转换为数字,表达式会优先调用 valueOf 方法,如果调用数组的 join 方法,对象x趋向于转换为字符串,表达式会优先调用 toString 方法。

trim

trim() 方法清除字符串首尾的空白并返回。

语法:str.trim()

1
复制代码console.log(" a b c ".trim()); // "a b c"

trim() 方法是 ECMAScript 5.1 标准加入的,它并不支持IE9以下的低版本IE浏览器,如需支持,请参考以下兼容写法:

1
2
3
4
5
复制代码if(!String.prototype.trim) {
String.prototype.trim = function () {
return this.replace(/^\s+|\s+$/g,'');
};
}
codePointAt(ES6)

codePointAt() 方法基于ECMAScript 2015(ES6)规范,返回使用UTF-16编码的给定位置的值的非负整数。

语法:str.codePointAt(position)

1
2
复制代码console.log("a".codePointAt(0)); // 97
console.log("\u4f60\u597d".codePointAt(0)); // 20320

如需在老的浏览器中使用该方法,请参考 Polyfill。

includes(ES6)

includes() 方法基于ECMAScript 2015(ES6)规范,它用来判断一个字符串是否属于另一个字符。如果是,则返回true,否则返回false。

语法:str.includes(subString [, position])

subString 表示要搜索的字符串,position 表示从当前字符串的哪个位置开始搜索字符串,默认值为0。

1
2
3
复制代码var str = "Practice makes perfect.";
console.log(str.includes("perfect")); // true
console.log(str.includes("perfect",100)); // false

实际上,Firefox 18~39中该方法的名称为contains,由于bug 1102219的存在,它被重命名为includes() 。目前只有Chrome v41+和Firefox v40+版本浏览器实现了它,如需在其它版本浏览器中使用该方法,请参考 Polyfill。

endsWith(ES6)

endsWith() 方法基于ECMAScript 2015(ES6)规范,它基本与 contains() 功能相同,不同的是,它用来判断一个字符串是否是原字符串的结尾。若是则返回true,否则返回false。

语法:str.endsWith(substring [, position])

与contains 方法不同,position 参数的默认值为字符串长度。

1
2
3
复制代码var str = "Learn and live.";
console.log(str.endsWith("live.")); // true
console.log(str.endsWith("Learn",5)); // true

同样目前只有 Firefox v17+版本实现了该方法。其它浏览器请参考 Polyfill。

normalize(ES6)

normalize() 方法基于ECMAScript 2015(ES6)规范,它会按照指定的 Unicode 正规形式将原字符串正规化。

语法:str.normalize([form])

form 参数可省略,目前有四种 Unicode 正规形式,即 “NFC”、”NFD”、”NFKC” 以及 “NFKD”,form的默认值为 “NFC”。如果form 传入了非法的参数值,则会抛出 RangeError 错误。

1
2
3
4
5
6
复制代码var str = "\u4f60\u597d";
console.log(str.normalize()); // 你好
console.log(str.normalize("NFC")); // 你好
console.log(str.normalize("NFD")); // 你好
console.log(str.normalize("NFKC")); // 你好
console.log(str.normalize("NFKD")); // 你好

目前只有Chrome v34+和Firefox v31+实现了它。

repeat(ES6)

repeat() 方法基于ECMAScript 2015(ES6)规范,它返回重复原字符串多次的新字符串。

语法:str.repeat(count)

count 参数只能取大于等于0 的数字。若该数字不为整数,将自动转换为整数形式,若为负数或者其他值将报错。

1
2
3
4
5
复制代码var str = "A still tongue makes a wise head.";
console.log(str.repeat(0)); // ""
console.log(str.repeat(1)); // A still tongue makes a wise head.
console.log(str.repeat(1.5)); // A still tongue makes a wise head.
console.log(str.repeat(-1)); // RangeError:Invalid count value

目前只有 Chrome v41+、Firefox v24+和Safari v9+版本浏览器实现了该方法。其他浏览器请参考 Polyfill。

startsWith(ES6)

startsWith() 方法基于ECMAScript 2015(ES6)规范,它用来判断当前字符串是否是以给定字符串开始的,若是则返回true,否则返回false。

语法:str.startsWith(subString [, position])

1
2
3
复制代码var str = "Where there is a will, there is a way.";
console.log(str.startsWith("Where")); // true
console.log(str.startsWith("there",6)); // true

目前以下版本浏览器实现了该方法,其他浏览器请参考 Polyfill。

Chrome Firefox Edge Opera Safari
41+ 17+ ✔️ 28+ 9+

其它非标准的方法暂时不作介绍,如需了解请参考 String.prototype 中标注为感叹号的方法。

HTML有关的方法

常用的方法有 anchor,link 其它方法如 big、blink、bold、fixed、fontcolor、fontsize、italics、small、strike、sub、sup均已废除。

接下来我们将介绍 anchor 和 link 两个方法,其他废除方法不作介绍。

anchor

anchor() 方法创建一个锚标签。

语法:str.anchor(name)

name 指定被创建的a标签的name属性,使用该方法创建的锚点,将会成为 document.anchors 数组的元素。

1
2
复制代码var str = "this is a anchor tag";
document.body.innerHTML = document.body.innerHTML + str.anchor("anchor1"); // body末尾将会追加这些内容 <a name="anchor1">this is a anchor tag</a>
link

link() 方法同样创建一个a标签。

语法:str.link(url)

url 指定被创建的a标签的href属性,如果url中包含特殊字符,将自动进行编码。例如 " 会被转义为 &\quot。 使用该方法创建的a标签,将会成为 document.links 数组中的元素。

1
2
复制代码var str = "百度";
document.write(str.link("https://www.baidu.com")); // <a href="https://www.baidu.com">百度</a>

小结

部分字符串方法之间存在很大的相似性,要注意区分他们的功能和使用场景。如:

  • substr 和 substring,都是两个参数,作用基本相同,两者第一个参数含义相同,但用法不同,前者可为负数,后者值为负数或者非整数时将隐式转换为0。前者第二个参数表示截取字符串的长度,后者第二个参数表示截取字符串的下标;同时substring第一个参数大于第二个参数时,执行结果同位置调换后的结果。
  • search方法与indexOf方法作用基本一致,都是查询到了就返回子串第一次出现的下标,否则返回-1,唯一的区别就在于search默认会将子串转化为正则表达式形式,而indexOf不做此处理,也不能处理正则。

另外,还记得吗?concat方法由于效率问题,不推荐使用。

通常,字符串中,常用的方法就charAt、indexOf、lastIndexOf、match、replace、search、slice、split、substr、substring、toLowerCase、toUpperCase、trim、valueof 等这些。熟悉它们的语法规则就能熟练地驾驭字符串。


本文就讨论这么多内容,大家有什么问题或好的想法欢迎在下方参与留言和评论。

本文作者:louis

本文链接:louiszhai.github.io/2016/01/12/…

参考文章

  • ECMAScript® 2015 Language Specification
  • String.prototype
  • valueOf() vs. toString() in Javascript

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

JavaScript深入之bind的模拟实现

发表于 2017-05-03

JavaScript深入系列第十一篇,通过bind函数的模拟实现,带大家真正了解bind的特性

bind

一句话介绍 bind:

bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。(来自于 MDN )

由此我们可以首先得出 bind 函数的两个特点:

  1. 返回一个函数
  2. 可以传入参数

返回函数的模拟实现

从第一个特点开始,我们举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码var foo = {
value: 1
};

function bar() {
console.log(this.value);
}

// 返回了一个函数
var bindFoo = bar.bind(foo);

bindFoo(); // 1

关于指定 this 的指向,我们可以使用 call 或者 apply 实现,关于 call 和 apply 的模拟实现,可以查看《JavaScript深入之call和apply的模拟实现》。我们来写第一版的代码:

1
2
3
4
5
6
7
8
复制代码// 第一版
Function.prototype.bind2 = function (context) {
var self = this;
return function () {
self.apply(context);
}

}

传参的模拟实现

接下来看第二点,可以传入参数。这个就有点让人费解了,我在 bind 的时候,是否可以传参呢?我在执行 bind 返回的函数的时候,可不可以传参呢?让我们看个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码var foo = {
value: 1
};

function bar(name, age) {
console.log(this.value);
console.log(name);
console.log(age);

}

var bindFoo = bar.bind(foo, 'daisy');
bindFoo('18');
// 1
// daisy
// 18

函数需要传 name 和 age 两个参数,竟然还可以在 bind 的时候,只传一个 name,在执行返回的函数的时候,再传另一个参数 age!

这可咋办?不急,我们用 arguments 进行处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码// 第二版
Function.prototype.bind2 = function (context) {

var self = this;
// 获取bind2函数从第二个参数到最后一个参数
var args = Array.prototype.slice.call(arguments, 1);

return function () {
// 这个时候的arguments是指bind返回的函数传入的参数
var bindArgs = Array.prototype.slice.call(arguments);
self.apply(context, args.concat(bindArgs));
}

}

构造函数效果的模拟实现

完成了这两点,最难的部分到啦!因为 bind 还有一个特点,就是

一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。

也就是说当 bind 返回的函数作为构造函数的时候,bind 时指定的 this 值会失效,但传入的参数依然生效。举个例子:

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
复制代码var value = 2;

var foo = {
value: 1
};

function bar(name, age) {
this.habit = 'shopping';
console.log(this.value);
console.log(name);
console.log(age);
}

bar.prototype.friend = 'kevin';

var bindFoo = bar.bind(foo, 'daisy');

var obj = new bindFoo('18');
// undefined
// daisy
// 18
console.log(obj.habit);
console.log(obj.friend);
// shopping
// kevin

注意:尽管在全局和 foo 中都声明了 value 值,最后依然返回了 undefind,说明绑定的 this 失效了,如果大家了解 new 的模拟实现,就会知道这个时候的 this 已经指向了 obj。

(哈哈,我这是为我的下一篇文章《JavaScript深入系列之new的模拟实现》打广告)。

所以我们可以通过修改返回的函数的原型来实现,让我们写一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码// 第三版
Function.prototype.bind2 = function (context) {
var self = this;
var args = Array.prototype.slice.call(arguments, 1);

var fbound = function () {

var bindArgs = Array.prototype.slice.call(arguments);
// 当作为构造函数时,this 指向实例,self 指向绑定函数,因为下面一句 `fbound.prototype = this.prototype;`,已经修改了 fbound.prototype 为 绑定函数的 prototype,此时结果为 true,当结果为 true 的时候,this 指向实例。
// 当作为普通函数时,this 指向 window,self 指向绑定函数,此时结果为 false,当结果为 false 的时候,this 指向绑定的 context。
self.apply(this instanceof self ? this : context, args.concat(bindArgs));
}
// 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承函数的原型中的值
fbound.prototype = this.prototype;
return fbound;
}

如果对原型链稍有困惑,可以查看《JavaScript深入之从原型到原型链》。

构造函数效果的优化实现

但是在这个写法中,我们直接将 fbound.prototype = this.prototype,我们直接修改 fbound.prototype 的时候,也会直接修改函数的 prototype。这个时候,我们可以通过一个空函数来进行中转:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码// 第四版
Function.prototype.bind2 = function (context) {

var self = this;
var args = Array.prototype.slice.call(arguments, 1);

var fNOP = function () {};

var fbound = function () {
var bindArgs = Array.prototype.slice.call(arguments);
self.apply(this instanceof self ? this : context, args.concat(bindArgs));
}
fNOP.prototype = this.prototype;
fbound.prototype = new fNOP();
return fbound;

}

到此为止,大的问题都已经解决,给自己一个赞!o( ̄▽ ̄)d

三个小问题

接下来处理些小问题:

1.apply 这段代码跟 MDN 上的稍有不同

在 MDN 中文版讲 bind 的模拟实现时,apply 这里的代码是:

1
2
复制代码
self.apply(this instanceof self ? this : context || this, args.concat(bindArgs))

多了一个关于 context 是否存在的判断,然而这个是错误的!

举个例子:

1
2
3
4
5
6
7
8
9
10
11
复制代码var value = 2;
var foo = {
value: 1,
bar: bar.bind(null)
};

function bar() {
console.log(this.value);
}

foo.bar() // 2

以上代码正常情况下会打印 2,如果换成了 context || this,这段代码就会打印 1!

所以这里不应该进行 context 的判断,大家查看 MDN 同样内容的英文版,就不存在这个判断!

2.调用 bind 的不是函数咋办?

不行,我们要报错!

1
2
3
复制代码if (typeof this !== "function") {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}

3.我要在线上用

那别忘了做个兼容:

1
2
3
复制代码Function.prototype.bind = Function.prototype.bind || function () {
……
};

当然最好是用es5-shim啦。

最终代码

所以最最后的代码就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码Function.prototype.bind2 = function (context) {

if (typeof this !== "function") {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}

var self = this;
var args = Array.prototype.slice.call(arguments, 1);
var fNOP = function () {};

var fbound = function () {
self.apply(this instanceof self ? this : context, args.concat(Array.prototype.slice.call(arguments)));
}

fNOP.prototype = this.prototype;
fbound.prototype = new fNOP();

return fbound;

}

下一篇文章

《JavaScript深入系列之new的模拟实现》

相关链接

《JavaScript深入之从原型到原型链》

《JavaScript深入之call和apply的模拟实现》

《JavaScript深入系列之new的模拟实现》

深入系列

JavaScript深入系列目录地址:github.com/mqyqingfeng…。

JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

JavaScript深入之call和apply的模拟实现

发表于 2017-05-02

JavaScript深入系列第十篇,通过call和apply的模拟实现,带你揭开call和apply改变this的真相

call

一句话介绍 call:

call() 方法在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。

举个例子:

1
2
3
4
5
6
7
8
9
复制代码var foo = {
value: 1
};

function bar() {
console.log(this.value);
}

bar.call(foo); // 1

注意两点:

  1. call 改变了 this 的指向,指向到 foo
  2. bar 函数执行了

模拟实现第一步

那么我们该怎么模拟实现这两个效果呢?

试想当调用 call 的时候,把 foo 对象改造成如下:

1
2
3
4
5
6
7
8
复制代码var foo = {
value: 1,
bar: function() {
console.log(this.value)
}
};

foo.bar(); // 1

这个时候 this 就指向了 foo,是不是很简单呢?

但是这样却给 foo 对象本身添加了一个属性,这可不行呐!

不过也不用担心,我们用 delete 再删除它不就好了~

所以我们模拟的步骤可以分为:

  1. 将函数设为对象的属性
  2. 执行该函数
  3. 删除该函数

以上个例子为例,就是:

1
2
3
4
5
6
复制代码// 第一步
foo.fn = bar
// 第二步
foo.fn()
// 第三步
delete foo.fn

fn 是对象的属性名,反正最后也要删除它,所以起成什么都无所谓。

根据这个思路,我们可以尝试着去写第一版的 call2 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码// 第一版
Function.prototype.call2 = function(context) {
// 首先要获取调用call的函数,用this可以获取
context.fn = this;
context.fn();
delete context.fn;
}

// 测试一下
var foo = {
value: 1
};

function bar() {
console.log(this.value);
}

bar.call2(foo); // 1

正好可以打印 1 哎!是不是很开心!(~ ̄▽ ̄)~

模拟实现第二步

最一开始也讲了,call 函数还能给定参数执行函数。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码var foo = {
value: 1
};

function bar(name, age) {
console.log(name)
console.log(age)
console.log(this.value);
}

bar.call(foo, 'kevin', 18);
// kevin
// 18
// 1

注意:传入的参数并不确定,这可咋办?

不急,我们可以从 Arguments 对象中取值,取出第二个到最后一个参数,然后放到一个数组里。

比如这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码// 以上个例子为例,此时的arguments为:
// arguments = {
// 0: foo,
// 1: 'kevin',
// 2: 18,
// length: 3
// }
// 因为arguments是类数组对象,所以可以用for循环
var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}

// 执行后 args为 [foo, 'kevin', 18]

不定长的参数问题解决了,我们接着要把这个参数数组放到要执行的函数的参数里面去。

1
2
3
4
复制代码// 将数组里的元素作为多个参数放进函数的形参里
context.fn(args.join(','))
// (O_o)??
// 这个方法肯定是不行的啦!!!

也许有人想到用 ES6 的方法,不过 call 是 ES3 的方法,我们为了模拟实现一个 ES3 的方法,要用到ES6的方法,好像……,嗯,也可以啦。但是我们这次用 eval 方法拼成一个函数,类似于这样:

1
复制代码eval('context.fn(' + args +')')

这里 args 会自动调用 Array.toString() 这个方法。

所以我们的第二版克服了两个大问题,代码如下:

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
复制代码// 第二版
Function.prototype.call2 = function(context) {
context.fn = this;
var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
eval('context.fn(' + args +')');
delete context.fn;
}

// 测试一下
var foo = {
value: 1
};

function bar(name, age) {
console.log(name)
console.log(age)
console.log(this.value);
}

bar.call2(foo, 'kevin', 18);
// kevin
// 18
// 1

(๑•̀ㅂ•́)و✧

模拟实现第三步

模拟代码已经完成 80%,还有两个小点要注意:

1.this 参数可以传 null,当为 null 的时候,视为指向 window

举个例子:

1
2
3
4
5
6
7
8
复制代码
var value = 1;

function bar() {
console.log(this.value);
}

bar.call(null); // 1

虽然这个例子本身不使用 call,结果依然一样。

2.函数是可以有返回值的!

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码
var obj = {
value: 1
}

function bar(name, age) {
return {
value: this.value,
name: name,
age: age
}
}

console.log(bar.call(obj, 'kevin', 18));
// Object {
// value: 1,
// name: 'kevin',
// age: 18
// }

不过都很好解决,让我们直接看第三版也就是最后一版的代码:

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
30
31
32
33
34
35
36
37
38
39
40
41
复制代码// 第三版
Function.prototype.call2 = function (context) {
var context = context || window;
context.fn = this;

var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}

var result = eval('context.fn(' + args +')');

delete context.fn
return result;
}

// 测试一下
var value = 2;

var obj = {
value: 1
}

function bar(name, age) {
console.log(this.value);
return {
value: this.value,
name: name,
age: age
}
}

bar.call(null); // 2

console.log(bar.call2(obj, 'kevin', 18));
// 1
// Object {
// value: 1,
// name: 'kevin',
// age: 18
// }

到此,我们完成了 call 的模拟实现,给自己一个赞 b( ̄▽ ̄)d

apply的模拟实现

apply 的实现跟 call 类似,在这里直接给代码,代码来自于知乎 @郑航的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码Function.prototype.apply = function (context, arr) {
var context = Object(context) || window;
context.fn = this;

var result;
if (!arr) {
result = context.fn();
}
else {
var args = [];
for (var i = 0, len = arr.length; i < len; i++) {
args.push('arr[' + i + ']');
}
result = eval('context.fn(' + args + ')')
}

delete context.fn
return result;
}

下一篇文章

JavaScript深入之bind的模拟实现

重要参考

知乎问题 不能使用call、apply、bind,如何用 js 实现 call 或者 apply 的功能?

深入系列

JavaScript深入系列目录地址:github.com/mqyqingfeng…。

JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【深度长文】JavaScript数组所有API全解密

发表于 2017-04-28

本文首发于CSDN网站,下面的版本又经过进一步的修订。

关于

  • 我的博客:louis blog
  • SF专栏:路易斯前端深度课
  • 原文链接:【深度长文】JavaScript数组全解密

全文共13k+字,系统讲解了JavaScript数组的各种特性和API。

数组是一种非常重要的数据类型,它语法简单、灵活、高效。 在多数编程语言中,数组都充当着至关重要的角色,以至于很难想象没有数组的编程语言会是什么模样。特别是JavaScript,它天生的灵活性,又进一步发挥了数组的特长,丰富了数组的使用场景。可以豪不夸张地说,不深入地了解数组,不足以写JavaScript。

截止ES7规范,数组共包含33个标准的API方法和一个非标准的API方法,使用场景和使用方案纷繁复杂,其中有不少浅坑、深坑、甚至神坑。下面将从Array构造器及ES6新特性开始,逐步帮助你掌握数组。

声明:以下未特别标明的方法均为ES5已实现的方法。

Array构造器

Array构造器用于创建一个新的数组。通常,我们推荐使用对象字面量创建数组,这是一个好习惯,但是总有对象字面量乏力的时候,比如说,我想创建一个长度为8的空数组。请比较如下两种方式:

1
2
3
4
5
复制代码// 使用Array构造器
var a = Array(8); // [undefined × 8]
// 使用对象字面量
var b = [];
b.length = 8; // [undefined × 8]

Array构造器明显要简洁一些,当然你也许会说,对象字面量也不错啊,那么我保持沉默。

如上,我使用了Array(8)而不是new Array(8),这会有影响吗?实际上,并没有影响,这得益于Array构造器内部对this指针的判断,ELS5_HTML规范是这么说的:

When Array is called as a function rather than as a constructor, it creates and initialises a new Array object. Thus the function call Array(…) is equivalent to the object creation expression new Array(…) with the same arguments.

从规范来看,浏览器内部大致做了如下类似的实现:

1
2
3
4
5
6
复制代码function Array(){
// 如果this不是Array的实例,那就重新new一个实例
if(!(this instanceof arguments.callee)){
return new arguments.callee();
}
}

上面,我似乎跳过了对Array构造器语法的介绍,没事,接下来我补上。

Array构造器根据参数长度的不同,有如下两种不同的处理:

  • new Array(arg1, arg2,…),参数长度为0或长度大于等于2时,传入的参数将按照顺序依次成为新数组的第0至N项(参数长度为0时,返回空数组)。
  • new Array(len),当len不是数值时,处理同上,返回一个只包含len元素一项的数组;当len为数值时,根据如下规范,len最大不能超过32位无符号整型,即需要小于2的32次方(len最大为Math.pow(2,32) -1或-1>>>0),否则将抛出RangeError。

If the argument len is a Number and ToUint32(len) is equal to len, then the length property of the newly constructed object is set to ToUint32(len). If the argument len is a Number and ToUint32(len) is not equal to len, a RangeError exception is thrown.

以上,请注意Array构造器对于单个数值参数的特殊处理,如果仅仅需要使用数组包裹📦 若干参数,不妨使用Array.of,具体请移步下一节。

ES6新增的构造函数方法

鉴于数组的常用性,ES6专门扩展了数组构造器Array ,新增2个方法:Array.of、Array.from。下面展开来聊。

Array.of

Array.of用于将参数依次转化为数组中的一项,然后返回这个新数组,而不管这个参数是数字还是其它。它基本上与Array构造器功能一致,唯一的区别就在单个数字参数的处理上。如下:

1
2
复制代码Array.of(8.0); // [8]
Array(8.0); // [undefined × 8]

参数为多个,或单个参数不是数字时,Array.of 与 Array构造器等同。

1
2
3
4
5
复制代码Array.of(8.0, 5); // [8, 5]
Array(8.0, 5); // [8, 5]

Array.of('8'); // ["8"]
Array('8'); // ["8"]

因此,若是需要使用数组包裹元素,推荐优先使用Array.of方法。

目前,以下版本浏览器提供了对Array.of的支持。

Chrome Firefox Edge Safari
45+ 25+ ✔️ 9.0+

即使其他版本浏览器不支持也不必担心,由于Array.of与Array构造器的这种高度相似性,实现一个polyfill十分简单。如下:

1
2
3
4
5
复制代码if (!Array.of){
Array.of = function(){
return Array.prototype.slice.call(arguments);
};
}

Array.from

语法:Array.from(arrayLike[, processingFn[, thisArg]])

Array.from的设计初衷是快速便捷的基于其他对象创建新数组,准确来说就是从一个类似数组的可迭代对象创建一个新的数组实例,说人话就是,只要一个对象有迭代器,Array.from就能把它变成一个数组(当然,是返回新的数组,不改变原对象)。

从语法上看,Array.from拥有3个形参,第一个为类似数组的对象,必选。第二个为加工函数,新生成的数组会经过该函数的加工再返回。第三个为this作用域,表示加工函数执行时this的值。后两个参数都是可选的。我们来看看用法。

1
2
3
4
5
复制代码var obj = {0: 'a', 1: 'b', 2:'c', length: 3};
Array.from(obj, function(value, index){
console.log(value, index, this, arguments.length);
return value.repeat(3); //必须指定返回值,否则返回undefined
}, obj);

执行结果如下:

Array.from执行结果

可以看到加工函数的this作用域被obj对象取代,也可以看到加工函数默认拥有两个形参,分别为迭代器当前元素的值和其索引。

注意,一旦使用加工函数,必须明确指定返回值,否则将隐式返回undefined,最终生成的数组也会变成一个只包含若干个undefined元素的空数组。

实际上,如果不需要指定this,加工函数完全可以是一个箭头函数。上述代码可以简化如下:

1
复制代码Array.from(obj, (value) => value.repeat(3));

除了上述obj对象以外,拥有迭代器的对象还包括这些:String,Set,Map,arguments 等,Array.from统统可以处理。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码// String
Array.from('abc'); // ["a", "b", "c"]
// Set
Array.from(new Set(['abc', 'def'])); // ["abc", "def"]
// Map
Array.from(new Map([[1, 'abc'], [2, 'def']])); // [[1
, 'abc'], [2, 'def']]
// 天生的类数组对象arguments
function fn(){
return Array.from(arguments);
}
fn(1, 2, 3); // [1, 2, 3]

到这你可能以为Array.from就讲完了,实际上还有一个重要的扩展场景必须提下。比如说生成一个从0到指定数字的新数组,Array.from就可以轻易的做到。

1
复制代码Array.from({length: 10}, (v, i) => i); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

后面我们将会看到,利用数组的keys方法实现上述功能,可能还要简单一些。

目前,以下版本浏览器提供了对Array.from的支持。

Chrome Firefox Edge Opera Safari
45+ 32+ ✔️ ✔️ 9.0+

Array.isArray

顾名思义,Array.isArray用来判断一个变量是否数组类型。JS的弱类型机制导致判断变量类型是初级前端开发者面试时的必考题,一般我都会将其作为考察候选人第一题,然后基于此展开。在ES5提供该方法之前,我们至少有如下5种方式去判断一个值是否数组:

1
2
3
4
5
6
7
8
9
10
11
复制代码var a = [];
// 1.基于instanceof
a instanceof Array;
// 2.基于constructor
a.constructor === Array;
// 3.基于Object.prototype.isPrototypeOf
Array.prototype.isPrototypeOf(a);
// 4.基于getPrototypeOf
Object.getPrototypeOf(a) === Array.prototype;
// 5.基于Object.prototype.toString
Object.prototype.toString.apply(a) === '[object Array]';

以上,除了Object.prototype.toString外,其它方法都不能正确判断变量的类型。

要知道,代码的运行环境十分复杂,一个变量可能使用浑身解数去迷惑它的创造者。且看:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码var a = {
__proto__: Array.prototype
};
// 分别在控制台试运行以下代码
// 1.基于instanceof
a instanceof Array; // true
// 2.基于constructor
a.constructor === Array; // true
// 3.基于Object.prototype.isPrototypeOf
Array.prototype.isPrototypeOf(a); // true
// 4.基于getPrototypeOf
Object.getPrototypeOf(a) === Array.prototype; // true

以上,4种方法将全部返回true,为什么呢?我们只是手动指定了某个对象的__proto__属性为Array.prototype,便导致了该对象继承了Array对象,这种毫不负责任的继承方式,使得基于继承的判断方案瞬间土崩瓦解。

不仅如此,我们还知道,Array是堆数据,变量指向的只是它的引用地址,因此每个页面的Array对象引用的地址都是不一样的。iframe中声明的数组,它的构造函数是iframe中的Array对象。如果在iframe声明了一个数组x,将其赋值给父页面的变量y,那么在父页面使用y instanceof Array ,结果一定是false的。而最后一种返回的是字符串,不会存在引用问题。实际上,多页面或系统之间的交互只有字符串能够畅行无阻。

鉴于上述的两点原因,故笔者推荐使用最后一种方法去撩面试官(别提是我说的),如果你还不信,这里恰好有篇文章跟我持有相同的观点:Determining with absolute accuracy whether or not a JavaScript object is an array。

相反,使用Array.isArray则非常简单,如下:

1
2
复制代码Array.isArray([]); // true
Array.isArray({0: 'a', length: 1}); // false

目前,以下版本浏览器提供了对Array.isArray的支持。

Chrome Firefox IE Opera Safari
5+ 4+ 9+ 10.5+ 5+

实际上,通过Object.prototype.toString去判断一个值的类型,也是各大主流库的标准。因此Array.isArray的polyfill通常长这样:

1
2
3
4
5
复制代码if (!Array.isArray){
Array.isArray = function(arg){
return Object.prototype.toString.call(arg) === '[object Array]';
};
}

数组推导

ES6对数组的增强不止是体现在api上,还包括语法糖。比如说for of,它就是借鉴其它语言而成的语法糖,这种基于原数组使用for of生成新数组的语法糖,叫做数组推导。数组推导最初起早在ES6的草案中,但在第27版(2014年8月)中被移除,目前只有Firefox v30+支持,推导有风险,使用需谨慎。所幸如今这些语言都还支持推导:CoffeeScript、Python、Haskell、Clojure,我们可以从中一窥端倪。这里我们以python的for in推导打个比方:

1
2
3
复制代码# python for in 推导
a = [1, 2, 3, 4]
print [i * i for i in a if i == 3] # [9]

如下是SpiderMonkey引擎(Firefox)之前基于ES4规范实现的数组推导(与python的推导十分相似):

1
复制代码[i * i for (i of a)] // [1, 4, 9, 16]

ES6中数组有关的for of在ES4的基础上进一步演化,for关键字居首,in在中间,最后才是运算表达式。如下:

1
复制代码[for (i of [1, 2, 3, 4]) i * i] // [1, 4, 9, 16]

同python的示例,ES6中数组有关的for of也可以使用if语句:

1
2
3
4
复制代码// 单个if
[for (i of [1, 2, 3, 4]) if (i == 3) i * i] // [9]
// 甚至是多个if
[for (i of [1, 2, 3, 4]) if (i > 2) if (i < 4) i * i] // [9]

更为强大的是,ES6数组推导还允许多重for of。

1
复制代码[for (i of [1, 2, 3]) for (j of [10, 100]) i * j] // [10, 100, 20, 200, 30, 300]

甚至,数组推导还能够嵌入另一个数组推导中。

1
复制代码[for (i of [1, 2, 3]) [for (j of [10, 100]) i * j] ] // [[10, 100], [20, 200], [30, 300]]

对于上述两个表达式,前者和后者唯一的区别,就在于后者的第二个推导是先返回数组,然后与外部的推导再进行一次运算。

除了多个数组推导嵌套外,ES6的数组推导还会为每次迭代分配一个新的作用域(目前Firefox也没有为每次迭代创建新的作用域):

1
2
3
4
复制代码// ES6规范
[for (x of [0, 1, 2]) () => x][0]() // 0
// Firefox运行
[for (x of [0, 1, 2]) () => x][0]() // 2

通过上面的实例,我们看到使用数组推导来创建新数组比forEach,map,filter等遍历方法更加简洁,只是非常可惜,它不是标准规范。

ES6不仅新增了对Array构造器相关API,还新增了8个原型的方法。接下来我会在原型方法的介绍中穿插着ES6相关方法的讲解,请耐心往下读。

原型

继承的常识告诉我们,js中所有的数组方法均来自于Array.prototype,和其他构造函数一样,你可以通过扩展 Array 的 prototype 属性上的方法来给所有数组实例增加方法。

值得一说的是,Array.prototype本身就是一个数组。

1
2
复制代码Array.isArray(Array.prototype); // true
console.log(Array.prototype.length);// 0

以下方法可以进一步验证:

1
2
复制代码console.log([].__proto__.length);// 0
console.log([].__proto__);// [Symbol(Symbol.unscopables): Object]

有关Symbol(Symbol.unscopables)的知识,这里不做详述,具体请移步后续章节。

方法

数组原型提供的方法非常之多,主要分为三种,一种是会改变自身值的,一种是不会改变自身值的,另外一种是遍历方法。

由于 Array.prototype 的某些属性被设置为[[DontEnum]],因此不能用一般的方法进行遍历,我们可以通过如下方式获取 Array.prototype 的所有方法:

1
复制代码Object.getOwnPropertyNames(Array.prototype); // ["length", "constructor", "toString", "toLocaleString", "join", "pop", "push", "reverse", "shift", "unshift", "slice", "splice", "sort", "filter", "forEach", "some", "every", "map", "indexOf", "lastIndexOf", "reduce", "reduceRight", "copyWithin", "find", "findIndex", "fill", "includes", "entries", "keys", "concat"]

改变自身值的方法(9个)

基于ES6,改变自身值的方法一共有9个,分别为pop、push、reverse、shift、sort、splice、unshift,以及两个ES6新增的方法copyWithin 和 fill。

对于能改变自身值的数组方法,日常开发中需要特别注意,尽量避免在循环遍历中去改变原数组的项。接下来,我们一起来深入地了解这些方法。

pop

pop() 方法删除一个数组中的最后的一个元素,并且返回这个元素。如果是栈的话,这个过程就是栈顶弹出。

1
2
3
4
复制代码var array = ["cat", "dog", "cow", "chicken", "mouse"];
var item = array.pop();
console.log(array); // ["cat", "dog", "cow", "chicken"]
console.log(item); // mouse

由于设计上的巧妙,pop方法可以应用在类数组对象上,即 鸭式辨型. 如下:

1
2
3
4
复制代码var o = {0:"cat", 1:"dog", 2:"cow", 3:"chicken", 4:"mouse", length:5}
var item = Array.prototype.pop.call(o);
console.log(o); // Object {0: "cat", 1: "dog", 2: "cow", 3: "chicken", length: 4}
console.log(item); // mouse

但如果类数组对象不具有length属性,那么该对象将被创建length属性,length值为0。如下:

1
2
3
4
复制代码var o = {0:"cat", 1:"dog", 2:"cow", 3:"chicken", 4:"mouse"}
var item = Array.prototype.pop.call(o);
console.log(array); // Object {0: "cat", 1: "dog", 2: "cow", 3: "chicken", 4: "mouse", length: 0}
console.log(item); // undefined
push

push()方法添加一个或者多个元素到数组末尾,并且返回数组新的长度。如果是栈的话,这个过程就是栈顶压入。

语法:arr.push(element1, …, elementN)

1
2
3
4
复制代码var array = ["football", "basketball", "volleyball", "Table tennis", "badminton"];
var i = array.push("golfball");
console.log(array); // ["football", "basketball", "volleyball", "Table tennis", "badminton", "golfball"]
console.log(i); // 6

同pop方法一样,push方法也可以应用到类数组对象上,如果length不能被转成一个数值或者不存在length属性时,则插入的元素索引为0,且length属性不存在时,将会创建它。

1
2
3
4
复制代码var o = {0:"football", 1:"basketball"};
var i = Array.prototype.push.call(o, "golfball");
console.log(o); // Object {0: "golfball", 1: "basketball", length: 1}
console.log(i); // 1

实际上,push方法是根据length属性来决定从哪里开始插入给定的值。

1
2
3
4
复制代码var o = {0:"football", 1:"basketball",length:1};
var i = Array.prototype.push.call(o,"golfball");
console.log(o); // Object {0: "football", 1: "golfball", length: 2}
console.log(i); // 2

利用push根据length属性插入元素这个特点,可以实现数组的合并,如下:

1
2
3
4
5
复制代码var array = ["football", "basketball"];
var array2 = ["volleyball", "golfball"];
var i = Array.prototype.push.apply(array,array2);
console.log(array); // ["football", "basketball", "volleyball", "golfball"]
console.log(i); // 4
reverse

reverse()方法颠倒数组中元素的位置,第一个会成为最后一个,最后一个会成为第一个,该方法返回对数组的引用。

语法:arr.reverse()

1
2
3
4
复制代码var array = [1,2,3,4,5];
var array2 = array.reverse();
console.log(array); // [5,4,3,2,1]
console.log(array2===array); // true

同上,reverse 也是鸭式辨型的受益者,颠倒元素的范围受length属性制约。如下:

1
2
3
4
复制代码var o = {0:"a", 1:"b", 2:"c", length:2};
var o2 = Array.prototype.reverse.call(o);
console.log(o); // Object {0: "b", 1: "a", 2: "c", length: 2}
console.log(o === o2); // true

如果 length 属性小于2 或者 length 属性不为数值,那么原类数组对象将没有变化。即使 length 属性不存在,该对象也不会去创建 length 属性。特别的是,当 length 属性较大时,类数组对象的『索引』会尽可能的向 length 看齐。如下:

1
2
3
4
复制代码var o = {0:"a", 1:"b", 2:"c",length:100};
var o2 = Array.prototype.reverse.call(o);
console.log(o); // Object {97: "c", 98: "b", 99: "a", length: 100}
console.log(o === o2); // true
shift

shift()方法删除数组的第一个元素,并返回这个元素。

语法:arr.shift()

1
2
3
4
复制代码var array = [1,2,3,4,5];
var item = array.shift();
console.log(array); // [2,3,4,5]
console.log(item); // 1

同样受益于鸭式辨型,对于类数组对象,shift仍然能够处理。如下:

1
2
3
4
复制代码var o = {0:"a", 1:"b", 2:"c", length:3};
var item = Array.prototype.shift.call(o);
console.log(o); // Object {0: "b", 1: "c", length: 2}
console.log(item); // a

如果类数组对象length属性不存在,将添加length属性,并初始化为0。如下:

1
2
3
4
复制代码var o = {0:"a", 1:"b", 2:"c"};
var item = Array.prototype.shift.call(o);
console.log(o); // Object {0: "a", 1: "b", 2:"c" length: 0}
console.log(item); // undefined
sort

sort()方法对数组元素进行排序,并返回这个数组。sort方法比较复杂,这里我将多花些篇幅来讲这块。

语法:arr.sort([comparefn])

comparefn是可选的,如果省略,数组元素将按照各自转换为字符串的Unicode(万国码)位点顺序排序,例如”Boy”将排到”apple”之前。当对数字排序的时候,25将会排到8之前,因为转换为字符串后,”25”将比”8”靠前。例如:

1
2
3
4
5
6
7
8
复制代码var array = ["apple","Boy","Cat","dog"];
var array2 = array.sort();
console.log(array); // ["Boy", "Cat", "apple", "dog"]
console.log(array2 == array); // true

array = [10, 1, 3, 20];
var array3 = array.sort();
console.log(array3); // [1, 10, 20, 3]

如果指明了comparefn,数组将按照调用该函数的返回值来排序。若 a 和 b 是两个将要比较的元素:

  • 若 comparefn(a, b) < 0,那么a 将排到 b 前面;
  • 若 comparefn(a, b) = 0,那么a 和 b 相对位置不变;
  • 若 comparefn(a, b) > 0,那么a , b 将调换位置;

如果数组元素为数字,则排序函数comparefn格式如下所示:

1
2
3
复制代码function compare(a, b){
return a-b;
}

如果数组元素为非ASCII字符的字符串(如包含类似 e、é、è、a、ä 或中文字符等非英文字符的字符串),则需要使用String.localeCompare。下面这个函数将排到正确的顺序。

1
2
3
4
5
6
7
8
9
10
复制代码var array = ['互','联','网','改','变','世','界'];
var array2 = array.sort();

var array = ['互','联','网','改','变','世','界']; // 重新赋值,避免干扰array2
var array3 = array.sort(function (a, b) {
return a.localeCompare(b);
});

console.log(array2); // ["世", "互", "变", "改", "界", "网", "联"]
console.log(array3); // ["变", "改", "互", "界", "联", "世", "网"]

如上,『互联网改变世界』这个数组,sort函数默认按照数组元素unicode字符串形式进行排序,然而实际上,我们期望的是按照拼音先后顺序进行排序,显然String.localeCompare 帮助我们达到了这个目的。

为什么上面测试中需要重新给array赋值呢,这是因为sort每次排序时改变的是数组本身,并且返回数组引用。如果不这么做,经过连续两次排序后,array2 和 array3 将指向同一个数组,最终影响我们测试。array重新赋值后就断开了对原数组的引用。

同上,sort一样受益于鸭式辨型,比如:

1
2
3
4
5
复制代码var o = {0:'互',1:'联',2:'网',3:'改',4:'变',5:'世',6:'界',length:7};
Array.prototype.sort.call(o,function(a, b){
return a.localeCompare(b);
});
console.log(o); // Object {0: "变", 1: "改", 2: "互", 3: "界", 4: "联", 5: "世", 6: "网", length: 7}, 可见同上述排序结果一致

注意:使用sort的鸭式辨型特性时,若类数组对象不具有length属性,它并不会进行排序,也不会为其添加length属性。

1
2
3
4
5
复制代码var o = {0:'互',1:'联',2:'网',3:'改',4:'变',5:'世',6:'界'};
Array.prototype.sort.call(o,function(a, b){
return a.localeCompare(b);
});
console.log(o); // Object {0: "互", 1: "联", 2: "网", 3: "改", 4: "变", 5: "世", 6: "界"}, 可见并未添加length属性
使用映射改善排序

comparefn 如果需要对数组元素多次转换以实现排序,那么使用map辅助排序将是个不错的选择。基本思想就是将数组中的每个元素实际比较的值取出来,排序后再将数组恢复。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码// 需要被排序的数组
var array = ['dog', 'Cat', 'Boy', 'apple'];
// 对需要排序的数字和位置的临时存储
var mapped = array.map(function(el, i) {
return { index: i, value: el.toLowerCase() };
})
// 按照多个值排序数组
mapped.sort(function(a, b) {
return +(a.value > b.value) || +(a.value === b.value) - 1;
});
// 根据索引得到排序的结果
var result = mapped.map(function(el){
return array[el.index];
});
console.log(result); // ["apple", "Boy", "Cat", "dog"]
奇怪的chrome

实际上,ECMAscript规范中并未规定具体的sort算法,这就势必导致各个浏览器不尽相同的sort算法,请看sort方法在Chrome浏览器下表现:

1
2
3
4
5
6
7
8
复制代码var array = [{ n: "a", v: 1 }, { n: "b", v: 1 }, { n: "c", v: 1 }, { n: "d", v: 1 }, { n: "e", v: 1 }, { n: "f", v: 1 }, { n: "g", v: 1 }, { n: "h", v: 1 }, { n: "i", v: 1 }, { n: "j", v: 1 }, { n: "k", v: 1 }, ];
array.sort(function (a, b) {
return a.v - b.v;
});
for (var i = 0,len = array.length; i < len; i++) {
console.log(array[i].n);
}
// f a c d e b g h i j k

由于v值相等,array数组排序前后应该不变,然而Chrome却表现异常,而其他浏览器(如IE 或 Firefox) 表现正常。

这是因为v8引擎为了高效排序(采用了不稳定排序)。即数组长度超过10条时,会调用另一种排序方法(快速排序);而10条及以下采用的是插入排序,此时结果将是稳定的,如下:

1
2
3
4
5
6
7
8
复制代码var array = [{ n: "a", v: 1 }, { n: "b", v: 1 }, { n: "c", v: 1 }, { n: "d", v: 1 }, { n: "e", v: 1 }, { n: "f", v: 1 }, { n: "g", v: 1 }, { n: "h", v: 1 }, { n: "i", v: 1 }, { n: "j", v: 1 },];
array.sort(function (a, b) {
return a.v - b.v;
});
for (var i = 0,len = array.length; i < len; i++) {
console.log(array[i].n);
}
// a b c d e f g h i j

从a 到 j 刚好10条数据。

那么我们该如何规避Chrome浏览器的这种”bug”呢?其实很简单,只需略动手脚,改变排序方法的返回值即可,如下:

1
2
3
复制代码array.sort(function (a, b) {
return a.v - b.v || array.indexOf(a)-array.indexOf(b);
});

使用数组的sort方法需要注意一点:各浏览器的针对sort方法内部算法实现不尽相同,排序函数尽量只返回-1、0、1三种不同的值,不要尝试返回true或false等其它数值,因为可能导致不可靠的排序结果。

问题分析

sort方法传入的排序函数如果返回布尔值会导致什么样的结果呢?

以下是常见的浏览器以及脚本引擎:

Browser Name ECMAScript Engine
Internet Explorer 6 - 8 JScript
Internet Explorer 9 - 10 Chakra
Firefox SpiderMonkey, IonMonkey, TraceMonkey
Chrome V8
Safair JavaScriptCore(SquirrelFish Extreme)
Opera Carakan

分析以下代码,预期将数组元素进行升序排序:

1
2
3
4
5
复制代码var array = [7, 6, 5, 4, 3, 2, 1, 0, 8, 9];
var comparefn = function (x, y) {
return x > y;
};
array.sort(comparefn);

代码中,comparefn 函数返回值为 bool 类型,并非为规范规定的 -1、0、1 值。那么执行此代码,各 JS 脚本引擎实现情况如何?

输出结果 是否符合预期
JScript [2, 3, 5, 1, 4, 6, 7, 0, 8, 9] 否
Carakan [0, 1, 3, 8, 2, 4, 9, 5, 6, 7] 否
Chakra & JavaScriptCore [7, 6, 5, 4, 3, 2, 1, 0, 8, 9] 否
SpiderMonkey [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 是
V8 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 是

根据表中数据可见,当数组内元素个数小于等于 10 时,现象如下:

  • JScript & Carakan 排序结果有误
  • Chakra & JavaScriptCore 看起来没有进行排序
  • SpiderMonkey 返回了预期的正确结果
  • V8 暂时看起来排序正确

将数组元素扩大至 11 位,现象如下:

1
2
3
4
5
复制代码var array = [7, 6, 5, 4, 3, 2, 1, 0, 10, 9, 8];
var comparefn = function (x, y) {
return x > y;
};
array.sort(comparefn);
JavaScript引擎 输出结果 是否符合预期
JScript [2, 3, 5, 1, 4, 6, 7, 0, 8, 9, 10] 否
Carakan [0, 1, 3, 8, 2, 4, 9, 5, 10, 6, 7] 否
Chakra & JavaScriptCore [7, 6, 5, 4, 3, 2, 1, 0, 10, 8, 9] 否
IonMonkey [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 是
V8 [5, 0, 1, 2, 3, 4, 6, 7, 8, 9, 10] 否

根据表中数据可见,当数组内元素个数大于 10 时:

  • JScript & Carakan 排序结果有误
  • Chakra & JavaScriptCore 看起来没有进行排序
  • SpiderMonkey 返回了预期的正确结果
  • V8 排序结果由正确转为不正确
splice

splice()方法用新元素替换旧元素的方式来修改数组。它是一个常用的方法,复杂的数组操作场景通常都会有它的身影,特别是需要维持原数组引用时,就地删除或者新增元素,splice是最适合的。

语法:arr.splice(start,deleteCount[, item1[, item2[, …]]])

start 指定从哪一位开始修改内容。如果超过了数组长度,则从数组末尾开始添加内容;如果是负值,则其指定的索引位置等同于 length+start (length为数组的长度),表示从数组末尾开始的第 -start 位。

deleteCount 指定要删除的元素个数,若等于0,则不删除。这种情况下,至少应该添加一位新元素,若大于start之后的元素总和,则start及之后的元素都将被删除。

itemN 指定新增的元素,如果缺省,则该方法只删除数组元素。

返回值 由原数组中被删除元素组成的数组,如果没有删除,则返回一个空数组。

下面来举栗子说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码var array = ["apple","boy"];
var splices = array.splice(1,1);
console.log(array); // ["apple"]
console.log(splices); // ["boy"] ,可见是从数组下标为1的元素开始删除,并且删除一个元素,由于itemN缺省,故此时该方法只删除元素

array = ["apple","boy"];
splices = array.splice(2,1,"cat");
console.log(array); // ["apple", "boy", "cat"]
console.log(splices); // [], 可见由于start超过数组长度,此时从数组末尾开始添加元素,并且原数组不会发生删除行为

array = ["apple","boy"];
splices = array.splice(-2,1,"cat");
console.log(array); // ["cat", "boy"]
console.log(splices); // ["apple"], 可见当start为负值时,是从数组末尾开始的第-start位开始删除,删除一个元素,并且从此处插入了一个元素

array = ["apple","boy"];
splices = array.splice(-3,1,"cat");
console.log(array); // ["cat", "boy"]
console.log(splices); // ["apple"], 可见即使-start超出数组长度,数组默认从首位开始删除

array = ["apple","boy"];
splices = array.splice(0,3,"cat");
console.log(array); // ["cat"]
console.log(splices); // ["apple", "boy"], 可见当deleteCount大于数组start之后的元素总和时,start及之后的元素都将被删除

同上, splice一样受益于鸭式辨型, 比如:

1
2
3
4
复制代码var o = {0:"apple",1:"boy",length:2};
var splices = Array.prototype.splice.call(o,1,1);
console.log(o); // Object {0: "apple", length: 1}, 可见对象o删除了一个属性,并且length-1
console.log(splices); // ["boy"]

注意:如果类数组对象没有length属性,splice将为该类数组对象添加length属性,并初始化为0。(此处忽略举例,如果需要请在评论里反馈)

如果需要删除数组中一个已存在的元素,可参考如下:

1
2
复制代码var array = ['a','b','c'];
array.splice(array.indexOf('b'),1);
unshift

unshift() 方法用于在数组开始处插入一些元素(就像是栈底插入),并返回数组新的长度。

语法:arr.unshift(element1, …, elementN)

1
2
3
4
复制代码var array = ["red", "green", "blue"];
var length = array.unshift("yellow");
console.log(array); // ["yellow", "red", "green", "blue"]
console.log(length); // 4

如果给unshift方法传入一个数组呢?

1
2
3
4
复制代码var array = ["red", "green", "blue"];
var length = array.unshift(["yellow"]);
console.log(array); // [["yellow"], "red", "green", "blue"]
console.log(length); // 4, 可见数组也能成功插入

同上,unshift也受益于鸭式辨型,呈上栗子:

1
2
3
4
复制代码var o = {0:"red", 1:"green", 2:"blue",length:3};
var length = Array.prototype.unshift.call(o,"gray");
console.log(o); // Object {0: "gray", 1: "red", 2: "green", 3: "blue", length: 4}
console.log(length); // 4

注意:如果类数组对象不指定length属性,则返回结果是这样的 Object {0: "gray", 1: "green", 2: "blue", length: 1},shift会认为数组长度为0,此时将从对象下标为0的位置开始插入,相应位置属性将被替换,此时初始化类数组对象的length属性为插入元素个数。

copyWithin(ES6)

copyWithin() 方法基于ECMAScript 2015(ES6)规范,用于数组内元素之间的替换,即替换元素和被替换元素均是数组内的元素。

语法:arr.copyWithin(target, start[, end = this.length])

taget 指定被替换元素的索引,start 指定替换元素起始的索引,end 可选,指的是替换元素结束位置的索引。

如果start为负,则其指定的索引位置等同于length+start,length为数组的长度。end也是如此。

注:目前只有Firefox(版本32及其以上版本)实现了该方法。

1
2
3
4
5
6
7
8
9
复制代码var array = [1,2,3,4,5]; 
var array2 = array.copyWithin(0,3);
console.log(array===array2,array2); // true [4, 5, 3, 4, 5]

var array = [1,2,3,4,5];
console.log(array.copyWithin(0,3,4)); // [4, 2, 3, 4, 5]

var array = [1,2,3,4,5];
console.log(array.copyWithin(0,-2,-1)); // [4, 2, 3, 4, 5]

同上,copyWithin一样受益于鸭式辨型,例如:

1
2
3
复制代码var o = {0:1, 1:2, 2:3, 3:4, 4:5,length:5}
var o2 = Array.prototype.copyWithin.call(o,0,3);
console.log(o===o2,o2); // true Object { 0=4, 1=5, 2=3, 更多...}

如需在Firefox之外的浏览器使用copyWithin方法,请参考 Polyfill。

fill(ES6)

fill() 方法基于ECMAScript 2015(ES6)规范,它同样用于数组元素替换,但与copyWithin略有不同,它主要用于将数组指定区间内的元素替换为某个值。

语法:arr.fill(value, start[, end = this.length])

value 指定被替换的值,start 指定替换元素起始的索引,end 可选,指的是替换元素结束位置的索引。

如果start为负,则其指定的索引位置等同于length+start,length为数组的长度。end也是如此。

注:目前只有Firefox(版本31及其以上版本)实现了该方法。

1
2
3
4
复制代码var array = [1,2,3,4,5];
var array2 = array.fill(10,0,3);
console.log(array===array2,array2); // true [10, 10, 10, 4, 5], 可见数组区间[0,3]的元素全部替换为10
// 其他的举例请参考copyWithin

同上,fill 一样受益于鸭式辨型,例如:

1
2
3
复制代码var o = {0:1, 1:2, 2:3, 3:4, 4:5,length:5}
var o2 = Array.prototype.fill.call(o,10,0,2);
console.log(o===o2,o2); true Object { 0=10, 1=10, 2=3, 更多...}

如需在Firefox之外的浏览器使用fill方法,请参考 Polyfill。

不会改变自身的方法(9个)

基于ES7,不会改变自身的方法一共有9个,分别为concat、join、slice、toString、toLocateString、indexOf、lastIndexOf、未标准的toSource以及ES7新增的方法includes。

concat

concat() 方法将传入的数组或者元素与原数组合并,组成一个新的数组并返回。

语法:arr.concat(value1, value2, …, valueN)

1
2
3
4
复制代码var array = [1, 2, 3];
var array2 = array.concat(4,[5,6],[7,8,9]);
console.log(array2); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
console.log(array); // [1, 2, 3], 可见原数组并未被修改

若concat方法中不传入参数,那么将基于原数组浅复制生成一个一模一样的新数组(指向新的地址空间)。

1
2
3
4
5
复制代码var array = [{a: 1}];
var array3 = array.concat();
console.log(array3); // [{a: 1}]
console.log(array3 === array); // false
console.log(array[0] === array3[0]); // true,新旧数组第一个元素依旧共用一个同一个对象的引用

同上,concat 一样受益于鸭式辨型,但其效果可能达不到我们的期望,如下:

1
2
3
复制代码var o = {0:"a", 1:"b", 2:"c",length:3};
var o2 = Array.prototype.concat.call(o,'d',{3:'e',4:'f',length:2},['g','h','i']);
console.log(o2); // [{0:"a", 1:"b", 2:"c", length:3}, 'd', {3:'e', 4:'f', length:2}, 'g', 'h', 'i']

可见,类数组对象合并后返回的是依然是数组,并不是我们期望的对象。

join

join() 方法将数组中的所有元素连接成一个字符串。

语法:arr.join([separator = ‘,’]) separator可选,缺省默认为逗号。

1
2
3
4
复制代码var array = ['We', 'are', 'Chinese'];
console.log(array.join()); // "We,are,Chinese"
console.log(array.join('+')); // "We+are+Chinese"
console.log(array.join('')); // "WeareChinese"

同上,join 一样受益于鸭式辨型,如下:

1
2
3
复制代码var o = {0:"We", 1:"are", 2:"Chinese", length:3};
console.log(Array.prototype.join.call(o,'+')); // "We+are+Chinese"
console.log(Array.prototype.join.call('abc')); // "a,b,c"
slice

slice() 方法将数组中一部分元素浅复制存入新的数组对象,并且返回这个数组对象。

语法:arr.slice([start[, end]])

参数 start 指定复制开始位置的索引,end如果有值则表示复制结束位置的索引(不包括此位置)。

如果 start 的值为负数,假如数组长度为 length,则表示从 length+start 的位置开始复制,此时参数 end 如果有值,只能是比 start 大的负数,否则将返回空数组。

slice方法参数为空时,同concat方法一样,都是浅复制生成一个新数组。

1
2
3
复制代码var array = ["one", "two", "three","four", "five"];
console.log(array.slice()); // ["one", "two", "three","four", "five"]
console.log(array.slice(2,3)); // ["three"]

浅复制 是指当对象的被复制时,只是复制了对象的引用,指向的依然是同一个对象。下面来说明slice为什么是浅复制。

1
2
3
4
5
复制代码var array = [{color:"yellow"}, 2, 3];
var array2 = array.slice(0,1);
console.log(array2); // [{color:"yellow"}]
array[0]["color"] = "blue";
console.log(array2); // [{color:"bule"}]

由于slice是浅复制,复制到的对象只是一个引用,改变原数组array的值,array2也随之改变。

同时,稍微利用下 slice 方法第一个参数为负数时的特性,我们可以非常方便的拿到数组的最后一项元素,如下:

1
复制代码console.log([1,2,3].slice(-1));//[3]

同上,slice 一样受益于鸭式辨型。如下:

1
2
3
复制代码var o = {0:{"color":"yellow"}, 1:2, 2:3, length:3};
var o2 = Array.prototype.slice.call(o,0,1);
console.log(o2); // [{color:"yellow"}] ,毫无违和感...

鉴于IE9以下版本对于该方法支持性并不是很好,如需更好的支持低版本IE浏览器,请参考polyfill。

toString

toString() 方法返回数组的字符串形式,该字符串由数组中的每个元素的 toString() 返回值经调用 join() 方法连接(由逗号隔开)组成。

语法: arr.toString()

1
2
3
复制代码var array = ['Jan', 'Feb', 'Mar', 'Apr'];
var str = array.toString();
console.log(str); // Jan,Feb,Mar,Apr

当数组直接和字符串作连接操作时,将会自动调用其toString() 方法。

1
2
3
4
5
6
7
复制代码var str = ['Jan', 'Feb', 'Mar', 'Apr'] + ',May';
console.log(str); // "Jan,Feb,Mar,Apr,May"
// 下面我们来试试鸭式辨型
var o = {0:'Jan', 1:'Feb', 2:'Mar', length:3};
var o2 = Array.prototype.toString.call(o);
console.log(o2); // [object Object]
console.log(o.toString()==o2); // true

可见,Array.prototype.toString()方法处理类数组对象时,跟类数组对象直接调用Object.prototype.toString()方法结果完全一致,说好的鸭式辨型呢?

根据ES5语义,toString() 方法是通用的,可被用于任何对象。如果对象有一个join() 方法,将会被调用,其返回值将被返回,没有则调用Object.prototype.toString(),为此,我们给o对象添加一个join方法。如下:

1
2
3
4
5
6
7
8
9
10
复制代码var o = {
0:'Jan',
1:'Feb',
2:'Mar',
length:3,
join:function(){
return Array.prototype.join.call(this);
}
};
console.log(Array.prototype.toString.call(o)); // "Jan,Feb,Mar"
toLocaleString

toLocaleString() 类似toString()的变型,该字符串由数组中的每个元素的 toLocaleString() 返回值经调用 join() 方法连接(由逗号隔开)组成。

语法:arr.toLocaleString()

数组中的元素将调用各自的 toLocaleString 方法:

  • Object:Object.prototype.toLocaleString()
  • Number:Number.prototype.toLocaleString()
  • Date:Date.prototype.toLocaleString()
1
2
3
复制代码var array= [{name:'zz'}, 123, "abc", new Date()];
var str = array.toLocaleString();
console.log(str); // [object Object],123,abc,2016/1/5 下午1:06:23

其鸭式辨型的写法也同toString 保持一致,如下:

1
2
3
4
5
6
7
8
9
10
复制代码var o = {
0:123,
1:'abc',
2:new Date(),
length:3,
join:function(){
return Array.prototype.join.call(this);
}
};
console.log(Array.prototype.toLocaleString.call(o)); // 123,abc,2016/1/5 下午1:16:50
indexOf

indexOf() 方法用于查找元素在数组中第一次出现时的索引,如果没有,则返回-1。

语法:arr.indexOf(element, fromIndex=0)

element 为需要查找的元素。

fromIndex 为开始查找的位置,缺省默认为0。如果超出数组长度,则返回-1。如果为负值,假设数组长度为length,则从数组的第 length + fromIndex项开始往数组末尾查找,如果length + fromIndex<0 则整个数组都会被查找。

indexOf使用严格相等(即使用 === 去匹配数组中的元素)。

1
2
3
4
5
复制代码var array = ['abc', 'def', 'ghi','123'];
console.log(array.indexOf('def')); // 1
console.log(array.indexOf('def',-1)); // -1 此时表示从最后一个元素往后查找,因此查找失败返回-1
console.log(array.indexOf('def',-4)); // 1 由于4大于数组长度,此时将查找整个数组,因此返回1
console.log(array.indexOf(123)); // -1, 由于是严格匹配,因此并不会匹配到字符串'123'

得益于鸭式辨型,indexOf 可以处理类数组对象。如下:

1
2
复制代码var o = {0:'abc', 1:'def', 2:'ghi', length:3};
console.log(Array.prototype.indexOf.call(o,'ghi',-4));//2

然而该方法并不支持IE9以下版本,如需更好的支持低版本IE浏览器(IE6~8), 请参考 Polyfill。

lastIndexOf

lastIndexOf() 方法用于查找元素在数组中最后一次出现时的索引,如果没有,则返回-1。并且它是indexOf的逆向查找,即从数组最后一个往前查找。

语法:arr.lastIndexOf(element, fromIndex=length-1)

element 为需要查找的元素。

fromIndex 为开始查找的位置,缺省默认为数组长度length-1。如果超出数组长度,由于是逆向查找,则查找整个数组。如果为负值,则从数组的第 length + fromIndex项开始往数组开头查找,如果length + fromIndex<0 则数组不会被查找。

同 indexOf 一样,lastIndexOf 也是严格匹配数组元素。

举例请参考 indexOf ,不再详述,兼容低版本IE浏览器(IE6~8),请参考 Polyfill。

includes(ES7)

includes() 方法基于ECMAScript 2016(ES7)规范,它用来判断当前数组是否包含某个指定的值,如果是,则返回 true,否则返回 false。

语法:arr.includes(element, fromIndex=0)

element 为需要查找的元素。

fromIndex 表示从该索引位置开始查找 element,缺省为0,它是正向查找,即从索引处往数组末尾查找。

1
2
3
4
复制代码var array = [1, 2, NaN];
console.log(array.includes(1)); // true
console.log(array.includes(NaN)); // true
console.log(array.includes(2,-4)); // true

该方法同样受益于鸭式辨型。如下:

1
2
3
复制代码var o = {0:'a', 1:'b', 2:'c', length:3};
var bool = Array.prototype.includes.call(o, 'a');
console.log(bool); // true

该方法只有在Chrome 47、opera 34、Safari 9版本及其更高版本中才被实现。如需支持其他浏览器,请参考 Polyfill。

toSource

toSource() 方法是非标准的,该方法返回数组的源代码,目前只有 Firefox 实现了它。

语法:arr.toSource()

1
2
3
4
5
复制代码var array = ['a', 'b', 'c'];
console.log(array.toSource()); // ["a", "b", "c"]
// 测试鸭式辨型
var o = {0:'a', 1:'b', 2:'c', length:3};
console.log(Array.prototype.toSource.call(o)); // ["a","b","c"]

遍历方法(12个)

基于ES6,不会改变自身的方法一共有12个,分别为forEach、every、some、filter、map、reduce、reduceRight 以及ES6新增的方法entries、find、findIndex、keys、values。

forEach

forEach() 方法指定数组的每项元素都执行一次传入的函数,返回值为undefined。

语法:arr.forEach(fn, thisArg)

fn 表示在数组每一项上执行的函数,接受三个参数:

  • value 当前正在被处理的元素的值
  • index 当前元素的数组索引
  • array 数组本身

thisArg 可选,用来当做fn函数内的this对象。

forEach 将为数组中每一项执行一次 fn 函数,那些已删除,新增或者从未赋值的项将被跳过(但不包括值为 undefined 的项)。

遍历过程中,fn会被传入上述三个参数。

1
2
3
4
5
6
7
8
复制代码var array = [1, 3, 5];
var obj = {name:'cc'};
var sReturn = array.forEach(function(value, index, array){
array[index] = value * value;
console.log(this.name); // cc被打印了三次
},obj);
console.log(array); // [1, 9, 25], 可见原数组改变了
console.log(sReturn); // undefined, 可见返回值为undefined

得益于鸭式辨型,虽然forEach不能直接遍历对象,但它可以通过call方式遍历类数组对象。如下:

1
2
3
4
5
6
7
8
9
复制代码var o = {0:1, 1:3, 2:5, length:3};
Array.prototype.forEach.call(o,function(value, index, obj){
console.log(value,index,obj);
obj[index] = value * value;
},o);
// 1 0 Object {0: 1, 1: 3, 2: 5, length: 3}
// 3 1 Object {0: 1, 1: 3, 2: 5, length: 3}
// 5 2 Object {0: 1, 1: 9, 2: 5, length: 3}
console.log(o); // Object {0: 1, 1: 9, 2: 25, length: 3}

参考前面的文章 详解JS遍历 中 forEach的讲解,我们知道,forEach无法直接退出循环,只能使用return 来达到for循环中continue的效果,并且forEach不能在低版本IE(6~8)中使用,兼容写法请参考 Polyfill。

every

every() 方法使用传入的函数测试所有元素,只要其中有一个函数返回值为 false,那么该方法的结果为 false;如果全部返回 true,那么该方法的结果才为 true。因此 every 方法存在如下规律:

  • 若需检测数组中存在元素大于100 (即 one > 100),那么我们需要在传入的函数中构造 “false” 返回值 (即返回 item <= 100),同时整个方法结果为 false 才表示数组存在元素满足条件;(简单理解为:若是单项判断,可用 one false ===> false)
  • 若需检测数组中是否所有元素都大于100 (即all > 100)那么我们需要在传入的函数中构造 “true” 返回值 (即返回 item > 100),同时整个方法结果为 true 才表示数组所有元素均满足条件。(简单理解为:若是全部判断,可用 all true ===> true)

语法同上述forEach,具体还可以参考 详解JS遍历 中every的讲解。

以下是鸭式辨型的写法:

1
2
3
4
5
复制代码var o = {0:10, 1:8, 2:25, length:3};
var bool = Array.prototype.every.call(o,function(value, index, obj){
return value >= 8;
},o);
console.log(bool); // true

every 一样不能在低版本IE(6~8)中使用,兼容写法请参考 Polyfill。

some

some() 方法刚好同 every() 方法相反,some 测试数组元素时,只要有一个函数返回值为 true,则该方法返回 true,若全部返回 false,则该方法返回 false。some 方法存在如下规律:

  • 若需检测数组中存在元素大于100 (即 one > 100),那么我们需要在传入的函数中构造 “true” 返回值 (即返回 item > 100),同时整个方法结果为 true 才表示数组存在元素满足条件;(简单理解为:若是单项判断,可用 one true ===> true)
  • 若需检测数组中是否所有元素都大于100(即 all > 100),那么我们需要在传入的函数中构造 “false” 返回值 (即返回 item <= 100),同时整个方法结果为 false 才表示数组所有元素均满足条件。(简单理解为:若是全部判断,可用 all false ===> false)

你注意到没有,some方法与includes方法有着异曲同工之妙,他们都是探测数组中是否拥有满足条件的元素,一旦找到,便返回true。多观察和总结这种微妙的关联关系,能够帮助我们深入理解它们的原理。

some 的鸭式辨型写法可以参照every,同样它也不能在低版本IE(6~8)中使用,兼容写法请参考 Polyfill。

filter

filter() 方法使用传入的函数测试所有元素,并返回所有通过测试的元素组成的新数组。它就好比一个过滤器,筛掉不符合条件的元素。

语法:arr.filter(fn, thisArg)

1
2
3
4
5
复制代码var array = [18, 9, 10, 35, 80];
var array2 = array.filter(function(value, index, array){
return value > 20;
});
console.log(array2); // [35, 80]

filter一样支持鸭式辨型,具体请参考every方法鸭式辨型写法。其在低版本IE(6~8)的兼容写法请参考 Polyfill。

map

map() 方法遍历数组,使用传入函数处理每个元素,并返回函数的返回值组成的新数组。

语法:arr.map(fn, thisArg)

参数介绍同 forEach 方法的参数介绍。

具体用法请参考 详解JS遍历 中 map 的讲解。

map 一样支持鸭式辨型, 具体请参考every方法鸭式辨型写法。

其在低版本IE(6~8)的兼容写法请参考 Polyfill。

reduce

reduce() 方法接收一个方法作为累加器,数组中的每个值(从左至右) 开始合并,最终为一个值。

语法:arr.reduce(fn, initialValue)

fn 表示在数组每一项上执行的函数,接受四个参数:

  • previousValue 上一次调用回调返回的值,或者是提供的初始值
  • value 数组中当前被处理元素的值
  • index 当前元素在数组中的索引
  • array 数组自身

initialValue 指定第一次调用 fn 的第一个参数。

当 fn 第一次执行时:

  • 如果 initialValue 在调用 reduce 时被提供,那么第一个 previousValue 将等于 initialValue,此时 item 等于数组中的第一个值;
  • 如果 initialValue 未被提供,那么 previousVaule 等于数组中的第一个值,item 等于数组中的第二个值。此时如果数组为空,那么将抛出 TypeError。
  • 如果数组仅有一个元素,并且没有提供 initialValue,或提供了 initialValue 但数组为空,那么fn不会被执行,数组的唯一值将被返回。
1
2
3
4
5
6
7
复制代码var array = [1, 2, 3, 4];
var s = array.reduce(function(previousValue, value, index, array){
return previousValue * value;
},1);
console.log(s); // 24
// ES6写法更加简洁
array.reduce((p, v) => p * v); // 24

以上回调被调用4次,每次的参数和返回见下表:

callback previousValue currentValue index array return value
第1次 1 1 1 [1,2,3,4] 1
第2次 1 2 2 [1,2,3,4] 2
第3次 2 3 3 [1,2,3,4] 6
第4次 6 4 4 [1,2,3,4] 24

reduce 一样支持鸭式辨型,具体请参考every方法鸭式辨型写法。

其在低版本IE(6~8)的兼容写法请参考 Polyfill。

reduceRight

reduceRight() 方法接收一个方法作为累加器,数组中的每个值(从右至左)开始合并,最终为一个值。除了与reduce执行方向相反外,其他完全与其一致,请参考上述 reduce 方法介绍。

其在低版本IE(6~8)的兼容写法请参考 Polyfill。

entries(ES6)

entries() 方法基于ECMAScript 2015(ES6)规范,返回一个数组迭代器对象,该对象包含数组中每个索引的键值对。

语法:arr.entries()

1
2
3
4
5
6
复制代码var array = ["a", "b", "c"];
var iterator = array.entries();
console.log(iterator.next().value); // [0, "a"]
console.log(iterator.next().value); // [1, "b"]
console.log(iterator.next().value); // [2, "c"]
console.log(iterator.next().value); // undefined, 迭代器处于数组末尾时, 再迭代就会返回undefined

很明显,entries 也受益于鸭式辨型,如下:

1
2
3
4
5
复制代码var o = {0:"a", 1:"b", 2:"c", length:3};
var iterator = Array.prototype.entries.call(o);
console.log(iterator.next().value); // [0, "a"]
console.log(iterator.next().value); // [1, "b"]
console.log(iterator.next().value); // [2, "c"]

由于该方法基于ES6,因此目前并不支持所有浏览器,以下是各浏览器支持版本:

Browser Chrome Firefox (Gecko) Internet Explorer Opera Safari
Basic support 38 28 (28) 未实现 25 7.1
find&findIndex(ES6)

find() 方法基于ECMAScript 2015(ES6)规范,返回数组中第一个满足条件的元素(如果有的话), 如果没有,则返回undefined。

findIndex() 方法也基于ECMAScript 2015(ES6)规范,它返回数组中第一个满足条件的元素的索引(如果有的话)否则返回-1。

语法:arr.find(fn, thisArg),arr.findIndex(fn, thisArg)

我们发现它们的语法与forEach等十分相似,其实不光语法,find(或findIndex)在参数及其使用注意事项上,均与forEach一致。因此此处将略去 find(或findIndex)的参数介绍。下面我们来看个例子🌰 :

1
2
3
4
5
6
7
8
9
10
11
复制代码var array = [1, 3, 5, 7, 8, 9, 10];
function f(value, index, array){
return value%2==0; // 返回偶数
}
function f2(value, index, array){
return value > 20; // 返回大于20的数
}
console.log(array.find(f)); // 8
console.log(array.find(f2)); // undefined
console.log(array.findIndex(f)); // 4
console.log(array.findIndex(f2)); // -1

由于其鸭式辨型写法也与forEach方法一致,故此处略去。

兼容性上我没有详测,可以知道的是,最新版的Chrome v47,以及Firefox的版本25均实现了它们。

keys(ES6)

keys() 方法基于ECMAScript 2015(ES6)规范,返回一个数组索引的迭代器。(浏览器实际实现可能会有调整)

语法:arr.keys()

1
2
3
4
5
复制代码var array = ["abc", "xyz"];
var iterator = array.keys();
console.log(iterator.next()); // Object {value: 0, done: false}
console.log(iterator.next()); // Object {value: 1, done: false}
console.log(iterator.next()); // Object {value: undefined, done: false}

索引迭代器会包含那些没有对应元素的索引,如下:

1
2
3
4
5
复制代码var array = ["abc", , "xyz"];
var sparseKeys = Object.keys(array);
var denseKeys = [...array.keys()];
console.log(sparseKeys); // ["0", "2"]
console.log(denseKeys); // [0, 1, 2]

其鸭式辨型写法请参考上述 entries 方法。

前面我们用Array.from生成一个从0到指定数字的新数组,利用keys也很容易实现。

1
2
复制代码[...Array(10).keys()]; // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[...new Array(10).keys()]; // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

由于Array的特性,new Array 和 Array 对单个数字的处理相同,因此以上两种均可行。

keys基于ES6,并未完全支持,以下是各浏览器支持版本:

Browser Chrome Firefox (Gecko) Internet Explorer Opera Safari
Basic support 38 28 (28) 未实现 25 7.1
values(ES6)

values() 方法基于ECMAScript 2015(ES6)规范,返回一个数组迭代器对象,该对象包含数组中每个索引的值。其用法基本与上述 entries 方法一致。

语法:arr.values()

遗憾的是,现在没有浏览器实现了该方法,因此下面将就着看看吧。

1
2
3
4
复制代码var array = ["abc", "xyz"];
var iterator = array.values();
console.log(iterator.next().value);//abc
console.log(iterator.next().value);//xyz
Symbol.iterator(ES6)

该方法基于ECMAScript 2015(ES6)规范,同 values 方法功能相同。

语法:arrSymbol.iterator

1
2
3
4
复制代码var array = ["abc", "xyz"];
var iterator = array[Symbol.iterator]();
console.log(iterator.next().value); // abc
console.log(iterator.next().value); // xyz

其鸭式辨型写法请参考上述 entries 方法。

由于该方法基于ES6,并未完全支持,以下是各浏览器支持版本:

Browser Chrome Firefox (Gecko) Internet Explorer Opera Safari
Basic support 38 36 (36) 1 未实现 25 未实现

小结

以上,Array.prototype 的各方法基本介绍完毕,这些方法之间存在很多共性。比如:

  • 所有插入元素的方法, 比如 push、unshift,一律返回数组新的长度;
  • 所有删除元素的方法,比如 pop、shift、splice 一律返回删除的元素,或者返回删除的多个元素组成的数组;
  • 部分遍历方法,比如 forEach、every、some、filter、map、find、findIndex,它们都包含function(value,index,array){} 和 thisArg 这样两个形参。

Array.prototype 的所有方法均具有鸭式辨型这种神奇的特性。它们不止可以用来处理数组对象,还可以处理类数组对象。

例如 javascript 中一个纯天然的类数组对象字符串(String),像join方法(不改变当前对象自身)就完全适用,可惜的是 Array.prototype 中很多方法均会去试图修改当前对象的 length 属性,比如说 pop、push、shift, unshift 方法,操作 String 对象时,由于String对象的长度本身不可更改,这将导致抛出TypeError错误。

还记得么,Array.prototype本身就是一个数组,并且它的长度为0。

后续章节我们将继续探索Array的一些事情。感谢您的阅读!


本问就讨论这么多内容,大家有什么问题或好的想法欢迎在下方参与留言和评论。

本文作者:louis

本文链接:louiszhai.github.io/2017/04/28/…

参考文章

  • Array.prototype
  • Chrome谷歌浏览器中js代码Array.sort排序的bug乱序解决办法
  • SJ9013: Array.prototype.sort当使用了 comparefn 后返回值不为 -1、0、1时,各引擎实现排序结果不一致

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

也谈图片压缩

发表于 2017-04-23

简介

随着目前设备像素的不断提高,基本随便一张照片即是M级别的大小,对于如此大的图片,不管是在内存空间、带宽资源和服务器数据空间上都是非常耗费的,特别是在移动端,由图片引起的OOM和图片上传质量过大等问题我想大家都遇到过,所以对于图片内存占用上和物理空间占用上进行压缩很有必要,在Android上,我们使用到的图片格式无非这五种:PNG、JPEG、Webp、SVG、GIF。其中GIF的位深为8位,所以文件通常比较小而且支持alpha通道以及动画,Webp在等质量的大小上和等大小的清晰度上都占极大优势,而SVG矢量图是由xml文件进行描述的,可以适配于任何分辨率的设备而保证图像不失真,Google的官方视频中也提到可用这两种格式进行某些场景下替换PNG或JPEG图像,这不但能节约带宽资源还能提高图片加载速度,所以图片压缩主要是对PNG和JPEG这两种格式,关于图片的压缩,有无损压缩和有损压缩两种方式,这两种压缩方式区别如下:

无损压缩:通过对冗余数据的存储方式进行优化,该方式不会丢失文件内容,压缩率受冗余度的影响,所以压缩率较低
有损压缩:通过丢失不会对文件造成太大影响的数据来达到压缩效果,所以压缩率较高

其中PNG是无损压缩格式图片,JPEG是有损压缩格式图片,所以对应的也有各自的压缩算法,在Android系统中,png的压缩是使用libpng进行压缩,场景有两个:编译阶段以及api层调用方式进行压缩。其中在编译阶段通过aapt打包工具会对drawable目录下png图片进行压缩,压缩率大约在40%以下,如果我们对编译后的apk进行解压,可以发现解压后drawable目录下的png图片比原先的变小了,但是,也有例外,对于NinePatch(.9)图片却变大了,这里先讲下原因,因为对于.9图片在编译过程中aapt会对它额外进行处理,使得.9图片会增加2~3个不同类型的Chunk块(注:api层调用方式进行压缩不会对.9进行额外处理),而jpeg的压缩是用libjpeg(7.0后有变化,后面另外说)进行压缩,场景只有在api层进行调用方式进行压缩。下面将主要围绕图片的压缩原理、压缩策略以及在Android上的运用进行讲解。

色彩空间

在此之前,我们先了解一下ColorSpace(色彩空间),通常来说,我们看一张彩色图片,会有几千甚至上万种颜色构成,色彩空间就是用来表示图片所构成的色彩范围,对于图片最常用的是使用三原色组成的RGB色彩空间,它最大可表示2^24(16777216)种颜色,其它的色彩空间还有YUV、CMYK、YCCK等,其中YUV在视频的开发方面会经常涉及到,它主要用于表示彩色视频中彩色图像的颜色空间,为什么使用这种呢?因为它节约带宽,每个像素位深最大不超过12位,最小为6位,在此不过多描述,了解即可,顺便提一点平时在进行视频相关开发时,进行视频采集到的图像数据都是YUV编码的,如果需保存某一帧图像,需把图像数据编码方式YUV转成RGB。

上面说到RGB色彩空间,它根据每个分量的所占位数不同又可以分为这两种:RGB_565、RGB_888,其中带alpha通道的有这两种:ARGB_4444、ARGB_8888,区别如下:

RGB_565:每个像素占两个字节,R分量占5位,G分量占6位,B分量占5位,最多能表示2^16(65536)中颜色
RGB_888:每个像素占三个字节,R、G、B分量各占8位,最多能表示2^24(16777216)中颜色
ARGB_4444:每个像素占两个字节,A、R、G、B分量各占4位,最多能表示2^12(4096)中颜色,成像效果比较差,所以Google给了它一个Deprecated,并且v4.4+后如果使用了它会自动转成用ARGB_8888
ARGB_8888:每个像素占四个字节,A、R、G、B分量各占8位,最多能表示2^24(16777216)中颜色,其中前面8位alpha(0~255)通道表示每个像素点的透明度

存储形式

图片的存储形式,主要以以下三种形式存在:File、Stream、Bitmap。其中在Android上File主要有PNG、JPEG、XML(VectorDrawable)、Webp和GIF这五种类型格式进行存储,下面分别对这三种存储形式以及压缩方式进行分析。

PNG

PNG是一种无损压缩的图像存储格式,正由于它使用的是无损压缩算法进行压缩,所以相同像素宽高的图像保存为PNG在文件大小上比JPEG往往要大的多,一般是JPEG大小的几倍左右。由于无损压缩不会丢失图像数据,并且支持alpha通道而且完整的保存了图像数据且无锯齿,所以一般应用在PS素材或图标上,这就为什么不管Android和iOS图标都是使用的png格式。

PNG图像根据每个像素位数的不同,可分为三种格式:PNG8、PNG24、PNG32。PNG8只支持256色,有索引色透明和Alpha透明两种方式,索引色透明只能简单的指明一个像素点为透明还是不透明,Alpha透明则支持像素点的透明度,PNG24支持全色1670万色,只支持不透明,PNG32支持全色1670万色,在PNG24基础上增加了8位的alpha分量,支持Alpha透明,目前大部分PNG图片使用的格式大都为PNG32。

PNG数据结构

一个标准PNG图像文件数据结构如下:


其中一个最简单的PNG图像应该至少包含PNG文件的签名Signature、文件头数据块IHDR、图像内容数据块IDAT以及图像结束数据块IEND,这三个的数据块叫做关键数据块,是每一个PNG文件必须包含的,否则PNG文件将无法正常显示,另外还有辅助数据块,如:PLTE(调色板数据块,仅用于索引PNG)、tEXt(文本信息数据块),这些辅助数据块是可选的,用于额外表示一个PNG文件的内容。
这些Chunk都由四部分内容组成:


含义如下:

Length:占4个字节,表示该Chunk中Data域的长度
Type:占4个字节,表示该Chunk的类型,如:IHDR、IDAT等
Data:占n个字节,存储着该Chunk的数据
CRC:占4个字节,循环冗余校验码

下面主要说下这三个关键数据块,它们表示的含义如下:

Signature:占8个字节,用于表示该文件是一个PNG文件,内容固定
IHDR:占25字节,其中Data域占13个字节,用于表示图像的基本信息,如图像的宽高与位深等,并且它永远都是第一个数据块
IDAT:占n个字节,用于表示图像的数据信息,它存储真实的图像数据,在一个PNG文件中,该数据块出现的数量为>=1
IEND:占12个字节,用于表示数据块内容已结束,永远都是最后一个数据块,内容固定

如下,我们查看一张最简单的PNG文件结构:

可以看到这张PNG图像只包含了Signature、IHDR、一个IDAT以及结束数据块IEND,可以说是最.简单的PNG图像。

前8个字节:

描述的Signature为ASCII字符.PNG表示.该文件为PNG文件。
后续的25个字节:

描述的是IHDR头数据块,其中前4个字节00 00 00 0D表示该数据块Data域的长度为13字节,然后是4个字节49 48 44 52描述的该数据块类型,对应的ASCII字符为IHDR,接下来是数据块真正存储的数据Data域,最后是4个字节的CRC校验码。关于IHDR数据块的Data,主要有四个我们比较关心的数据:图像宽高、色深以及颜色类型。其中宽和高各占4个字节,位深和颜色类型占1个字节,对应字节为:

.

从中可以得知该PNG图像宽和高都为00 00 00 30(48px),色深为8bit,颜色类型为6(6代表带alpha通道的彩色图像)。

更多细节可以参考PNG百科以及W3C Introduce

NinePatch(.9)

NinePatch(.9)图片是Android上一个可动态伸缩的PNG图片,为什么它具有这种特性呢?原理是在PNG图像基础上添加一个额外的1像素的边框来描述动态伸缩与内容填充的区域,然后在编译打包时通过aapt工具对.9图进行额外处理,具体是提取所添加的1像素边框的信息,这些信息会通过额外类型npTc和npOl的Chunk数据块保存在PNG文件中,当在图片加载时,会在判断该图片是否为.9图来选择性的构造一个NinePatchDrawable还是BitmapDrawable对象,NinePatchDrawable即是一个可对内容区域进行动态伸缩的Drawable,判断是否为.9图以及构造一个NinePatchD.rawable代码为:

1
2
3
4
if (npChunk != null && NinePatch.isNinePatchChunk(npChunk)) {复制代码  
NinePatchDrawable npDrawable = new NinePatchDrawable(getResources(), bitmap, npChunk, new Rect(), null);
//...
}

一个.9图片可以由自带的draw9patch工具进行制作,制作后文件的后缀为.9.png。

PNG压缩

关于对PNG图片的压缩,Android默认使用的是libpng库进行PNG图片的压缩,场景有两个地方:aapt打包时和bitmap.compress()时。
所以对于Android中PNG的压缩或想获取更好的压缩率,我们有两种做法:

1、屏蔽在aapt打包时默认的libpng的压缩,我们自己使用第三方压缩工具进行png图像的压缩
2、对于api层面,使用自己编译的lib库替换系统的api进行png的压缩

对于一些第三方png压缩工具,有:Pngquant、Pngout、TinyPng、Optipn以及ImageOptim等。

通常来说如果我们不满足于在aapt打包时进行的png图片压缩,我们可以通过上面的工具进行png的压缩,此时,必须屏蔽aapt打包时的压缩,为什么呢?避免压缩覆盖,因为每个压缩工具的压缩算法也不同,所以对于压缩只能并且最好只有一次,否则极可能导致经过第三方压缩后再经过libpng压缩,最后的png图片大小并没有发生明显的变化。

我们可以通过gradle的aaptOptions配置来屏蔽aapt打包时对png进行压缩,进而使.用我们自己压缩的png图片,通过以下配置:

1
2
3
4
    aaptOptions {复制代码  
cruncherEnabled false
}
}

其中这不会屏蔽对.9图片的处理,所以不影响.9图的使用。

JPEG

JPEG是一种有损压缩的图像存储格式,不支持alpha通道,由于它具有高压缩比,在压缩过程中把重复的数据和无关紧要的数据会选择性的丢失,所以如果不需要用到alpha通道,那么大都图片格式都用该格式。

JPEG数据结构

一张JPEG图片的数据结构大致如下:

JPEG文件主要是由多个segment段组成,每个segment又由标识码和压缩数据组成,标识码由两个字节组成,第一个为固定值0xFF,而区分每个标识码的类型主要由第二个进行区分,下面介绍一下常用的标识码:

FFD8:表示图像的开始,段名为SOI
FFE0:表示JFIF数据块,段名为APP0
FFC0:表示图像帧开始,段名为SOFO
FFC4:表示Huffman表,段名为DHT
FFDA:表示从上往下开始扫描图像,段名为SOS
FFD9:表示图像结束,段名为EOI

更多JPEG格式细节可以看JPEG Wiki

JPEG压缩

对于JPEG图片的压缩,文章开头说到了Android默认使用的是libjpeg库进行压缩的,不过在Android7.0+发生了一点点变化,主要是做了两点优化

1、内部使用的JPEG压缩库改为libjpeg-turbo,这是一个基于libjpeg的涡轮增压库,主要的一特点就是速度比libjpeg快
2、使用Huffman编码替代Arithmetic编码

上面第二点为主要优化点,有兴趣的同学可以用一台Android7.0+手机以及7.0以下版本的手机,压缩相同一张图片,会发现在相同质量下Android7.0+机子上的压缩后的图片大小比7.0以下的要小。

VectorDrawable

矢量图是通过一系列的xml标签进行描述图像的行为信息,通过xml文件进行存储,使得它文件比一般PNG、JPEG更小,同时具备高度的伸缩性且不失真,但在Android上的矢量图(VectorDrawable)描述标签和广义上的SVG矢量图有些差别,在Android具体的对矢量图进行描述的标签主要是vector与path,其中path的格式和定义是一样的,矢量图的行为内容描述也主要在该标签中,在支持上:

1、在Android5.0+提供原生支持
2、使用support-library-v23.2+版本提供全版本支持,具体可查看官方博客

WebP

Webp图片格式是Google推出的一个支持alpha通道的有损压缩格式,据Google官方表明,同质量情况下Webp图像要比JPEG、PNG图像小25%~45%左右,在支持上Android4.0+版本提供原生支持,使用libwebp库进行编解码。

GIF

GIF图像最广泛的应用是用于显示动画图像,它具备文件小且支持alpha通道的优点,不过它是由8位进行表示每个像素的色彩,仅支持256色,所以在对色彩要求比较高的场合不太适合。

Stream

图片的存储形式从File转到内存中时,图片内容以字节方式存储在Stream中,此时所占的内存大小为File文件大小。

Bitmap

在Android中,任何图片资源的显示对象都是通过bitmap来显示的,除了xml资源则是通过Canvas来绘制的,所以,对于某些纯色或者规则类的图像,可以通过xml进行描述或Canvas来绘制,这样所占用的内存比通过bitmap来显示将少几个等级。

Bitmap所占用内存的计算:

pixelWidth pixelHeight bytesPerPixel

即Bitmap的宽x高x每像素所占字节数,所以相同一个Bitmap对象,对于每像素占2字节的RGB_565色彩空间所占内存是每像素4字节的ARGB_8888占用内存的一半。

Bitmap与Drawable的联系

关于Bitmap和Drawable的关系,可以看官方的解释,Drawable是一个抽象的概念,来描述某些具备可绘制的的对象,它是一个抽象类,而Bitmap是一个最简单的Drawable实体对象,Bitmap并不继承于Drawable,它们之间建立关联最终是通过BitmapDrawable对象,该对象会把具体的Bitmap实例对象渲染到Canvas上。Drawable更注重描述的是某绘制的行为,而Bitmap则是注重存储着图像的像素信息。

Bitmap存储空间

随着版本的变化以及存储空间的变化,Bitmap的存储空间主要有三个地方
Native Memory
Android2.3以下版本,bitmap像素数据存储在native内存中,释放内存需主动调用recycle()方法
Dalvik Heap
Android3.0+版本,在Android2.3版本引入了并发的垃圾回收器后,在3.0以后的版本bitmap的像素数据则存储在虚拟机堆中,不需要主动调用recycle()来回收内存,gc会主动回收
Ashmem
匿名共享内存空间,说到这个,就会联想起大名鼎鼎的Fresco图片库,它巧妙的利用了这一空间来进行Bitmap对象的存储,对于Ashmem空间,首先想到的是与App进程空间是隔离且互不影响的,这点在Android4.4以下版本是这样的,在Android4.4+后版本,Ashmem空间将会包含在App所占用的内存空间中。看Fresco源码也可以看出,对于4.4+版本,对于Bitmap的解码使用了另外的解码器。在Android4.4以下版本如何使用Ashmem进行bitmap.的存储呢?通过DecodeOptions:

1
options.inInputShareable = true;复制代码

以及通过MemoryFile可将图片的字节数据存储在Ashmem中。

压缩方式

对于图片的压缩方式,针对内存空间来说主要有两个类型:

1、减少虚拟机堆中所占用的内存大小
2、减少在硬盘中所占用的物理内存大小

下面主要说说这两种类型的压缩方式

降低色彩位数

所谓的降低色彩位数,就是降低RGB各个分量的位数,如ARGB_8888到RGB_565,每个像素所占字节从4个减小到2个字节,对应的内存大小也节约了一半。

对于每个像素的R、G、B分量的转换过程,即一个分量占用8bit需要转换为5bit(6bit),或者是一个分量占用5bit(6bit)需要转换为8bit,这两个过程叫:压缩、补偿。

压缩

压缩是各个分量位数的降低

以RGB_888到RGB_565为例:

RGB_888每个.分量占8位,每个像素的位深为24bit,如下:

RGB_565每个分量分别占5位、6位、5位,每个像素位深为16bit,对于各分量的压缩,避免不了丢失一些精度,为使得精度丢失最小,我们只需取其高位即可,RGB_888到RGB_565.的转换后最终每个分量值为:

补偿

补偿是各个分量位数的增加

以RGB_565到RGB_888为例:
RGB_565每个分量分别占5位.、6位、5位,每个像素位深为16bit,如下:

RGB_888每个分量占8位,每个像素的位深为24bit,对于各个分量的补偿,常用的做法是

用原分量的值进行填充,剩下的用原分量的低位进行循环补偿

RGB_565到RGB_888的转换后最终每个分量的值为:

1
复制代码

尺寸压缩

尺寸压缩主要是缩减bitmap的大小,当加载一张大图片时,进行合适的尺寸的压缩是减小内存占用的很有效的方法

SamplingSize

采样率压缩,这基本是人人皆知的办法,在图片decode阶段,先获取其宽高然后进行判断是否符合我们.预期的,否则进行一定比例的缩放,代码为:

1
2
3
4
5
6
7
8
    BitmapFactory.Options options = new BitmapFactory.Options();复制代码  
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(filePath, options);
int outWidth = options.outWidth;
int outHeight = options.outHeight;
options.inSampleSize = computeSampleSize(outWidth, outHeight);
return BitmapFactory.decodeFile(filePath, options);
}

这种方式采样率只能支持2的幂次方的值进行缩放,所以一般decode出来的bitmap大小往往不是我们预期的大小,有可能大很多也有可能小很多

Matrix

Matrix矩阵变换,可以对bitmap进行非常多的操作,其中一项是对bitmap进行等比缩放,这种方式可以精确的缩.放到符合我们预期的bitmap大小,代码如下:

1
2
3
4
5
int bitmapHeight = bitmap.getHeight();复制代码  
Matrix matrix = new Matrix();
float rate = computeScaleRate(bitmapWidth, bitmapHeight);
matrix.postScale(rate, rate);
Bitmap result = Bitmap.createBitmap(bitmap, 0, 0, bitmapWidth, bitmapHeight, matrix, true);

设备屏幕密度与drawable关系

我们知道在不同屏幕密度的设备下,会选择最适合该设备的资源目录进行资源的加载,如对于屏幕dpi为480的则会最优选择xxhdpi目录,关于分辨率与dpi以及屏幕密度对应关系为:

Dpi 分辨率 Res Density
160dpi 320x533 mdpi 1
240dpi 480x800 hdpi 1.5
320dpi 720x1280 xhdpi 2
480dpi 1080x1920 xxhdpi 3
560dpi 1440x2560 xxxhdpi 3.5

如果没有对应的目录,则会使用默认目录。假如有一设备dpi为480,并且没有xxdpi目录下没有该图片资源,那么在进行图片资源decode时候,会把图片适配到对应的屏幕分辨率,进行放大或缩小,缩放比例是:

宽、高 * (当前设备dpi/目标资源目录对应的dpi)

对于获取设备的dpi以.及density可由Resources静态方法方便获取:

1
2
3
4
int densityDpi = displayMetrics.densityDpi;复制代码  
int density = displayMetrics.density;
int width = displayMetrics.widthPixels;
int height = displayMetrics.heightPixels;

一些建议
对于某些高清无码比较大的图片,如一些背景或者引导图等,可由第三方压缩工具进行压缩后放入assets目录中,避免可能在drawable目录下加载引起的放大导致消耗内存过大或缩小导致图片过小等问题。

质量压缩

对于质量压缩,也就是图片的所占物理内存的大小,主要是通过一些lib库进行压缩,如Android默认的bitmap.compress(),可选择PNG与JPEG等进行压缩,如果不满足于内置的lib压缩库效果,可自己选择替换系统api进行压缩,在上述JPEG这节中说的Android底层所用libjpeg库在7.0+版本变化,主要是进行了JPEG图片压缩的优化,所以为了弥补在Android7.0以下对JPEG压缩的质量问题以及对bitmap压缩进行合理适配,特此写了一个开源库
Tiny,对于JPEG的压缩选择和Android7.0后一样的库libjpeg-turbo库进行压缩,同时也开启了Huffman编码。

Tiny

下面是使用Tiny图片压缩库进行压缩与微信朋友圈的效果对比示例:

图片信息 Tiny Wechat
6.66MB (3500x2156) 151KB (1280x788) 135KB (1280x789)
4.28MB (4160x3120) 219KB (1280x960) 195KB (1280x960)
2.60MB (4032x3024) 193KB (1280x960)) 173KB (1280x960)
372KB (500x500) 38.67KB (500x500) 34.05KB (500x500)
236KB (960x1280) 127KB (960x1280) 118KB (960x1280)

Tiny项目地址为:戳我

Reference

Image compression for Android developers - Google I/O 2016
Android Support Library 23.2
WebP Compression Techniques in Detail
Managing Bitmap Memory

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

正则表达式前端使用手册

发表于 2017-03-14

导读

你有没有在搜索文本的时候绞尽脑汁, 试了一个又一个表达式, 还是不行.

你有没有在表单验证的时候, 只是做做样子(只要不为空就好), 然后烧香拜佛, 虔诚祈祷, 千万不要出错.

你有没有在使用sed 和 grep 命令的时候, 感觉莫名其妙, 明明应该支持的元字符, 却就是匹配不到.

甚至, 你压根没遇到过上述情况, 你只是一遍又一遍的调用 replace 而已 (把非搜索文本全部替换为空, 然后就只剩搜索文本了), 面对别人家的简洁高效的语句, 你只能在心中呐喊, replace 大法好.

为什么要学正则表达式. 有位网友这么说: 江湖传说里, 程序员的正则表达式和医生的处方, 道士的鬼符齐名, 曰: 普通人看不懂的三件神器. 这个传说至少向我们透露了两点信息: 一是正则表达式很牛, 能和医生的处方, 道士的鬼符齐名, 并被大家提起, 可见其江湖地位. 二是正则表达式很难, 这也从侧面说明了, 如果你可以熟练的掌握并应用它, 在装逼的路上, 你将如日中天 (别问我中天是谁……) !

显然, 有关正则表达的介绍, 无须我多言. 这里就借助 Jeffrey Friedl 的《精通正则表达式》一书的序言正式抛个砖.

“如果罗列计算机软件领域的伟大发明, 我相信绝对不会超过二十项, 在这个名单当中, 当然应该包括分组交换网络, Web, Lisp, 哈希算法, UNIX, 编译技术, 关系模型, 面向对象, XML这些大名鼎鼎的家伙, 而正则表达式也绝对不应该被漏掉.

对很多实际工作而言, 正则表达式简直是灵丹妙药, 能够成百倍的提高开发效率和程序质量, 正则表达式在生物信息学和人类基因图谱的研究中所发挥的关键作用, 更是被传为佳话. CSDN的创始人蒋涛先生在早年开发专业软件产品时, 就曾经体验过这一工具的巨大威力, 并且一直印象深刻.”

因此, 我们没有理由不去了解正则表达式, 甚至是熟练掌握并运用它.

本文以正则基础语法开篇, 结合具体实例, 逐步讲解正则表达式匹配原理. 代码实例使用语言包括 js, php, python, java(因有些匹配模式, js并未支持, 需要借助其他语言讲解). 内容包括初阶技能和高阶技能, 适合新手学习和进阶. 本文力求简单通俗易懂, 同时为求全面, 涉及知识较多, 共计12k字, 篇幅较长, 请耐心阅读, 如有阅读障碍请及时联系我.

回顾历史

要论正则表达式的渊源, 最早可以追溯至对人类神经系统如何工作的早期研究. Warren McCulloch 和 Walter Pitts 这两位神经大咖 (神经生理学家) 研究出一种数学方式来描述这些神经网络.

1956 年, 一位叫 Stephen Kleene 的数学家在 McCulloch 和 Pitts 早期工作的基础上, 发表了一篇标题为”神经网事件的表示法”的论文, 引入了正则表达式的概念.

随后, 发现可以将这一工作应用于使用 Ken Thompson 的计算搜索算法的一些早期研究中. 而 Ken Thompson 又是 Unix 的主要发明人. 因此半个世纪以前的Unix 中的 qed 编辑器(1966 qed编辑器问世) 成了第一个使用正则表达式的应用程序.

至此之后, 正则表达式成为家喻户晓的文本处理工具, 几乎各大编程语言都以支持正则表达式作为卖点, 当然 JavaScript 也不例外.

正则表达式的定义

正则表达式是由普通字符和特殊字符(也叫元字符或限定符)组成的文字模板. 如下便是简单的匹配连续数字的正则表达式:

1
/[0-9]+//\d+/

“\d” 就是元字符, 而 “+” 则是限定符.

元字符

元字符 描述
. 匹配除换行符以外的任意字符
\d 匹配数字, 等价于字符组[0-9]
\w 匹配字母, 数字, 下划线或汉字
\s 匹配任意的空白符(包括制表符,空格,换行等)
\b 匹配单词开始或结束的位置
^ 匹配行首
$ 匹配行尾

反义元字符

元字符 描述
\D 匹配非数字的任意字符, 等价于[^0-9]
\W 匹配除字母,数字,下划线或汉字之外的任意字符
\S 匹配非空白的任意字符
\B 匹配非单词开始或结束的位置
[^x] 匹配除x以外的任意字符

可以看出正则表达式严格区分大小写.

重复限定符

限定符共有6个, 假设重复次数为x次, 那么将有如下规则:

限定符 描述
* x>=0
+ x>=1
? x=0 or x=1
{n} x=n
{n,} x>=n
{n,m} n<=x<=m

字符组

[…] 匹配中括号内字符之一. 如: [xyz] 匹配字符 x, y 或 z. 如果中括号中包含元字符, 则元字符降级为普通字符, 不再具有元字符的功能, 如 [+.?] 匹配 加号, 点号或问号.

排除性字符组

[^…] 匹配任何未列出的字符,. 如: [^x] 匹配除x以外的任意字符.

多选结构

| 就是或的意思, 表示两者中的一个. 如: a|b 匹配a或者b字符.

括号

括号 常用来界定重复限定符的范围, 以及将字符分组. 如: (ab)+ 可以匹配abab..等, 其中 ab 便是一个分组.

转义字符

\ 即转义字符, 通常 \ * + ? | { [ ( ) ] }^ $ . # 和 空白 这些字符都需要转义.

操作符的运算优先级

  1. \ 转义符
  2. (), (?:), (?=), [] 圆括号或方括号
  3. *, +, ?, {n}, {n,}, {n,m} 限定符
  4. ^, $ 位置
  5. | “或” 操作

测试

我们来测试下上面的知识点, 写一个匹配手机号码的正则表达式, 如下:

1
(\+86)?1\d{10}

① “+86” 匹配文本 “+86”, 后面接元字符问号, 表示可匹配1次或0次, 合起来表示 “(+86)?” 匹配 “+86” 或者 “”.

② 普通字符”1” 匹配文本 “1”.

③ 元字符 “\d” 匹配数字0到9, 区间量词 “{10}” 表示匹配 10 次, 合起来表示 “\d{10}” 匹配连续的10个数字.

以上, 匹配结果如下:

修饰符

javaScript中正则表达式默认有如下五种修饰符:

  • g (全文查找), 如上述截图, 实际上就开启了全文查找模式.
  • i (忽略大小写查找)
  • m (多行查找)
  • y (ES6新增的粘连修饰符)
  • u (ES6新增)

常用的正则表达式

  1. 汉字: ^[\u4e00-\u9fa5]{0,}$
  2. Email: ^\w+([-+.]\w+)*@\w+([-.]\w+)*.\w+([-.]\w+)*$
  3. URL: ^https?://([\w-]+.)+[\w-]+(/[\w-./?%&=]*)?$
  4. 手机号码: ^1\d{10}$
  5. 身份证号: ^(\d{15}|\d{17}(\d|X))$
  6. 中国邮政编码: [1-9]\d{5}(?!\d) (邮政编码为6位数字)

密码验证

密码验证是常见的需求, 一般来说, 常规密码大致会满足规律: 6-16位, 数字, 字母, 字符至少包含两种, 同时不能包含中文和空格. 如下便是常规密码验证的正则描述:

1
var reg = /(?!^[0-9]+$)(?!^[A-z]+$)(?!^[^A-z0-9]+$)^[^\s\u4e00-\u9fa5]{6,16}$/;

正则的几大家族

正则表达式分类

在 linux 和 osx 下, 常见的正则表达式, 至少有以下三种:

  • 基本的正则表达式( Basic Regular Expression 又叫 Basic RegEx 简称 BREs )
  • 扩展的正则表达式( Extended Regular Expression 又叫 Extended RegEx 简称 EREs )
  • Perl 的正则表达式( Perl Regular Expression 又叫 Perl RegEx 简称 PREs )

正则表达式比较

字符 说明 Basic RegEx Extended RegEx python RegEx Perl regEx
转义
^ 匹配行首,例如’^dog’匹配以字符串dog开头的行(注意:awk 指令中,’^’则是匹配字符串的开始) ^ ^ ^ ^
$ 匹配行尾,例如:’^、dog$’ 匹配以字符串 dog 为结尾的行(注意:awk 指令中,’$’则是匹配字符串的结尾) $ $ $ $
^$ 匹配空行 ^$ ^$ ^$ ^$
^string$ 匹配行,例如:’^dog$’匹配只含一个字符串 dog 的行 ^string$ ^string$ ^string$ ^string$
< 匹配单词,例如:’<frog’ (等价于’\bfrog’),匹配以 frog 开头的单词 < < 不支持 不支持(但可以使用\b来匹配单词,例如:’\bfrog’)
> 匹配单词,例如:’frog>‘(等价于’frog\b ‘),匹配以 frog 结尾的单词 > > 不支持 不支持(但可以使用\b来匹配单词,例如:’frog\b’)
\ 匹配一个单词或者一个特定字符,例如:’\ ‘(等价于’\bfrog\b’)、’\ ‘ \ \ 不支持 不支持(但可以使用\b来匹配单词,例如:’\bfrog\b’
() 匹配表达式,例如:不支持’(frog)’ 不支持(但可以使用,如:dog () () ()
匹配表达式,例如:不支持’(frog)’ 不支持(同()) 不支持(同()) 不支持(同())
? 匹配前面的子表达式 0 次或 1 次(等价于{0,1}),例如:where(is)?能匹配”where” 以及”whereis” 不支持(同?) ? ? ?
? 匹配前面的子表达式 0 次或 1 次(等价于’{0,1}‘),例如:’whereis? ‘能匹配 “where”以及”whereis” ? 不支持(同?) 不支持(同?) 不支持(同?)
? 当该字符紧跟在任何一个其他限制符(*, +, ?, {n},{n,}, {n,m}) 后面时,匹配模式是非贪婪的。非贪婪模式尽可能少的匹配所搜索的字符串,而默认的贪婪模式则尽可能多的匹配所搜索的字符串。例如,对于字符串 “oooo”,’o+?’ 将匹配单个”o”,而 ‘o+’ 将匹配所有 ‘o’ 不支持 不支持 不支持 不支持
. 匹配除换行符(’\n’)之外的任意单个字符(注意:awk 指令中的句点能匹配换行符) . .(如果要匹配包括“\n”在内的任何一个字符,请使用: [\s\S] . .(如果要匹配包括“\n”在内的任何一个字符,请使用:’ [.\n] ‘
* 匹配前面的子表达式 0 次或多次(等价于{0, }),例如:zo* 能匹配 “z”以及 “zoo” * * * *
+ 匹配前面的子表达式 1 次或多次(等价于’{1, }‘),例如:’whereis+ ‘能匹配 “whereis”以及”whereisis” + 不支持(同+) 不支持(同+) 不支持(同+)
+ 匹配前面的子表达式 1 次或多次(等价于{1, }),例如:zo+能匹配 “zo”以及 “zoo”,但不能匹配 “z” 不支持(同+) + + +
{n} n 必须是一个 0 或者正整数,匹配子表达式 n 次,例如:zo{2}能匹配 不支持(同{n}) {n} {n} {n}
{n,} “zooz”,但不能匹配 “Bob”n 必须是一个 0 或者正整数,匹配子表达式大于等于 n次,例如:go{2,} 不支持(同{n,}) {n,} {n,} {n,}
{n,m} 能匹配 “good”,但不能匹配 godm 和 n 均为非负整数,其中 n <= m,最少匹配 n 次且最多匹配 m 次 ,例如:o{1,3}将配”fooooood” 中的前三个 o(请注意在逗号和两个数之间不能有空格) 不支持(同{n,m}) {n,m} {n,m} {n,m}
x l y 匹配 x 或 y 不支持(同x \l y x l y x l y x l y
[0-9] 匹配从 0 到 9 中的任意一个数字字符(注意:要写成递增) [0-9] [0-9] [0-9] [0-9]
[xyz] 字符集合,匹配所包含的任意一个字符,例如:’[abc]’可以匹配”lay” 中的 ‘a’(注意:如果元字符,例如:. *等,它们被放在[ ]中,那么它们将变成一个普通字符) [xyz] [xyz] [xyz] [xyz]
[^xyz] 负值字符集合,匹配未包含的任意一个字符(注意:不包括换行符),例如:’[^abc]’ 可以匹配 “Lay” 中的’L’(注意:[^xyz]在awk 指令中则是匹配未包含的任意一个字符+换行符) [^xyz] [^xyz] [^xyz] [^xyz]
[A-Za-z] 匹配大写字母或者小写字母中的任意一个字符(注意:要写成递增) [A-Za-z] [A-Za-z] [A-Za-z] [A-Za-z]
[^A-Za-z] 匹配除了大写与小写字母之外的任意一个字符(注意:写成递增) [^A-Za-z] [^A-Za-z] [^A-Za-z] [^A-Za-z]
\d 匹配从 0 到 9 中的任意一个数字字符(等价于 [0-9]) 不支持 不支持 \d \d
\D 匹配非数字字符(等价于 [^0-9]) 不支持 不支持 \D \D
\S 匹配任何非空白字符(等价于[^\f\n\r\t\v]) 不支持 不支持 \S \S
\s 匹配任何空白字符,包括空格、制表符、换页符等等(等价于[ \f\n\r\t\v]) 不支持 不支持 \s \s
\W 匹配任何非单词字符 (等价于[^A-Za-z0-9_]) \W \W \W \W
\w 匹配包括下划线的任何单词字符(等价于[A-Za-z0-9_]) \w \w \w \w
\B 匹配非单词边界,例如:’er\B’ 能匹配 “verb” 中的’er’,但不能匹配”never” 中的’er’ \B \B \B \B
\b 匹配一个单词边界,也就是指单词和空格间的位置,例如: ‘er\b’ 可以匹配”never” 中的 ‘er’,但不能匹配 “verb” 中的’er’ \b \b \b \b
\t 匹配一个横向制表符(等价于 \x09和 \cI) 不支持 不支持 \t \t
\v 匹配一个垂直制表符(等价于 \x0b和 \cK) 不支持 不支持 \v \v
\n 匹配一个换行符(等价于 \x0a 和\cJ) 不支持 不支持 \n \n
\f 匹配一个换页符(等价于\x0c 和\cL) 不支持 不支持 \f \f
\r 匹配一个回车符(等价于 \x0d 和\cM) 不支持 不支持 \r \r
\ 匹配转义字符本身”\” \ \ \ \
\cx 匹配由 x 指明的控制字符,例如:\cM匹配一个Control-M 或回车符,x 的值必须为A-Z 或 a-z 之一,否则,将 c 视为一个原义的 ‘c’ 字符 不支持 不支持 \cx
\xn 匹配 n,其中 n 为十六进制转义值。十六进制转义值必须为确定的两个数字长,例如:’\x41’ 匹配 “A”。’\x041’ 则等价于’\x04’ & “1”。正则表达式中可以使用 ASCII 编码 不支持 不支持 \xn
\num 匹配 num,其中 num是一个正整数。表示对所获取的匹配的引用 不支持 \num \num
[:alnum:] 匹配任何一个字母或数字([A-Za-z0-9]),例如:’[[:alnum:]] ‘ [:alnum:] [:alnum:] [:alnum:] [:alnum:]
[:alpha:] 匹配任何一个字母([A-Za-z]), 例如:’ [[:alpha:]] ‘ [:alpha:] [:alpha:] [:alpha:] [:alpha:]
[:digit:] 匹配任何一个数字([0-9]),例如:’[[:digit:]] ‘ [:digit:] [:digit:] [:digit:] [:digit:]
[:lower:] 匹配任何一个小写字母([a-z]), 例如:’ [[:lower:]] ‘ [:lower:] [:lower:] [:lower:] [:lower:]
[:upper:] 匹配任何一个大写字母([A-Z]) [:upper:] [:upper:] [:upper:] [:upper:]
[:space:] 任何一个空白字符: 支持制表符、空格,例如:’ [[:space:]] ‘ [:space:] [:space:] [:space:] [:space:]
[:blank:] 空格和制表符(横向和纵向),例如:’[[:blank:]]’ó’[\s\t\v]’ [:blank:] [:blank:] [:blank:] [:blank:]
[:graph:] 任何一个可以看得见的且可以打印的字符(注意:不包括空格和换行符等),例如:’[[:graph:]] ‘ [:graph:] [:graph:] [:graph:] [:graph:]
[:print:] 任何一个可以打印的字符(注意:不包括:[:cntrl:]、字符串结束符’\0’、EOF 文件结束符(-1), 但包括空格符号),例如:’[[:print:]] ‘ [:print:] [:print:] [:print:] [:print:]
[:cntrl:] 任何一个控制字符(ASCII 字符集中的前 32 个字符,即:用十进制表示为从 0 到31,例如:换行符、制表符等等),例如:’ [[:cntrl:]]’ [:cntrl:] [:cntrl:] [:cntrl:] [:cntrl:]
[:punct:] 任何一个标点符号(不包括:[:alnum:]、[:cntrl:]、[:space:]这些字符集) [:punct:] [:punct:] [:punct:] [:punct:]
[:xdigit:] 任何一个十六进制数(即:0-9,a-f,A-F) [:xdigit:] [:xdigit:] [:xdigit:] [:xdigit:]

注意

  • js中支持的是EREs.
  • 当使用 BREs ( 基本正则表达式 ) 时,必须在下列这些符号(?,+,|,{,},(,))前加上转义字符 \ .
  • 上述[[:xxxx:]] 形式的正则表达式, 是php中内置的通用字符簇, js中并不支持.

linux/osx下常用命令与正则表达式的关系

我曾经尝试在 grep 和 sed 命令中书写正则表达式, 经常发现不能使用元字符, 而且有时候需要转义, 有时候不需要转义, 始终不能摸清它的规律. 如果恰好你也有同样的困惑, 那么请往下看, 相信应该能有所收获.

grep , egrep , sed , awk 正则表达式特点

  1. grep 支持:BREs、EREs、PREs 正则表达式

grep 指令后不跟任何参数, 则表示要使用 “BREs”

grep 指令后跟 ”-E” 参数, 则表示要使用 “EREs”

grep 指令后跟 “-P” 参数, 则表示要使用 “PREs”
2. egrep 支持:EREs、PREs 正则表达式

egrep 指令后不跟任何参数, 则表示要使用 “EREs”

egrep 指令后跟 “-P” 参数, 则表示要使用 “PREs”
3. sed 支持: BREs、EREs

sed 指令默认是使用 “BREs”

sed 指令后跟 “-r” 参数 , 则表示要使用“EREs”
4. awk 支持 EREs, 并且默认使用 “EREs”

正则表达式初阶技能

贪婪模式与非贪婪模式

默认情况下, 所有的限定词都是贪婪模式, 表示尽可能多的去捕获字符; 而在限定词后增加?, 则是非贪婪模式, 表示尽可能少的去捕获字符. 如下:

1
var str = "aaab",    reg1 = /a+/, //贪婪模式    reg2 = /a+?/;//非贪婪模式console.log(str.match(reg1)); //["aaa"], 由于是贪婪模式, 捕获了所有的aconsole.log(str.match(regs)); //["a"], 由于是非贪婪模式, 只捕获到第一个a

实际上, 非贪婪模式非常有效, 特别是当匹配html标签时. 比如匹配一个配对出现的div, 方案一可能会匹配到很多的div标签对, 而方案二则只会匹配一个div标签对.

1
var str = "<div class='v1'><div class='v2'>test</div><input type='text'/></div>";var reg1 = /<div.*<\/div>/; //方案一,贪婪匹配var reg2 = /<div.*?<\/div>/;//方案二,非贪婪匹配console.log(str.match(reg1));//"<div class='v1'><div class='v2'>test</div><input type='text'/></div>"console.log(str.match(reg2));//"<div class='v1'><div class='v2'>test</div>"
区间量词的非贪婪模式

一般情况下, 非贪婪模式, 我们使用的是”*?”, 或 “+?” 这种形式, 还有一种是 “{n,m}?”.

区间量词”{n,m}” 也是匹配优先, 虽有匹配次数上限, 但是在到达上限之前, 它依然是尽可能多的匹配, 而”{n,m}?” 则表示在区间范围内, 尽可能少的匹配.

需要注意的是:

  • 能达到同样匹配结果的贪婪与非贪婪模式, 通常是贪婪模式的匹配效率较高.
  • 所有的非贪婪模式, 都可以通过修改量词修饰的子表达式, 转换为贪婪模式.
  • 贪婪模式可以与固化分组(后面会讲到)结合,提升匹配效率,而非贪婪模式却不可以.

分组

正则的分组主要通过小括号来实现, 括号包裹的子表达式作为一个分组, 括号后可以紧跟限定词表示重复次数. 如下, 小括号内包裹的abc便是一个分组:

1
/(abc)+/.test("abc123") == true

那么分组有什么用呢? 一般来说, 分组是为了方便的表示重复次数, 除此之外, 还有一个作用就是用于捕获, 请往下看.

捕获性分组

捕获性分组, 通常由一对小括号加上子表达式组成. 捕获性分组会创建反向引用, 每个反向引用都由一个编号或名称来标识, js中主要是通过 $+编号 或者 \+编号 表示法进行引用. 如下便是一个捕获性分组的例子.

1
var color = "#808080";var output = color.replace(/#(\d+)/,"$1"+"~~");//自然也可以写成 "$1~~"console.log(RegExp.$1);//808080console.log(output);//808080~~

以上, (\d+) 表示一个捕获性分组, RegExp.$1 指向该分组捕获的内容. $+编号 这种引用通常在正则表达式之外使用. \+编号 这种引用却可以在正则表达式中使用, 可用于匹配不同位置相同部分的子串.

1
var url = "www.google.google.com";var re = /([a-z]+)\.\1/;console.log(url.replace(re,"$1"));//"www.google.com"

以上, 相同部分的”google”字符串只被替换一次.

非捕获性分组

非捕获性分组, 通常由一对括号加上”?:”加上子表达式组成, 非捕获性分组不会创建反向引用, 就好像没有括号一样. 如下:

1
var color = "#808080";var output = color.replace(/#(?:\d+)/,"$1"+"~~");console.log(RegExp.$1);//""console.log(output);//$1~~

以上, (?:\d+) 表示一个非捕获性分组, 由于分组不捕获任何内容, 所以, RegExp.$1 就指向了空字符串.

同时, 由于$1 的反向引用不存在, 因此最终它被当成了普通字符串进行替换.

实际上, 捕获性分组和无捕获性分组在搜索效率方面也没什么不同, 没有哪一个比另一个更快.

命名分组

语法: (?
…)

命名分组也是捕获性分组, 它将匹配的字符串捕获到一个组名称或编号名称中, 在获得匹配结果后, 可通过分组名进行获取. 如下是一个python的命名分组的例子.

1
import redata = "#808080"regExp = r"#(?P<one>\d+)"replaceString = "\g<one>" + "~~"print re.sub(regExp,replaceString,data) # 808080~~

python的命名分组表达式与标准格式相比, 在 ? 后多了一大写的 P 字符, 并且python通过“\g<命名>”表示法进行引用. (如果是捕获性分组, python通过”\g<编号>”表示法进行引用)

与python不同的是, javaScript 中并不支持命名分组.

固化分组

固化分组, 又叫原子组.

语法: (?>…)

如上所述, 我们在使用非贪婪模式时, 匹配过程中可能会进行多次的回溯, 回溯越多, 正则表达式的运行效率就越低. 而固化分组就是用来减少回溯次数的.

实际上, 固化分组(?>…)的匹配与正常的匹配并无分别, 它并不会改变匹配结果. 唯一的不同就是: 固化分组匹配结束时, 它匹配到的文本已经固化为一个单元, 只能作为整体而保留或放弃, 括号内的子表达式中未尝试过的备用状态都会被放弃, 所以回溯永远也不能选择其中的状态(因此不能参与回溯). 下面我们来通过一个例子更好地理解固化分组.

假如要处理一批数据, 原格式为 123.456, 因为浮点数显示问题, 部分数据格式会变为123.456000000789这种, 现要求只保留小数点后2~3位, 但是最后一位不能为0, 那么这个正则怎么写呢?

1
var str = "123.456000000789";str = str.replace(/(\.\d\d[1-9]?)\d*/,"$1"); //123.456

以上的正则, 对于”123.456” 这种格式的数据, 将白白处理一遍. 为了提高效率, 我们将正则最后的一个”*”改为”+”. 如下:

1
var str = "123.456";str = str.replace(/(\.\d\d[1-9]?)\d+/,"$1"); //123.45

此时, “\d\d[1-9]?” 子表达式, 匹配是 “45”, 而不是 “456”, 这是因为正则末尾使用了”+”, 表示末尾至少要匹配一个数字, 因此末尾的子表达式”\d+” 匹配到了 “6”. 显然 “123.45” 不是我们期望的匹配结果, 那我们应该怎么做呢? 能否让 “[1-9]?” 一旦匹配成功, 便不再进行回溯, 这里就要用到我们上面说的固化分组.

“(.\d\d(?>[1-9]?))\d+” 便是上述正则的固化分组形式. 由于字符串 “123.456” 不满足该固化分组的正则, 所以, 匹配会失败, 符合我们期望.

下面我们来分析下固化分组的正则 (.\d\d(?>[1-9]?))\d+ 为什么匹配不到字符串”123.456”.

很明显, 对于上述固化分组, 只存在两种匹配结果.

情况①: 若 [1-9] 匹配失败, 正则会返回 ? 留下的备用状态. 然后匹配脱离固化分组, 继续前进到[\d+]. 当控制权离开固化分组时, 没有备用状态需要放弃(因固化分组中根本没有创建任何备用状态).

情况②: 若 [1-9] 匹配成功, 匹配脱离固化分组之后, ? 保存的备用状态仍然存在, 但是, 由于它属于已经结束的固化分组, 所以会被抛弃.

对于字符串 “123.456”, 由于 [1-9] 能够匹配成功, 所以它符合情况②. 下面我们来还原情况②的执行现场.

  1. 匹配所处的状态: 匹配已经走到了 “6” 的位置, 匹配将继续前进;==>
  2. 子表达式 \d+ 发现无法匹配, 正则引擎便尝试回溯;==>
  3. 查看是否存在备用状态以供回溯?==>
  4. “?” 保存的备用状态属于已经结束的固化分组, 所以该备用状态会被放弃;==>
  5. 此时固化分组匹配到的 “6”, 便不能用于正则引擎的回溯;==>
  6. 尝试回溯失败;==>
  7. 正则匹配失败.==>
  8. 文本 “123.456” 没有被正则表达式匹配上, 符合预期.

相应的流程图如下:

正则表达式流程图

遗憾的是, javaScript, java 和 python中并不支持固化分组的语法, 不过, 它在php和.NET中表现良好. 下面提供了一个php版的固化分组形式的正则表达式, 以供尝试.

1
$str = "123.456";echo preg_replace("/(\.\d\d(?>[1-9]?))\d+/","\\1",$str); //固化分组

不仅如此, php还提供了占有量词优先的语法. 如下:

1
$str = "123.456";echo preg_replace("/(\.\d\d[1-9]?+)\d+/","\\1",$str); //占有量词优先

虽然java不支持固化分组的语法, 但java也提供了占有量词优先的语法, 同样能够避免正则回溯. 如下:

1
String str = "123.456";System.out.println(str.replaceAll("(\\.\\d\\d[1-9]?+)\\d+", "$1"));// 123.456

值得注意的是: java中 replaceAll 方法需要转义反斜杠.

正则表达式高阶技能-零宽断言

如果说正则分组是写轮眼, 那么零宽断言就是万花筒写轮眼终极奥义-须佐能乎(这里借火影忍术打个比方). 合理地使用零宽断言, 能够能分组之不能, 极大地增强正则匹配能力, 它甚至可以帮助你在匹配条件非常模糊的情况下快速地定位文本.

零宽断言, 又叫环视. 环视只进行子表达式的匹配, 匹配到的内容不保存到最终的匹配结果, 由于匹配是零宽度的, 故最终匹配到的只是一个位置.

环视按照方向划分, 有顺序和逆序两种(也叫前瞻和后瞻), 按照是否匹配有肯定和否定两种, 组合之, 便有4种环视. 4种环视并不复杂, 如下便是它们的描述.

字符 描述 示例
(?:pattern) 非捕获性分组, 匹配pattern的位置, 但不捕获匹配结果.也就是说不创建反向引用, 就好像没有括号一样. ‘abcd(?:e)匹配’abcde
(?=pattern) 顺序肯定环视, 匹配后面是pattern 的位置, 不捕获匹配结果. ‘Windows (?=2000)’匹配 “Windows2000” 中的 “Windows”; 不匹配 “Windows3.1” 中的 “Windows”
(?!pattern) 顺序否定环视, 匹配后面不是 pattern 的位置, 不捕获匹配结果. ‘Windows (?!2000)’匹配 “Windows3.1” 中的 “Windows”; 不匹配 “Windows2000” 中的 “Windows”
(?<=pattern) 逆序肯定环视, 匹配前面是 pattern 的位置, 不捕获匹配结果. ‘(?<=Office)2000’匹配 “ Office2000” 中的 “2000”; 不匹配 “Windows2000” 中的 “2000”
(?<!pattern) 逆序否定环视, 匹配前面不是 pattern 的位置, 不捕获匹配结果. ‘(?<!Office)2000’匹配 “ Windows2000” 中的 “2000”; 不匹配 “ Office2000” 中的 “2000”

非捕获性分组由于结构与环视相似, 故列在表中, 以做对比. 以上4种环视中, 目前 javaScript 中只支持前两种, 也就是只支持 顺序肯定环视 和 顺序否定环视. 下面我们通过实例来帮助理解下:

1
var str = "123abc789",s;//没有使用环视,abc直接被替换s = str.replace(/abc/,456);console.log(s); //123456789//使用了顺序肯定环视,捕获到了a前面的位置,所以abc没有被替换,只是将3替换成了3456s = str.replace(/3(?=abc)/,3456);console.log(s); //123456abc789//使用了顺序否定环视,由于3后面跟着abc,不满意条件,故捕获失败,所以原字符串没有被替换s = str.replace(/3(?!abc)/,3456);console.log(s); //123abc789

下面通过python来演示下 逆序肯定环视 和 逆序否定环视 的用法.

1
import redata = "123abc789"# 使用了逆序肯定环视,替换左边为123的连续的小写英文字母,匹配成功,故abc被替换为456regExp = r"(?<=123)[a-z]+"replaceString = "456"print re.sub(regExp,replaceString,data) # 123456789# 使用了逆序否定环视,由于英文字母左侧不能为123,故子表达式[a-z]+捕获到bc,最终bc被替换为456regExp = r"(?<!123)[a-z]+"replaceString = "456"print re.sub(regExp,replaceString,data) # 123a456789

需要注意的是: python 和 perl 语言中的 逆序环视 的子表达式只能使用定长的文本. 比如将上述 “(?<=123)” (逆序肯定环视)子表达式写成 “(?<=[0-9]+)”, python解释器将会报错: “error: look-behind requires fixed-width pattern”.

场景回顾

获取html片段

假如现在, js 通过 ajax 获取到一段 html 代码如下:

1
var responseText = "<div data='dev.xxx.txt'></div><img src='dev.xxx.png' />";

现我们需要替换img标签的src 属性中的 “dev”字符串 为 “test” 字符串.

① 由于上述 responseText 字符串中包含至少两个子字符串 “dev”, 显然不能直接 replace 字符串 “dev”为 “test”.

② 同时由于 js 中不支持逆序环视, 我们也不能在正则中判断前缀为 “src=’”, 然后再替换”dev”.

③ 我们注意到 img 标签的 src 属性以 “.png” 结尾, 基于此, 就可以使用顺序肯定环视. 如下:

1
var reg = /dev(?=[^']*png)/; //为了防止匹配到第一个dev, 通配符前面需要排除单引号或者是尖括号var str = responseText.replace(reg,"test");console.log(str);//<div data='dev.xxx'></div><img src='test.xxx.png' />

当然, 以上不止顺序肯定环视一种解法, 捕获性分组同样可以做到. 那么环视高级在哪里呢? 环视高级的地方就在于它通过一次捕获就可以定位到一个位置, 对于复杂的文本替换场景, 常有奇效, 而分组则需要更多的操作. 请往下看.

千位分割符

千位分隔符, 顾名思义, 就是数字中的逗号. 参考西方的习惯, 数字之中加入一个符号, 避免因数字太长难以直观的看出它的值. 故而数字之中, 每隔三位添加一个逗号, 即千位分隔符.

那么怎么将一串数字转化为千位分隔符形式呢?

1
var str = "1234567890";(+str).toLocaleString();//"1,234,567,890"

如上, toLocaleString() 返回当前对象的”本地化”字符串形式.

  • 如果该对象是Number类型, 那么将返回该数值的按照特定符号分割的字符串形式.
  • 如果该对象是Array类型, 那么先将数组中的每项转化为字符串, 然后将这些字符串以指定分隔符连接起来并返回.

toLocaleString 方法特殊, 有本地化特性, 对于天朝, 默认的分隔符是英文逗号. 因此使用它恰好可以将数值转化为千位分隔符形式的字符串. 如果考虑到国际化, 以上方法就有可能会失效了.

我们尝试使用环视来处理下.

1
function thousand(str){  return str.replace(/(?!^)(?=([0-9]{3})+$)/g,',');}console.log(thousand(str));//"1,234,567,890"console.log(thousand("123456"));//"123,456"console.log(thousand("1234567879876543210"));//"1,234,567,879,876,543,210"

上述使用到的正则分为两块. (?!^) 和 (?=([0-9]{3})+$). 我们先来看后面的部分, 然后逐步分析之.

  1. “[0-9]{3}” 表示连续3位数字.
  2. “([0-9]{3})+” 表示连续3位数字至少出现一次或更多次.
  3. “([0-9]{3})+$” 表示连续3的正整数倍的数字, 直到字符串末尾.
  4. 那么 (?=([0-9]{3})+$) 就表示匹配一个零宽度的位置, 并且从这个位置到字符串末尾, 中间拥有3的正整数倍的数字.
  5. 正则表达式使用全局匹配g, 表示匹配到一个位置后, 它会继续匹配, 直至匹配不到.
  6. 将这个位置替换为逗号, 实际上就是每3位数字添加一个逗号.
  7. 当然对于字符串”123456”这种刚好拥有3的正整数倍的数字的, 当然不能在1前面添加逗号. 那么使用 (?!^) 就指定了这个替换的位置不能为起始位置.

千位分隔符实例, 展示了环视的强大, 一步到位.

正则表达式在JS中的应用

ES6对正则的扩展

ES6对正则扩展了又两种修饰符(其他语言可能不支持):

  • y (粘连sticky修饰符), 与g类似, 也是全局匹配, 并且下一次匹配都是从上一次匹配成功的下一个位置开始, 不同之处在于, g修饰符只要剩余位置中存在匹配即可, 而y修饰符确保匹配必须从剩余的第一个位置开始.
1
var s = "abc_ab_a";var r1 = /[a-z]+/g;var r2 = /[a-z]+/y;console.log(r1.exec(s),r1.lastIndex); // ["abc", index: 0, input: "abc_ab_a"] 3console.log(r2.exec(s),r2.lastIndex); // ["abc", index: 0, input: "abc_ab_a"] 3console.log(r1.exec(s),r1.lastIndex); // ["ab", index: 4, input: "abc_ab_a"] 6console.log(r2.exec(s),r2.lastIndex); // null 0

如上, 由于第二次匹配的开始位置是下标3, 对应的字符串是 “_”, 而使用y修饰符的正则对象r2, 需要从剩余的第一个位置开始, 所以匹配失败, 返回null.

正则对象的 sticky 属性, 表示是否设置了y修饰符. 这点将会在后面讲到.

  • u 修饰符, 提供了对正则表达式添加4字节码点的支持. 比如 “𝌆” 字符是一个4字节字符, 直接使用正则匹配将会失败, 而使用u修饰符后, 将会等到正确的结果.
1
var s = "𝌆";console.log(/^.$/.test(s));//falseconsole.log(/^.$/u.test(s));//true
UCS-2字节码

有关字节码点, 稍微提下. javaScript 只能处理UCS-2编码(js于1995年5月被Brendan Eich花费10天设计出来, 比1996年7月发布的编码规范UTF-16早了一年多, 当时只有UCS-2可选). 由于UCS-2先天不足, 造成了所有字符在js中都是2个字节. 如果是4个字节的字符, 将会默认被当作两个双字节字符处理. 因此 js 的字符处理函数都会受到限制, 无法返回正确结果. 如下:

1
var s = "𝌆";console.log(s == "\uD834\uDF06");//true 𝌆相当于UTF-16中的0xD834DF06console.log(s.length);//2 长度为2, 表示这是4字节字符

幸运的是, ES6可以自动识别4字节的字符.因此遍历字符串可以直接使用for of循环. 同时, js中如果直接使用码点表示Unicode字符, 对于4字节字符, ES5里是没办法识别的. 为此ES6修复了这个问题, 只需将码点放在大括号内即可.

1
console.log(s === "\u1D306");//false   ES5无法识别𝌆console.log(s === "\u{1D306}");//true  ES6可以借助大括号识别𝌆
附: ES6新增的处理4字节码的函数
  • String.fromCodePoint():从Unicode码点返回对应字符
  • String.prototype.codePointAt():从字符返回对应的码点
  • String.prototype.at():返回字符串给定位置的字符

有关js中的unicode字符集, 请参考阮一峰老师的 Unicode与JavaScript详解.

以上是ES6对正则的扩展. 另一个方面, 从方法上看, javaScript 中与正则表达式有关的方法有:

方法名 compile test exec match search replace split
所属对象 RegExp RegExp RegExp String String String String

由上, 一共有7个与js相关的方法, 这些方法分别来自于 RegExp 与 String 对象. 首先我们先来看看js中的正则类 RegExp.

RegExp

RegExp 对象表示正则表达式, 主要用于对字符串执行模式匹配.

语法: new RegExp(pattern[, flags])

参数 pattern 是一个字符串, 指定了正则表达式字符串或其他的正则表达式对象.

参数 flags 是一个可选的字符串, 包含属性 “g”、”i” 和 “m”, 分别用于指定全局匹配、区分大小写的匹配和多行匹配. 如果pattern 是正则表达式, 而不是字符串, 则必须省略该参数.

1
var pattern = "[0-9]";var reg = new RegExp(pattern,"g");// 上述创建正则表达式对象,可以用对象字面量形式代替,也推荐下面这种var reg = /[0-9]/g;

以上, 通过对象字面量和构造函数创建正则表达式, 有个小插曲.

“对于正则表达式的直接量, ECMAscript 3规定在每次它时都会返回同一个RegExp对象, 因此用直接量创建的正则表达式的会共享一个实例. 直到ECMAScript 5才规定每次返回不同的实例.”

所以, 现在我们基本不用担心这个问题, 只需要注意在低版本的非IE浏览器中尽量使用构造函数创建正则(这点上, IE一直遵守ES5规定, 其他浏览器的低级版本遵循ES3规定).

RegExp 实例对象包含如下属性:

实例属性 描述
global 是否包含全局标志(true/false)
ignoreCase 是否包含区分大小写标志(true/false)
multiline 是否包含多行标志(true/false)
source 返回创建RegExp对象实例时指定的表达式文本字符串形式
lastIndex 表示原字符串中匹配的字符串末尾的后一个位置, 默认为0
flags(ES6) 返回正则表达式的修饰符
sticky(ES6) 是否设置了y(粘连)修饰符(true/false)
compile

compile 方法用于在执行过程中改变和重新编译正则表达式.

语法: compile(pattern[, flags])

参数介绍请参考上述 RegExp 构造器. 用法如下:

1
var reg = new RegExp("abc", "gi"); var reg2 = reg.compile("new abc", "g");console.log(reg);// /new abc/gconsole.log(reg2);// undefined

可见 compile 方法会改变原正则表达式对象, 并重新编译, 而且它的返回值为空.

test

test 方法用于检测一个字符串是否匹配某个正则规则, 只要是字符串中含有与正则规则匹配的文本, 该方法就返回true, 否则返回 false.

语法: test(string), 用法如下:

1
console.log(/[0-9]+/.test("abc123"));//trueconsole.log(/[0-9]+/.test("abc"));//false

以上, 字符串”abc123” 包含数字, 故 test 方法返回 true; 而 字符串”abc” 不包含数字, 故返回 false.

如果需要使用 test 方法测试字符串是否完成匹配某个正则规则, 那么可以在正则表达式里增加开始(^)和结束($)元字符. 如下:

1
console.log(/^[0-9]+$/.test("abc123"));//false

以上, 由于字符串”abc123” 并非以数字开始, 也并非以数字结束, 故 test 方法返回false.

实际上, 如果正则表达式带有全局标志(带有参数g)时, test 方法还受正则对象的lastIndex属性影响,如下:

1
var reg = /[a-z]+/;//正则不带全局标志console.log(reg.test("abc"));//trueconsole.log(reg.test("de"));//truevar reg = /[a-z]+/g;//正则带有全局标志gconsole.log(reg.test("abc"));//trueconsole.log(reg.lastIndex);//3, 下次运行test时,将从索引为3的位置开始查找console.log(reg.test("de"));//false

该影响将在exec 方法讲解中予以分析.

exec

exec 方法用于检测字符串对正则表达式的匹配, 如果找到了匹配的文本, 则返回一个结果数组, 否则返回null.

语法: exec(string)

exec 方法返回的数组中包含两个额外的属性, index 和 input. 并且该数组具有如下特点:

  • 第 0 个项表示正则表达式捕获的文本
  • 第 1n 项表示第 1n 个反向引用, 依次指向第 1n 个分组捕获的文本, 可以使用RegExp.$ + “编号1n” 依次获取分组中的文本
  • index 表示匹配字符串的初始位置
  • input 表示正在检索的字符串

无论正则表达式有无全局标示”g”, exec 的表现都相同. 但正则表达式对象的表现却有些不同. 下面我们来详细说明下正则表达式对象的表现都有哪些不同.

假设正则表达式对象为 reg , 检测的字符为 string , reg.exec(string) 返回值为 array.

若 reg 包含全局标示”g”, 那么 reg.lastIndex 属性表示原字符串中匹配的字符串末尾的后一个位置, 即下次匹配开始的位置, 此时 reg.lastIndex == array.index(匹配开始的位置) + array[0].length(匹配字符串的长度). 如下:

1
var reg = /([a-z]+)/gi,    string = "World Internet Conference";var array = reg.exec(string);console.log(array);//["World", "World", index: 0, input: "World Internet Conference"]console.log(RegExp.$1);//Worldconsole.log(reg.lastIndex);//5, 刚好等于 array.index + array[0].length

随着检索继续, array.index 的值将往后递增, 也就是说, reg.lastIndex 的值也会同步往后递增. 因此, 我们也可以通过反复调用 exec 方法来遍历字符串中所有的匹配文本. 直到 exec 方法再也匹配不到文本时, 它将返回 null, 并把 reg.lastIndex 属性重置为 0.

接着上述例子, 我们继续执行代码, 看看上面说的对不对, 如下所示:

1
array = reg.exec(string);console.log(array);//["Internet", "Internet", index: 6, input: "World Internet Conference"]console.log(reg.lastIndex);//14array = reg.exec(string);console.log(array);//["Conference", "Conference", index: 15, input: "World Internet Conference"]console.log(reg.lastIndex);//25array = reg.exec(string);console.log(array);//nullconsole.log(reg.lastIndex);//0

以上代码中, 随着反复调用 exec 方法, reg.lastIndex 属性最终被重置为 0.

问题回顾

在 test 方法的讲解中, 我们留下了一个问题. 如果正则表达式带有全局标志g, 以上 test 方法的执行结果将受 reg.lastIndex影响, 不仅如此, exec 方法也一样. 由于 reg.lastIndex 的值并不总是为零, 并且它决定了下次匹配开始的位置, 如果在一个字符串中完成了一次匹配之后要开始检索新的字符串, 那就必须要手动地把 lastIndex 属性重置为 0. 避免出现下面这种错误:

1
var reg = /[0-9]+/g,    str1 = "123abc",    str2 = "123456";reg.exec(str1);console.log(reg.lastIndex);//3var array = reg.exec(str2);console.log(array);//["456", index: 3, input: "123456"]

以上代码, 正确执行结果应该是 “123456”, 因此建议在第二次执行 exec 方法前, 增加一句 “reg.lastIndex = 0;”.

若 reg 不包含全局标示”g”, 那么 exec 方法的执行结果(array)将与 string.match(reg) 方法执行结果完全相同.

String

match, search, replace, split 方法请参考 字符串常用方法 中的讲解.

如下展示了使用捕获性分组处理文本模板, 最终生成完整字符串的过程:

1
var tmp = "An ${a} a ${b} keeps the ${c} away";var obj = {  a:"apple",  b:"day",  c:"doctor"};function tmpl(t,o){  return t.replace(/\${(.)}/g,function(m,p){    console.log('m:'+m+' p:'+p);    return o[p];  });}tmpl(tmp,obj);

上述功能使用ES6可这么实现:

1
var obj = {  a:"apple",  b:"day",  c:"doctor"};with(obj){  console.log(`An ${a} a ${b} keeps the ${c} away`);}

正则表达式在H5中的应用

H5中新增了 pattern 属性, 规定了用于验证输入字段的模式, pattern的模式匹配支持正则表达式的书写方式. 默认 pattern 属性是全部匹配, 即无论正则表达式中有无 “^”, “$” 元字符, 它都是匹配所有文本.

注: pattern 适用于以下 input 类型:text, search, url, telephone, email 以及 password. 如果需要取消表单验证, 在form标签上增加 novalidate 属性即可.

正则引擎

目前正则引擎有两种, DFA 和 NFA, NFA又可以分为传统型NFA和POSIX NFA.

  • DFA Deterministic finite automaton 确定型有穷自动机
  • NFA Non-deterministic finite automaton 非确定型有穷自动机
  • Traditional NFA
  • POSIX NFA

DFA引擎不支持回溯, 匹配快速, 并且不支持捕获组, 因此也就不支持反向引用. 上述awk, egrep命令均支持 DFA引擎.

POSIX NFA主要指符合POSIX标准的NFA引擎, 像 javaScript, java, php, python, c#等语言均实现了NFA引擎.

有关正则表达式详细的匹配原理, 暂时没在网上看到适合的文章, 建议选读 Jeffrey Friedl 的 <精通正则表达式>[第三版] 中第4章-表达式的匹配原理(p143-p183), Jeffrey Friedl 对正则表达式有着深刻的理解, 相信他能够帮助您更好的学习正则.

有关NFA引擎的简单实现, 可以参考文章 基于ε-NFA的正则表达式引擎 - twoon.

总结

在学习正则的初级阶段, 重在理解 ①贪婪与非贪婪模式, ②分组, ③捕获性与非捕获性分组, ④命名分组, ⑤固化分组, 体会设计的精妙之处. 而高级阶段, 主要在于熟练运用⑥零宽断言(或环视)解决问题, 并且熟悉正则匹配的原理.

实际上, 正则在 javaScript 中的功能不算强大, js 仅仅支持了①贪婪与非贪婪模式, ②分组, ③捕获性与非捕获性分组 以及 ⑥零宽断言中的顺序环视. 如果再稍微熟悉些 js 中7种与正则有关的方法(compile, test, exec, match, search, replace, split), 那么处理文本或字符串将游刃有余.

正则表达式, 在文本处理方面天赋异禀, 它的功能十分强大, 很多时候甚至是唯一解决方案. 正则不局限于js, 当下热门的编辑器(比如Sublime, Atom) 以及 IDE(比如WebStorm, IntelliJ IDEA) 都支持它. 您甚至可以在任何时候任何语言中, 尝试使用正则解决问题, 也许之前不能解决的问题, 现在可以轻松的解决.

附其他语言正则资料:

  • Python正则表达式操作指南
  • java正则表达式

本文就讨论这么多内容,大家有什么问题或好的想法欢迎在下方参与留言和评论.

本文作者: louis

本文简介: 本文断断续续历时两个月而成, 共计12k字, 为求简洁全面地还原前端场景中正则的使用规律, 搜集了大量正则相关资料, 并剔除不少冗余字句, 码字不易, 喜欢的请点个赞👍或者收藏, 我将持续保持更新.

本文链接: louiszhai.github.io/2016/06/13/…

参考文章

  • Jeffrey Friedl 的 <精通正则表达式>[第三版]
  • linux shell 正则表达式(BREs,EREs,PREs)差异比较
  • 正则表达式之捕获组/非捕获组介绍_正则表达式_脚本之家
  • 正则表达式(一) – 元字符 - 逆心 - 博客园
  • 正则表达式详解 - guisu,程序人生。 逆水行舟,不进则退。 - 博客频道 - CSDN.NET
  • 正则表达式之固化分组 - taek - 博客园
  • 正则表达式之 贪婪与非贪婪模式详解(概述)_正则表达式_脚本之家
  • JAVASCRIPT 正则表达式学习–>基础与零宽断言(转自司徒正美) - 随风之羽 - 博客频道 - CSDN.NET
  • Unicode与JavaScript详解 - 阮一峰的网络日志

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…398399

开发者博客

3990 日志
1304 标签
RSS
© 2024 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%