笔试专题
牛客网输入输出
输入
//readline()读取一行字符串,split(' ')按空格拆分为数组
let l=readline().split(' ');
//将数组中的字符串数据转为数字类型
let n=parseInt(l[0]);
let m=parseInt(l[1]);
输出
//两种写法效果一样,输出a b的值
console.log(a,b);
print(a,b);
关于字符串
关于数组
方法
1.push、pop、unshift、shift
2.sort、reverse、reduce、
forEach(回调函数)
forEach 方法中的 function 回调有三个参数: 第一个参数是遍历的数组内容, 第二个参数是对应的数组索引, 第三个参数是数组本身
3.slice、splice、
4.concat、join
5.toString
6.valueOf
关于对象
利用对象统计元素出现次数
元素可以是数字也可以是字符、字符串。注意用 obj[foo]来调用属性。
l = readline().split(" ");
let obj = {};
for (let i = 0; i < n; i++) {
if (obj[l[i]]) {
obj[l[i]]++;
} else {
obj[l[i]] = 1;
}
}
调用时
obj[待求元素] --> 返回出现次数
遍历对象
也可以用于遍历数组
for (const key in obj) {
res[key] = clone(obj[key]);
}
关于数字
交换两个变量的值
[a, b] = [b, a];
取小数点
num.toFixed(2) --> num=2时,返回2.00
手写专题
call、apply、bind
模拟实现 call
- 1.判断当前
this
是否为函数,防止Function.prototype.myCall()
直接调用 - 2.
context
为可选参数,如果不传的话默认上下文为window
- 3.为
context
创建一个Symbol
(保证不会重名)属性,将当前函数赋值给这个属性 - 4.处理参数,传入第一个参数后的其余参数
- 4.调用函数后即删除该
Symbol
属性
Function.prototype.myCall = function (context = window, ...args) {
// 用于防止Function.prototype.myCall() 直接调用
//(这块我不懂,建议不写
if (this === Function.prototype) {
return undefined;
}
//正式开始
context = context || window;
const fn = Symbol();
//关键步骤:此时的this指向调用myCall的函数,相当于为context添加了一个属性,值为调用myCall的函数
context[fn] = this;
//接着立即执行这个函数,执行时的上下文即为context
const result = context[fn](...args);
delete context[fn];
return result;
};
模拟实现 apply
apply
实现类似call
,参数为数组
Function.prototype.myApply = function (context = window, args) {
if (this === Function.prototype) {
return undefined;
}
const fn = Symbol();
context[fn] = this;
let result;
if (Array.isArray(args)) {
result = context[fn](...args);
} else {
result = context[fn]();
}
delete context[fn];
return result;
};
模拟实现 bind
- 1.处理参数,返回一个闭包
- 2.判断是否为构造函数调用,如果是则使用
new
调用当前函数 - 3.如果不是,使用
apply
,将context
和处理好的参数传入
Function.prototype.myBind = function (context, ...args1) {
if (this === Function.prototype) {
throw new TypeError("Error");
}
//保存此时的this指向,指向为调用myBind的函数
const _this = this;
return function F(...args2) {
//判断是否用于构造函数(这个判断不太懂,建议不写
//此时this的指向为调用F的对象,正常应该是window
if (this instanceof F) {
return new _this(...args1, ...args2);
}
return _this.apply(context, args1.concat(args2));
};
};
调用例子:
扩展
获取函数中的参数:
// 获取argument对象 类数组对象 不能调用数组方法
function test1() {
console.log("获取argument对象 类数组对象 不能调用数组方法", arguments);
}
// 获取参数数组 可以调用数组方法
function test2(...args) {
console.log("获取参数数组 可以调用数组方法", args);
}
// 获取除第一个参数的剩余参数数组
function test3(first, ...args) {
console.log("获取argument对象 类数组对象 不能调用数组方法", args);
}
// 透传参数
function test4(first, ...args) {
fn(...args);
fn(...arguments);
}
function fn() {
console.log("透传", ...arguments);
}
test1(1, 2, 3);
test2(1, 2, 3);
test3(1, 2, 3);
test4(1, 2, 3);
防抖与节流
防抖
原理
防抖(debounce
):不管事件触发频率多高,一定在事件触发n
秒后才执行,如果你在一个事件触发的 n
秒内又触发了这个事件,就以新的事件的时间为准,n
秒后才执行,总之,触发完事件 n
秒内不再触发事件,n
秒后再执行。
应用场景
窗口大小变化,调整样式
window.addEventListener("resize", debounce(handleResize, 200));
搜索框,输入后 1000 毫秒搜索
debounce(fetchSelectData, 300);
表单验证,输入 1000 毫秒后验证
debounce(validator, 1000);
实现
注意考虑两个问题:
- 在
debounce
函数中返回一个闭包,这里用的普通function
,里面的setTimeout
则用的箭头函数,这样做的意义是让this
的指向准确,this
的真实指向并非debounce
的调用者,而是返回闭包的调用者。 - 当我们不使用防抖处理时,
handle()
函数的this
指向调用此函数的 container,而在外层使用防抖处理后,this 的指向会变成 Window。其次,我们也要获取到事件对象event
。 - let timer =null 只会在第一次主线程执行时被调用(即使没有事件触发),后面会直接调用闭包函数
function debounce(event, time) {
let timer = null;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
//修正this指向,从 window 修正为 调用元素
event.apply(this, args);
}, time);
};
}
s;
function handle(e) {
//不修正时this为window,修正后this为发起调用的元素
console.log(this);
//不修正时e为undefined,修正后为事件对象event
console.log(e);
}
调用:
container.addEventListener("click", debounce(handle, 1000));
有时候我们需要让函数立即执行一次,再等后面事件触发后等待n
秒执行,我们给debounce
函数一个flag
用于标示是否立即执行。
当定时器变量timer
为空时,说明是第一次执行,我们立即执行它。
function debounce(event, time, flag) {
let timer = null;
return function (...args) {
clearTimeout(timer);
if (flag && !timer) {
event.apply(this, args);
}
timer = setTimeout(() => {
event.apply(this, args);
}, time);
};
}
节流
定义
节流(throttle
):不管事件触发频率多高,只在单位时间内执行一次。
实现
有两种方式可以实现节流,使用时间戳和定时器。
时间戳实现
第一次事件肯定触发,最后一次不会触发
function throttle(event, time) {
let pre = 0;
return function (...args) {
if (Date.now() - pre > time) {
pre = Date.now();
event.apply(this, args);
}
}
定时器实现
第一次事件不会触发,最后一次一定触发
function throttle(event, time) {
let timer = null;
return function (...args) {
if (!timer) {
timer = setTimeout(() => {
timer = null;
event.apply(this, args);
}, time);
}
};
}
结合版
定时器和时间戳的结合版,也相当于节流和防抖的结合版,第一次和最后一次都会触发
function throttle(event, time) {
let pre = 0;
let timer = null;
return function (...args) {
if (Date.now() - pre > time) {
clearTimeout(timer);
timer = null;
pre = Date.now();
event.apply(this, args);
} else if (!timer) {
timer = setTimeout(() => {
event.apply(this, args);
}, time);
}
};
}
深拷贝
深拷贝和浅拷贝的定义
浅拷贝:
创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
我们用很多简单的方法都能实现浅拷贝:
arr.slice();
arr.concat();
深拷贝:
将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象
话不多说,浅拷贝就不再多说,下面我们直入正题:
JSON 版
在不使用第三方库的情况下,我们想要深拷贝一个对象,用的最多的就是下面这个方法。
JSON.parse(JSON.stringify());
这种写法非常简单,而且可以应对大部分的应用场景,但是它还是有很大缺陷的,比如拷贝其他引用类型、拷贝函数、循环引用等情况。
基础递归版本
如果是浅拷贝的话,我们可以很容易写出下面的代码:
function clone(target) {
let cloneTarget = {};
for (const key in target) {
cloneTarget[key] = target[key];
}
return cloneTarget;
}
创建一个新的对象,遍历需要克隆的对象,将需要克隆对象的属性依次添加到新对象上,返回。
如果是深拷贝的话,考虑到我们要拷贝的对象是不知道有多少层深度的,我们可以用递归来解决问题,稍微改写上面的代码:
- 如果是原始类型,无需继续拷贝,直接返回
- 如果是引用类型,创建一个新的对象,遍历需要克隆的对象,将需要克隆对象的属性执行深拷贝后依次添加到新对象上。
很容易理解,如果有更深层次的对象可以继续递归直到属性为原始类型,这样我们就完成了一个最简单的深拷贝:
module.exports = function clone(target) {
if (typeof target === "object") {
let cloneTarget = Array.isArray(target) ? [] : {};
for (const key in target) {
cloneTarget[key] = clone(target[key]);
}
return cloneTarget;
} else {
return target;
}
};
通过 lodash 函数库实现深拷贝
lodash 是当前很热门的函数库,提供了 _.cloneDeep()可以快速实现对象或数组的深拷贝
// 引入loadsh函数库
import _ from "loadsh";
// 使用cloneDeep实现深拷贝
var obj = [{ a: 1 }, { b: 2 }];
var deep = _.cloneDeep(obj);
console.log(deep[0] === objects[0]); // => false
数组去重、扁平、最值、排序
去重
一、利用 ES6 Set 去重(ES6 中最常用)
function unique(arr) {
return Array.from(new Set(arr));
}
var arr = [
1,
1,
"true",
"true",
true,
true,
15,
15,
false,
false,
undefined,
undefined,
null,
null,
NaN,
NaN,
"NaN",
0,
0,
"a",
"a",
{},
{},
];
console.log(unique(arr));
//[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {}, {}]
不考虑兼容性,这种去重的方法代码最少。这种方法还无法去掉“{}”空对象,后面的高阶方法会添加去掉重复“{}”的方法。
二、暴力双层循环(ES5 中最常用)
function unique(arr) {
for (var i = 0; i < arr.length; i++) {
for (var j = i + 1; j < arr.length; j++) {
if (arr[i] == arr[j]) {
//第一个等于第二个,splice方法删除第二个
arr.splice(j, 1);
j--;
}
}
}
return arr;
}
var arr = [
1,
1,
"true",
"true",
true,
true,
15,
15,
false,
false,
undefined,
undefined,
null,
null,
NaN,
NaN,
"NaN",
0,
0,
"a",
"a",
{},
{},
];
console.log(unique(arr));
//[1, "true", 15, false, undefined, NaN, NaN, "NaN", "a", {…}, {…}]
//NaN和{}没有去重,两个null直接消失了
三、利用 indexOf 去重
function unique(arr) {
if (!Array.isArray(arr)) {
console.log("type error!");
return;
}
var array = [];
for (var i = 0; i < arr.length; i++) {
//indexOf查不到就会返回-1,否则返回命中的第一个目标值
if (array.indexOf(arr[i]) === -1) {
array.push(arr[i]);
}
}
return array;
}
var arr = [
1,
1,
"true",
"true",
true,
true,
15,
15,
false,
false,
undefined,
undefined,
null,
null,
NaN,
NaN,
"NaN",
0,
0,
"a",
"a",
{},
{},
];
console.log(unique(arr));
// [1, "true", true, 15, false, undefined, null, NaN, NaN, "NaN", 0, "a", {…}, {…}]
//NaN、{}没有去重
新建一个空的结果数组,for 循环原数组,判断结果数组是否存在当前元素,如果有相同的值则跳过,不相同则 push 进数组。
四、利用 sort()
function unique(arr) {
if (!Array.isArray(arr)) {
console.log("type error!");
return;
}
arr = arr.sort();
var array = [arr[0]];
for (var i = 1; i < arr.length; i++) {
if (arr[i] !== arr[i - 1]) {
arrry.push(arr[i]);
}
}
return arrry;
}
var arr = [
1,
1,
"true",
"true",
true,
true,
15,
15,
false,
false,
undefined,
undefined,
null,
null,
NaN,
NaN,
"NaN",
0,
0,
"a",
"a",
{},
{},
];
console.log(unique(arr));
// [0, 1, 15, "NaN", NaN, NaN, {…}, {…}, "a", false, null, true, "true", undefined]
//NaN、{}没有去重
利用 sort()排序方法,然后根据排序后的结果进行遍历及相邻元素比对。
五、利用对象的属性不能相同的特点进行去重
(这种数组去重的方法有问题,不建议用,有待改进。但可以顺便统计元素出现顺序)
function unique(arr) {
if (!Array.isArray(arr)) {
console.log("type error!");
return;
}
var arrry = [];
var obj = {};
for (var i = 0; i < arr.length; i++) {
if (!obj[arr[i]]) {
arrry.push(arr[i]);
obj[arr[i]] = 1;
} else {
obj[arr[i]]++;
}
}
return arrry;
}
var arr = [
1,
1,
"true",
"true",
true,
true,
15,
15,
false,
false,
undefined,
undefined,
null,
null,
NaN,
NaN,
"NaN",
0,
0,
"a",
"a",
{},
{},
];
console.log(unique(arr));
//[1, "true", 15, false, undefined, null, NaN, 0, "a", {…}]
//两个true直接去掉了,NaN和{}去重
六、利用 includes
includes 是数组的方法,这种类似三、indexOf 去重
function unique(arr) {
if (!Array.isArray(arr)) {
console.log("type error!");
return;
}
var array = [];
for (var i = 0; i < arr.length; i++) {
if (!array.includes(arr[i])) {
//includes 检测数组是否有某个值
array.push(arr[i]);
}
}
return array;
}
var arr = [
1,
1,
"true",
"true",
true,
true,
15,
15,
false,
false,
undefined,
undefined,
null,
null,
NaN,
NaN,
"NaN",
0,
0,
"a",
"a",
{},
{},
];
console.log(unique(arr));
//[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}, {…}] //{}没有去重
七、利用 hasOwnProperty
function unique(arr) {
var obj = {};
return arr.filter(function (item, index, arr) {
return obj.hasOwnProperty(typeof item + item)
? false
: (obj[typeof item + item] = true);
});
}
var arr = [
1,
1,
"true",
"true",
true,
true,
15,
15,
false,
false,
undefined,
undefined,
null,
null,
NaN,
NaN,
"NaN",
0,
0,
"a",
"a",
{},
{},
];
console.log(unique(arr));
//[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}] //所有的都去重了
利用 hasOwnProperty 判断是否存在对象属性
八、利用 filter
function unique(arr) {
return arr.filter(function (item, index, arr) {
//当前元素在原始数组中的第一个索引==当前索引值,否则返回当前元素
return arr.indexOf(item, 0) === index;
});
}
var arr = [
1,
1,
"true",
"true",
true,
true,
15,
15,
false,
false,
undefined,
undefined,
null,
null,
NaN,
NaN,
"NaN",
0,
0,
"a",
"a",
{},
{},
];
console.log(unique(arr));
//[1, "true", true, 15, false, undefined, null, "NaN", 0, "a", {…}, {…}]
九、利用递归去重
function unique(arr) {
var array = arr;
var len = array.length;
array.sort(function (a, b) {
//排序后更加方便去重
return a - b;
});
function loop(index) {
if (index >= 1) {
if (array[index] === array[index - 1]) {
array.splice(index, 1);
}
loop(index - 1); //递归loop,然后数组去重
}
}
loop(len - 1);
return array;
}
var arr = [
1,
1,
"true",
"true",
true,
true,
15,
15,
false,
false,
undefined,
undefined,
null,
null,
NaN,
NaN,
"NaN",
0,
0,
"a",
"a",
{},
{},
];
console.log(unique(arr));
//[1, "a", "true", true, 15, false, 1, {…}, null, NaN, NaN, "NaN", 0, "a", {…}, undefined]
十、利用 Map 数据结构去重
function arrayNonRepeatfy(arr) {
let map = new Map();
let array = new Array(); // 数组用于返回结果
for (let i = 0; i < arr.length; i++) {
if(map .has(arr[i])) { // 如果有该key值
map .set(arr[i], true);
} else {
map .set(arr[i], false); // 如果没有该key值
array .push(arr[i]);
}
}
return array ;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
//[1, "a", "true", true, 15, false, 1, {…}, null, NaN, NaN, "NaN", 0, "a", {…}, undefined]
创建一个空 Map 数据结构,遍历需要去重的数组,把数组的每一个元素作为 key 存到 Map 中。由于 Map 中不会出现相同的 key 值,所以最终得到的就是去重后的结果。
十一、利用 reduce+includes
function unique(arr){
return arr.reduce((prev,cur) => prev.includes(cur) ? prev : [...prev,cur],[]);
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr));
// [1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}, {…}]
十二、[...new Set(arr)]
[...new Set(arr)]
//代码就是这么少----(其实,严格来说并不算是一种,相对于第一种方法来说只是简化了代码)
扁平
基本实现
const flat = (array) => {
let result = [];
for (let i = 0; i < array.length; i++) {
if (Array.isArray(array[i])) {
result = result.concat(flat(array[i]));
} else {
result.push(array[i]);
}
}
return result;
};
使用 reduce 简化
function flatten(array) {
return array.reduce(
(target, current) =>
Array.isArray(current)
? target.concat(flatten(current))
: target.concat(current),
[]
);
}
根据指定深度扁平数组
function flattenByDeep(array, deep = 1) {
return array.reduce(
(target, current) =>
Array.isArray(current) && deep > 1
? target.concat(flattenByDeep(current, deep - 1))
: target.concat(current),
[]
);
}
最值
排序
快速排序 不稳定 o(nlogn)
实现步骤:
- 选择一个基准元素
target
(一般选择第一个数) - 将比
target
小的元素移动到数组左边,比target
大的元素移动到数组右边 - 分别对
target
左侧和右侧的元素进行快速排序
从上面的步骤中我们可以看出,快速排序也利用了分治的思想(将问题分解成一些小问题递归求解)
下面是对序列6、1、2、7、9、3、4、5、10、8
排序的过程:
 {
if (array.length < 2) {
return array;
}
const target = array[0];
const left = [];
const right = [];
for (let i = 1; i < array.length; i++) {
if (array[i] < target) {
left.push(array[i]);
} else {
right.push(array[i]);
}
}
return quickSort(left).concat([target], quickSort(right));
}
写法 2
记录一个索引l
从数组最左侧开始,记录一个索引r
从数组右侧开始
在l<r
的条件下,找到右侧小于target
的值array[r]
,并将其赋值到array[l]
在l<r
的条件下,找到左侧大于target
的值array[l]
,并将其赋值到array[r]
这样让l=r
时,左侧的值全部小于target
,右侧的值全部小于target
,将target
放到该位置
不需要额外存储空间,写法思路稍复杂(有能力推荐这种写法)
function quickSort(array, start, end) {
if (end - start < 1) {
return;
}
const target = array[start];
let l = start;
let r = end;
while (l < r) {
while (l < r && array[r] >= target) {
r--;
}
array[l] = array[r];
while (l < r && array[l] < target) {
l++;
}
array[r] = array[l];
}
array[l] = target;
quickSort(array, start, l - 1);
quickSort(array, l + 1, end);
return array;
}
复杂度
时间复杂度:平均O(nlogn)
,最坏O(n2)
,实际上大多数情况下小于O(nlogn)
空间复杂度:O(logn)
(递归调用消耗)
稳定性
不稳定
冒泡排序 稳定 o(n^2)
循环数组,比较当前元素和下一个元素,如果当前元素比下一个元素大,向上冒泡。
这样一次循环之后最后一个数就是本数组最大的数。
下一次循环继续上面的操作,不循环已经排序好的数。
优化:当一次循环没有发生冒泡,说明已经排序完成,停止循环。
解法
function bubbleSort(array) {
for (let j = 0; j < array.length; j++) {
let complete = true;
for (let i = 0; i < array.length - 1 - j; i++) {
// 比较相邻数
if (array[i] > array[i + 1]) {
[array[i], array[i + 1]] = [array[i + 1], array[i]];
complete = false;
}
}
// 没有冒泡结束循环
if (complete) {
break;
}
}
return array;
}
复杂度
时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性
稳定
归并排序 稳定 o(nlogn)
利用归并
的思想实现的排序方法。
该算法是采用分治法(Divide and Conquer
)的一个非常典型的应用。(分治法将问题分成一些小的问题然后递归求解,而治的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
- 将已有序的子序列合并,得到完全有序的序列
- 即先使每个子序列有序,再使子序列段间有序
- 若将两个有序表合并成一个有序表,称为二路归并
分割:
- 将数组从中点进行分割,分为左、右两个数组
- 递归分割左、右数组,直到数组长度小于
2
归并:
如果需要合并,那么左右两数组已经有序了。
创建一个临时存储数组temp
,比较两数组第一个元素,将较小的元素加入临时数组
若左右数组有一个为空,那么此时另一个数组一定大于temp
中的所有元素,直接将其所有元素加入temp
写法 1
分割数组时直接将数组分割为两个数组,合并时直接合并数组。
优点:思路简单,写法简单
缺点:空间复杂度略高,需要复制多个数组
//主递归函数mergeSort
function mergeSort(array) {
if (array.length < 2) {
return array;
}
//mid半个数组的长度
const mid = Math.floor(array.length / 2);
const front = array.slice(0, mid);
const end = array.slice(mid);
return merge(mergeSort(front), mergeSort(end));
}
//合并两个有序数列merge
function merge(front, end) {
//temp为有序数列
const temp = [];
while (front.length && end.length) {
if (front[0] < end[0]) {
temp.push(front.shift());
} else {
temp.push(end.shift());
}
}
while (front.length) {
temp.push(front.shift());
}
while (end.length) {
temp.push(end.shift());
}
return temp;
}
写法 2
记录数组的索引,使用left、right
两个索引来限定当前分割的数组。
优点:空间复杂度低,只需一个temp
存储空间,不需要拷贝数组
缺点:写法复杂
//mergeSort 大框架函数
function mergeSort(array, left, right, temp) {
if (left < right) {
const mid = Math.floor((left + right) / 2);
mergeSort(array, left, mid, temp);
mergeSort(array, mid + 1, right, temp);
//将排好序的两部分进行二路归并
merge(array, left, right, temp);
}
return array;
}
//merge 二路归并函数
function merge(array, left, right, temp) {
const mid = Math.floor((left + right) / 2);
let leftIndex = left;
let rightIndex = mid + 1;
let tempIndex = 0;
//左数组右数组都不空的情况
while (leftIndex <= mid && rightIndex <= right) {
if (array[leftIndex] < array[rightIndex]) {
temp[tempIndex++] = array[leftIndex++];
} else {
temp[tempIndex++] = array[rightIndex++];
}
}
//左数组还有剩余
while (leftIndex <= mid) {
temp[tempIndex++] = array[leftIndex++];
}
//右数组还有剩余
while (rightIndex <= right) {
temp[tempIndex++] = array[rightIndex++];
}
//temp循环赋值给array
tempIndex = 0;
for (let i = left; i <= right; i++) {
array[i] = temp[tempIndex++];
}
}
复杂度
时间复杂度:O(nlogn)
空间复杂度:O(n)
稳定性
稳定
堆排序 不稳定 o(nlogn)
堆是一颗顺序存储的完全二叉树,一层存满才会往下一层存。
步骤:
创建一个大顶堆,然后使大顶堆的堆顶是最大的元素。
交换第一个元素和最后一个元素,接下来每轮都让剩余的元素里最大值浮到顶端,然后顶端和末尾交换。
其实就是复杂版的冒泡排序。
解法
function heapSort(array) {
creatHeap(array);
//输出顶堆
console.log(array);
// 交换第一个和最后一个元素,然后重新调整大顶堆
for (let i = array.length - 1; i > 0; i--) {
[array[i], array[0]] = [array[0], array[i]];
adjust(array, 0, i);
}
return array;
}
// 构建大顶堆,从第一个非叶子节点start开始,进行下沉操作
function creatHeap(array) {
const len = array.length;
const start = parseInt(len / 2) - 1;
for (let i = start; i >= 0; i--) {
adjust(array, i, len);
}
}
// 将第target个元素进行下沉,孩子节点有比他大的就与较大值交换,结束之后能保证target处的元素为此子树中的最大值
function adjust(array, target, len) {
for (let i = 2 * target + 1; i < len; i = 2 * i + 1) {
// 找到两个子节点中最大的,用i记录下标
if (i + 1 < len && array[i + 1] > array[i]) {
i = i + 1;
}
// 下沉,即target和较大的子节点i交换,继续对target元素进行下沉
if (array[i] > array[target]) {
[array[i], array[target]] = [array[target], array[i]];
target = i;
} else {
break;
}
}
}
复杂度
时间复杂度:O(nlogn)
空间复杂度:O(1)
稳定性
不稳定
插入排序 稳定 o(n^2)
将左侧序列看成一个有序序列,每次将右侧序列的第一个数字插入该有序序列。
插入时,从有序序列最右侧开始比较,若比较的数较大,后移一位。
function insertSort(array) {
//初始默认左侧第一个数天然成为有序序列,i表示右侧序列的起始序号
for (let i = 1; i < array.length; i++) {
//target表示被插入的数当前位置
let target = i;
//j表示下一个要与target作比较的数的位置
for (let j = i - 1; j >= 0; j--) {
if (array[target] < array[j]) {
[array[target], array[j]] = [array[j], array[target]];
target = j;
} else {
break;
}
}
}
return array;
}
选择排序 不稳定 o(n^2)
每次循环选取一个最小的数字放到前面的有序序列中。
function selectionSort(array) {
//i表示寻找最小数的目标序列的队首
for (let i = 0; i < array.length - 1; i++) {
//minIndex记录当前最小数下标
let minIndex = i;
for (let j = i + 1; j < array.length; j++) {
if (array[j] < array[minIndex]) {
minIndex = j;
}
}
[array[minIndex], array[i]] = [array[i], array[minIndex]];
}
}
希尔排序 不稳定 o(n^1.2~n^2)
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。
- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
假设待排序文件有 10 个记录,其关键字分别是:49,38,65,97,76,13,27,49,55,04。增量系列的取值依次为:5,3,1,增量变化大概是每次除以 2 即可。最后一次的增量一定为 1。
//自组采用直接插入排序 针对有序序列在插入时采用交换法
function shellSort(arr) {
//逐步减半步长shellwidth直至为1为止
for (let shellWidth = arr.length / 2; shellWidth > 0; shellWidth / 2) {
//根据步长,将数组进行分组,并使用插入排序法进行交换排序,atom是起始点
//第一轮步长5:[shellwidth+1,1],[shellwidth+2,2]...
//第二轮步长2:[shellwidth+1,1],[shellwidth+2,2],[shellwidth+3,3,1]...
for (let atom = shellWidth; atom < arr.length; atom++) {
//atom-shellWidth 表示和该元素同组的隔壁相邻的元素,对于同一组的元素,进行插入排序
//这一个while循环其实就是做了一轮从atom开始的向前冒泡,不是一个完整的插入排序
while (atom - shellWidth > 0 && arr[atom - shellWidth] > arr[atom]) {
[arr[atom], arr[shellWidth]] = [arr[shellWidth], arr[atom]];
atom = atom - shellWidth;
}
}
}
}
基数排序 稳定 O(nlog(r)m) (r 为所采取的基数-10,m 为堆数-位数的最大值)
基数排序不需要直接对元素进行相互比较,也不需要将元素相互交换,你需要做的就是对元素进行“分类”。这也是基数排序的魅力所在。在实际项目中,如果对效率有所要求,范围已经知道,并且不太关心空间的使用时,可以选择用基数排序。
步骤:先对个位进行 0-9 分类,再对十位,依此类推。
const radixSort = (arr) => {
let length = arr.length;
if (length <= 1) {
return arr;
}
let maxDigit = String(parseInt(Math.max(...arr))).length; // 取最大位数
let mod = 10;
let dev = 1;
let counter = [];
for (let i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
//将所有数按第i位数字推入各个counter[bucket]里
for (let j = 0; j < arr.length; j++) {
let bucket = parseInt((arr[j] % mod) / dev);
counter[bucket] = counter[bucket] == null ? [] : counter[bucket];
counter[bucket].push(arr[j]);
}
let pos = 0;
//按新顺序改写覆盖arr
for (let j = 0; j < counter.length; j++) {
let value = null;
if (counter[j] != null) {
while ((value = counter[j].shift()) != null) {
arr[pos++] = value;
}
}
}
}
return arr;
};
函数柯里化
定义
在数学和计算机科学中,柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。
通俗易懂的解释:用闭包把参数保存起来,当参数的数量足够执行函数了,就开始执行函数。
实现
- 判断当前函数传入的参数是否大于或等于
fn
需要参数的数量,如果是,直接执行fn
- 如果传入参数数量不够,返回一个闭包,暂存传入的参数,并重新返回
currying
函数(args 被暂存,args2 为下次调用时传入的参数)
function currying(fn, ...args) {
//fn的length是函数声明时的形参个数(在第一个具有默认值之前的参数个数
if (args.length >= fn.length) {
return fn(...args);
} else {
return (...args2) => currying(fn, ...args, ...args2);
}
}
我们来一个简单的实例验证一下:
function fun(a, b, c) {
console.log(a, b, c);
}
const curryingFun = currying(fun);
curryingFun(1)(2)(3); // 1 2 3
curryingFun(1, 2)(3); // 1 2 3
curryingFun(1, 2, 3); // 1 2 3
应用场景
参数复用
function getUrl(protocol, domain, path) {
return protocol + "://" + domain + "/" + path;
}
var page1 = getUrl("http", "www.conardli.top", "page1.html");
var page2 = getUrl("http", "www.conardli.top", "page2.html");
我们使用currying
来简化它:
let conardliSite = currying(simpleURL)("http", "www.conardli.top");
let page1 = conardliSite("page1.html");
JSONP
- 1.将传入的 data 数据转化为 url 字符串形式
- 2.处理 url 中的回调函数
- 3.创建一个 script 标签并插入到页面中
- 4.挂载回调函数
(function (window, document) {
"use strict";
var jsonp = function (url, data, callback) {
// 1.将传入的data数据转化为url字符串形式
// {id:1,name:'jack'} => id=1&name=jack
var dataString = url.indexof("?") == -1 ? "?" : "&";
for (var key in data) {
dataString += key + "=" + data[key] + "&";
}
// 2 处理url中的回调函数
// cbFuncName回调函数的名字 :my_json_cb_名字的前缀 + 随机数(把小数点去掉)
var cbFuncName = "my_json_cb_" + Math.random().toString().replace(".", "");
dataString += "callback=" + cbFuncName;
// 3.创建一个script标签并插入到页面中
var scriptEle = document.createElement("script");
scriptEle.src = url + dataString;
// 4.挂载回调函数
window[cbFuncName] = function (data) {
callback(data);
// 处理完回调函数的数据之后,删除jsonp的script标签
document.body.removeChild(scriptEle);
};
document.body.appendChild(scriptEle);
};
window.$jsonp = jsonp;
})(window, document);
Promise
基础版本
- 设定三个状态
PENDING、FULFILLED、REJECTED
,只能由PENDING
改变为FULFILLED、REJECTED
,并且只能改变一次 MyPromise
接收一个函数executor
,executor
有两个参数resolve
方法和reject
方法resolve
将PENDING
改变为FULFILLED
reject
将PENDING
改变为FULFILLED
promise
变为FULFILLED
状态后具有一个唯一的value
promise
变为REJECTED
状态后具有一个唯一的reason
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";
function MyPromise(executor) {
this.state = PENDING;
this.value = null;
this.reason = null;
const resolve = (value) => {
if (this.state === PENDING) {
this.state = FULFILLED;
this.value = value;
}
};
const reject = (reason) => {
if (this.state === PENDING) {
this.state = REJECTED;
this.reason = reason;
}
};
try {
executor(resolve, reject);
} catch (reason) {
reject(reason);
}
}
then 方法
then
方法接受两个参数onFulfilled、onRejected
,它们分别在状态由PENDING
改变为FULFILLED、REJECTED
后调用- 一个
promise
可绑定多个then
方法 then
方法可以同步调用也可以异步调用- 同步调用:状态已经改变,直接调用
onFulfilled
方法 - 异步调用:状态还是
PENDING
,将onFulfilled、onRejected
分别加入两个函数数组onFulfilledCallbacks、onRejectedCallbacks
,当异步调用resolve
和reject
时,将两个数组中绑定的事件依次执行。
function MyPromise(executor) {
this.state = PENDING;
this.value = null;
this.reason = null;
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = (value) => {
if (this.state === PENDING) {
this.state = FULFILLED;
this.value = value;
//依次执行数组中的函数
this.onFulfilledCallbacks.forEach((fun) => {
fun();
});
}
};
const reject = (reason) => {
if (this.state === PENDING) {
this.state = REJECTED;
this.reason = reason;
//依次执行数组中的函数
this.onRejectedCallbacks.forEach((fun) => {
fun();
});
}
};
try {
executor(resolve, reject);
} catch (reason) {
reject(reason);
}
}
MyPromise.prototype.then = function (onFulfilled, onRejected) {
switch (this.state) {
case FULFILLED:
onFulfilled(this.value);
break;
case REJECTED:
onFulfilled(this.value);
break;
case PENDING:
this.onFulfilledCallbacks.push(() => {
onFulfilled(this.value);
});
this.onRejectedCallbacks.push(() => {
onRejected(this.reason);
});
break;
}
};
then 方法异步调用
如下面的代码:输入顺序是:1、2、ConardLi
console.log(1);
let promise = new Promise((resolve, reject) => {
resolve("ConardLi");
});
promise.then((value) => {
console.log(value);
});
console.log(2);
虽然resolve
是同步执行的,我们必须保证then
是异步调用的,我们用setTimeout
来模拟异步调用(并不能实现微任务和宏任务的执行机制,只是保证异步调用)
MyPromise.prototype.then = function (onFulfilled, onRejected) {
if (typeof onFulfilled != "function") {
onFulfilled = function (value) {
return value;
};
}
if (typeof onRejected != "function") {
onRejected = function (reason) {
throw reason;
};
}
switch (this.state) {
case FULFILLED:
setTimeout(() => {
onFulfilled(this.value);
}, 0);
break;
case REJECTED:
setTimeout(() => {
onRejected(this.reason);
}, 0);
break;
case PENDING:
this.onFulfilledCallbacks.push(() => {
setTimeout(() => {
onFulfilled(this.value);
}, 0);
});
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
onRejected(this.reason);
}, 0);
});
break;
}
};
then 方法链式调用
保证链式调用,即then
方法中要返回一个新的promise
,并将then
方法的返回值进行resolve
。
注意:这种实现并不能保证
then
方法中返回一个新的promise
,只能保证链式调用。
MyPromise.prototype.then = function (onFulfilled, onRejected) {
if (typeof onFulfilled != "function") {
onFulfilled = function (value) {
return value;
};
}
if (typeof onRejected != "function") {
onRejected = function (reason) {
throw reason;
};
}
const promise2 = new MyPromise((resolve, reject) => {
switch (this.state) {
case FULFILLED:
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolve(x);
} catch (reason) {
reject(reason);
}
}, 0);
break;
case REJECTED:
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolve(x);
} catch (reason) {
reject(reason);
}
}, 0);
break;
case PENDING:
this.onFulfilledCallbacks.push(() => {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolve(x);
} catch (reason) {
reject(reason);
}
}, 0);
});
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolve(x);
} catch (reason) {
reject(reason);
}
}, 0);
});
break;
}
});
return promise2;
};
catch 方法
catch方法与then方法第二个参数的区别:
若上面没有定义reject
方法,所有的异常会走向catch
方法:
MyPromise.prototype.catch = function (onRejected) {
return this.then(null, onRejected);
};
finally 方法
不管是resolve
还是reject
都会调用finally
。
MyPromise.prototype.finally = function (fn) {
return this.then(
(value) => {
fn();
return value;
},
(reason) => {
fn();
throw reason;
}
);
};
Promise.resolve
Promise.resolve
用来生成一个直接处于FULFILLED
状态的 Promise。
MyPromise.reject = function (value) {
return new MyPromise((resolve, reject) => {
resolve(value);
});
};
Promise.reject
Promise.reject
用来生成一个直接处于REJECTED
状态的 Promise。
MyPromise.reject = function (reason) {
return new MyPromise((resolve, reject) => {
reject(reason);
});
};
all 方法
接受一个promise
数组,当所有promise
状态resolve
后,执行resolve
这个方法返回一个新的 promise 对象,该 promise 对象在参数对象数组里所有的 promise 对象都成功的时候才会触发成功,一旦有任何一个 promise 对象失败则立即触发该 promise 对象的失败。这个新的 promise 对象在触发成功状态以后,会把一个包含数组里所有 promise 返回值的数组作为成功回调的返回值,顺序跟数组的顺序保持一致;如果这个新的 promise 对象触发了失败状态,它会把数组里第一个触发失败的 promise 对象的错误信息作为它的失败错误信息。Promise.all 方法常被用于处理多个 promise 对象的状态集合。
MyPromise.all = function (promises) {
return new Promise((resolve, reject) => {
if (promises.length === 0) {
resolve([]);
} else {
let result = [];
let index = 0;
for (let i = 0; i < promises.length; i++) {
promises[i].then(
(data) => {
result[i] = data;
if (++index === promises.length) {
resolve(result);
}
},
(err) => {
reject(err);
return;
}
);
}
}
});
};
race 方法
接受一个promise
数组,当有一个promise
状态resolve
后,执行resolve
当promise
数组里的任意一个子promise
被成功或失败后,父promise
马上也会用该子promise
的成功返回值或失败详情作为参数调用父promise
绑定的相应句柄,并返回该 promise 对象。
MyPromise.race = function (promises) {
return new Promise((resolve, reject) => {
if (promises.length === 0) {
resolve();
} else {
let index = 0;
for (let i = 0; i < promises.length; i++) {
promises[i].then(
(data) => {
resolve(data);
},
(err) => {
reject(err);
return;
}
);
}
}
});
};
基于 Promise 的 AJAX 封装
懒加载
监听图片高度
图片,用一个其他属性存储真正的图片地址:
<img src="loading.gif" data-src="https://cdn.pixabay.com/photo/2015/09/09/16/05/forest-931706_1280.jpg" alt="">
<img src="loading.gif" data-src="https://cdn.pixabay.com/photo/2014/08/01/00/08/pier-407252_1280.jpg" alt="">
<img src="loading.gif" data-src="https://cdn.pixabay.com/photo/2014/12/15/17/16/pier-569314_1280.jpg" alt="">
<img src="loading.gif" data-src="https://cdn.pixabay.com/photo/2010/12/13/10/09/abstract-2384_1280.jpg" alt="">
<img src="loading.gif" data-src="https://cdn.pixabay.com/photo/2015/10/24/11/09/drop-of-water-1004250_1280.jpg"
通过图片offsetTop
和window
的innerHeight
,scrollTop
判断图片是否位于可视区域。
var img = document.getElementsByTagName("img");
var n = 0; //存储图片加载到的位置,避免每次都从第一张图片开始遍历
lazyload(); //页面载入完毕加载可是区域内的图片
// 节流函数,保证每200ms触发一次
function throttle(event, time) {
let timer = null;
return function (...args) {
if (!timer) {
timer = setTimeout(() => {
timer = null;
event.apply(this, args);
}, time);
}
};
}
window.addEventListener("scroll", throttle(lazyload, 200));
function lazyload() {
//监听页面滚动事件
var seeHeight = window.innerHeight; //可见区域高度
var scrollTop = document.documentElement.scrollTop || document.body.scrollTop; //滚动条距离顶部高度
for (var i = n; i < img.length; i++) {
console.log(img[i].offsetTop, seeHeight, scrollTop);
if (img[i].offsetTop < seeHeight + scrollTop) {
if (img[i].getAttribute("src") == "loading.gif") {
img[i].src = img[i].getAttribute("data-src");
}
n = i + 1;
}
}
}
IntersectionObserver
IntersectionObserver 接口 (从属于 Intersection Observer API) 提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的方法。祖先元素与视窗(viewport)被称为根(root)。
Intersection Observer
可以不用监听scroll
事件,做到元素一可见便调用回调,在回调里面我们来判断元素是否可见。
if (IntersectionObserver) {
let lazyImageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach((entry, index) => {
let lazyImage = entry.target;
// 如果元素可见
if (entry.intersectionRatio > 0) {
if (lazyImage.getAttribute("src") == "loading.gif") {
lazyImage.src = lazyImage.getAttribute("data-src");
}
lazyImageObserver.unobserve(lazyImage);
}
});
});
for (let i = 0; i < img.length; i++) {
lazyImageObserver.observe(img[i]);
}
}
正则表达式
- 手机号码
/^1[3456789]\d{9}$/g
- QQ 号码
/^[1-9][0-9]{4,9}$/g
- 十六进制颜色(#48D1CC or #0AB)
/^#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$/g
- 邮箱
/^([A-Za-z0-9_-.]+)@([A-Za-z0-9_-.]+).([A-Za-z]{2,6})$/g
- URL
/^((https?|ftp|file)://)?([\da-z.-]+).([a-z.]{2,6})([/\w.-]*)*/?$/g
概述
正则表达式(regular expression)是一种表达文本模式(即字符串结构)的方法,有点像字符串的模板,常常用来按照“给定模式”匹配文本。比如,正则表达式给出一个 Email 地址的模式,然后用它来确定一个字符串是否为 Email 地址。JavaScript 的正则表达式体系是参照 Perl 5 建立的。
新建正则表达式有两种方法。一种是使用字面量,以斜杠表示开始和结束。
var regex = /xyz/;
另一种是使用RegExp
构造函数。
var regex = new RegExp("xyz");
上面两种写法是等价的,都新建了一个内容为xyz
的正则表达式对象。它们的主要区别是,第一种方法在引擎编译代码时,就会新建正则表达式,第二种方法在运行时新建正则表达式,所以前者的效率较高。而且,前者比较便利和直观,所以实际应用中,基本上都采用字面量定义正则表达式。
RegExp
构造函数还可以接受第二个参数,表示修饰符。
var regex = new RegExp("xyz", "i");
// 等价于
var regex = /xyz/i;
上面代码中,正则表达式/xyz/
有一个修饰符i
。
实例属性
正则对象的实例属性分成两类。
一类是修饰符相关,用于了解设置了什么修饰符。
RegExp.prototype.ignoreCase
:布尔值,表示是否设置了i
修饰符。RegExp.prototype.global
:布尔值,表示是否设置了g
修饰符。RegExp.prototype.multiline
:布尔值,表示是否设置了m
修饰符。RegExp.prototype.flags
:字符串,包含了已经设置的所有修饰符,按字母排序。
上面四个属性都是只读的。
var r = /abc/gim;
r.ignoreCase; // true
r.global; // true
r.multiline; // true
r.flags; // 'gim'
另一类是与修饰符无关的属性,主要是下面两个。
RegExp.prototype.lastIndex
:返回一个整数,表示下一次开始搜索的位置。该属性可读写,但是只在进行连续搜索时有意义,详细介绍请看后文。RegExp.prototype.source
:返回正则表达式的字符串形式(不包括反斜杠),该属性只读。
var r = /abc/gim;
r.lastIndex; // 0
r.source; // "abc"
正则表达式的实例方法
RegExp.prototype.test()
正则实例对象的test
方法返回一个布尔值,表示当前模式是否能匹配参数字符串。
/cat/.test("cats and dogs"); // true
上面代码验证参数字符串之中是否包含cat
,结果返回true
。
如果正则表达式不带g
修饰符,则每一次test
方法都从 0 开始。
如果正则表达式带有g
修饰符,则每一次test
方法都从上一次结束的位置(即 r.lastIndex)开始向后匹配。
var r = /x/g;
var s = "_x_x";
r.lastIndex; // 0
r.test(s); // true
r.lastIndex; // 2
r.test(s); // true
r.lastIndex; // 4
r.test(s); // false
r.lastIndex; // 0
带有g
修饰符时,可以通过正则对象的lastIndex
属性指定开始搜索的位置。
var r = /x/g;
var s = "_x_x";
r.lastIndex = 4;
r.test(s); // false
r.lastIndex; // 0
r.test(s); // true
上面代码指定从字符串的第五个位置开始搜索,这个位置为空,所以返回false
。同时,lastIndex
属性重置为0
,所以第二次执行r.test(s)
会返回true
。
注意,带有g
修饰符时,正则表达式内部会记住上一次的lastIndex
属性,这时不应该更换所要匹配的字符串,否则会有一些难以察觉的错误。
var r = /bb/g;
r.test("bb"); // true
r.test("-bb-"); // false
上面代码中,由于正则表达式r
是从上一次的lastIndex
位置开始匹配,导致第二次执行test
方法时出现预期以外的结果。
lastIndex
属性只对同一个正则表达式有效,所以下面这样写是错误的。
var count = 0;
while (/a/g.test("babaa")) count++;
上面代码会导致无限循环,因为while
循环的每次匹配条件都是一个新的正则表达式,导致lastIndex
属性总是等于 0。
如果正则模式是一个空字符串,则匹配所有字符串。
new RegExp("").test("abc");
// true
RegExp.prototype.exec()
正则实例对象的exec()
方法,用来返回匹配结果。如果发现匹配,就返回一个数组,成员是匹配成功的子字符串,否则返回null
。
var s = "_x_x";
var r1 = /x/;
var r2 = /y/;
r1.exec(s); // ["x"]
r2.exec(s); // null
上面代码中,正则对象r1
匹配成功,返回一个数组,成员是匹配结果;正则对象r2
匹配失败,返回null
。
如果正则表示式包含圆括号(即含有“组匹配”),则返回的数组会包括多个成员。第一个成员是整个匹配成功的结果,后面的成员就是圆括号对应的匹配成功的组。也就是说,第二个成员对应第一个括号,第三个成员对应第二个括号,以此类推。整个数组的length
属性等于组匹配的数量再加 1。
var s = "_x_x";
var r = /_(x)/;
r.exec(s); // ["_x", "x"]
上面代码的exec()
方法,返回一个数组。第一个成员是整个匹配的结果,第二个成员是圆括号匹配的结果。
exec()
方法的返回数组还包含以下两个属性:
input
:整个原字符串。index
:模式匹配成功的开始位置(从 0 开始计数)。
var r = /a(b+)a/;
var arr = r.exec("_abbba_aba_");
arr; // ["abbba", "bbb"]
arr.index; // 1
arr.input; // "_abbba_aba_"
上面代码中的index
属性等于 1,是因为从原字符串的第二个位置开始匹配成功。
如果正则表达式加上g
修饰符,则可以使用多次exec()
方法,下一次搜索的位置从上一次匹配成功结束的位置开始。
var reg = /a/g;
var str = "abc_abc_abc";
var r1 = reg.exec(str);
r1; // ["a"]
r1.index; // 0
reg.lastIndex; // 1
var r2 = reg.exec(str);
r2; // ["a"]
r2.index; // 4
reg.lastIndex; // 5
var r3 = reg.exec(str);
r3; // ["a"]
r3.index; // 8
reg.lastIndex; // 9
var r4 = reg.exec(str);
r4; // null
reg.lastIndex; // 0
上面代码连续用了四次exec()
方法,前三次都是从上一次匹配结束的位置向后匹配。当第三次匹配结束以后,整个字符串已经到达尾部,匹配结果返回null
,正则实例对象的lastIndex
属性也重置为0
,意味着第四次匹配将从头开始。
利用g
修饰符允许多次匹配的特点,可以用一个循环完成全部匹配。
var reg = /a/g;
var str = "abc_abc_abc";
while (true) {
var match = reg.exec(str);
if (!match) break;
console.log("#" + match.index + ":" + match[0]);
}
// #0:a
// #4:a
// #8:a
上面代码中,只要exec()
方法不返回null
,就会一直循环下去,每次输出匹配的位置和匹配的文本。
正则实例对象的lastIndex
属性不仅可读,还可写。设置了g
修饰符的时候,只要手动设置了lastIndex
的值,就会从指定位置开始匹配。
字符串的实例方法
字符串的实例方法之中,有 4 种与正则表达式有关。
String.prototype.match()
:返回一个数组,成员是所有匹配的子字符串。String.prototype.search()
:按照给定的正则表达式进行搜索,返回一个整数,表示匹配开始的位置。String.prototype.replace()
:按照给定的正则表达式进行替换,返回替换后的字符串。String.prototype.split()
:按照给定规则进行字符串分割,返回一个数组,包含分割后的各个成员。
str.match() 返回匹配结果
字符串实例对象的match
方法对字符串进行正则匹配,返回匹配结果,格式是一个数组。
var s = "_x_x";
var r1 = /x/;
var r2 = /y/;
s.match(r1); // ["x"]
s.match(r2); // null
从上面代码可以看到,字符串的match
方法与正则对象的exec
方法非常类似:匹配成功返回一个数组,匹配失败返回null
。
如果正则表达式带有g
修饰符,则该方法与正则对象的exec
方法行为不同,会一次性返回所有匹配成功的结果。
var s = "abba";
var r = /a/g;
s.match(r); // ["a", "a"]
r.exec(s); // ["a"]
设置正则表达式的lastIndex
属性,对match
方法无效,匹配总是从字符串的第一个字符开始。
var r = /a|b/g;
r.lastIndex = 7;
"xaxb".match(r); // ['a', 'b']
r.lastIndex; // 0
上面代码表示,设置正则对象的lastIndex
属性是无效的。
str.search() 返回第一个匹配位置
字符串对象的search
方法,返回第一个满足条件的匹配结果在整个字符串中的位置。如果没有任何匹配,则返回-1
。
search() 方法不执行全局匹配,它将忽略标志 g。它同时忽略 regexp 的 lastIndex 属性,总是从字符串的开始进行检索。
"_x_x".search(/x/); // 1
str.replace() 返回替换后的字符串
字符串对象的replace
方法可以替换匹配的值。它接受两个参数,第一个是正则表达式,表示搜索模式,第二个是替换的内容。不改变原字符串的值。
str.replace(search, replacement);
正则表达式如果不加g
修饰符,就替换第一个匹配成功的值,否则替换所有匹配成功的值。
"aaa".replace("a", "b"); // "baa"
"aaa".replace(/a/, "b"); // "baa"
"aaa".replace(/a/g, "b"); // "bbb"
上面代码中,最后一个正则表达式使用了g
修饰符,导致所有的a
都被替换掉了。
replace
方法的一个应用,就是消除字符串首尾两端的空格。
var str = " #id div.class ";
str.replace(/^\s+|\s+$/g, "");
// "#id div.class"
replace
方法的第二个参数可以使用美元符号$
,用来指代所替换的内容。
- $&:匹配结果。
- $`:匹配结果前面的文本。
- $':匹配结果后面的文本。
- $n:匹配成功的第
n
组内容,n
是从 1 开始的自然数。 - $$:指代美元符号
$
。
"hello world".replace(/(\w+)\s(\w+)/, "$2 $1");
// "world hello"
"abc".replace("b", "[$`-$&-$']");
// "a[a-b-c]c"
上面代码中,第一个例子是将匹配的组互换位置,第二个例子是改写匹配的值。
replace
方法的第二个参数还可以是一个函数,将每一个匹配内容替换为函数返回值。
"3 and 5".replace(/[0-9]+/g, function (match) {
return 2 * match;
});
// "6 and 10"
var a = "The quick brown fox jumped over the lazy dog.";
var pattern = /quick|brown|lazy/gi;
a.replace(pattern, function replacer(match) {
return match.toUpperCase();
});
// The QUICK BROWN fox jumped over the LAZY dog.
作为replace
方法第二个参数的替换函数,可以接受多个参数。其中,第一个参数是捕捉到的内容,第二个参数是捕捉到的组匹配(有多少个组匹配,就有多少个对应的参数)。此外,最后还可以添加两个参数,倒数第二个参数是捕捉到的内容在整个字符串中的位置(比如从第五个位置开始),最后一个参数是原字符串。下面是一个网页模板替换的例子。
var prices = {
p1: "$1.99",
p2: "$9.99",
p3: "$5.00",
};
var template =
'<span id="p1"></span>' + '<span id="p2"></span>' + '<span id="p3"></span>';
template.replace(
/(<span id=")(.*?)(">)(</span>)/g,
function (match, $1, $2, $3, $4) {
return $1 + $2 + $3 + prices[$2] + $4;
}
);
// "<span id="p1">$1.99</span><span id="p2">$9.99</span><span id="p3">$5.00</span>"
上面代码的捕捉模式中,有四个括号,所以会产生四个组匹配,在匹配函数中用$1
到$4
表示。匹配函数的作用是将价格插入模板中。
str.split() 分割字符串
字符串对象的split
方法按照正则规则分割字符串,返回一个由分割后的各个部分组成的数组。
str.split(separator, [limit]);
该方法接受两个参数,第一个参数是正则表达式,表示分隔规则,第二个参数是返回数组的最大长度。
// 非正则分隔
"a, b,c, d".split(",");
// [ 'a', ' b', 'c', ' d' ]
// 正则分隔,去除多余的空格
"a, b,c, d".split(/, */);
// [ 'a', 'b', 'c', 'd' ]
// 指定返回数组的最大成员
"a, b,c, d".split(/, */, 2)[("a", "b")];
上面代码使用正则表达式,去除了子字符串的逗号后面的空格。
// 例一
"aaa*a*".split(/a*/);
// [ '', '*', '*' ]
// 例二
"aaa**a*".split(/a*/);
// ["", "*", "*", "*"]
上面代码的分割规则是 0 次或多次的a
,由于正则默认是贪婪匹配,所以例一的第一个分隔符是aaa
,第二个分割符是a
,将字符串分成三个部分,包含开始处的空字符串。例二的第一个分隔符是aaa
,第二个分隔符是 0 个a
(即空字符),第三个分隔符是a
,所以将字符串分成四个部分。
如果正则表达式带有括号,则括号匹配的部分也会作为数组成员返回。
'aaa*a*'.split(/(a*)/)
// [ '', 'aaa', '*', 'a', '*' ]
上面代码的正则表达式使用了括号,第一个组匹配是aaa
,第二个组匹配是a
,它们都作为数组成员返回。
匹配规则
正则表达式的规则很复杂,下面一一介绍这些规则。
字面量字符和元字符
大部分字符在正则表达式中,就是字面的含义,比如/a/
匹配a
,/b/
匹配b
。如果在正则表达式之中,某个字符只表示它字面的含义(就像前面的a
和b
),那么它们就叫做“字面量字符”(literal characters)。
/dog/.test('old dog') // true
上面代码中正则表达式的dog
,就是字面量字符,所以/dog/
匹配old dog
,因为它就表示d
、o
、g
三个字母连在一起。
除了字面量字符以外,还有一部分字符有特殊含义,不代表字面的意思。它们叫做“元字符”(metacharacters),主要有以下几个。
(1)点字符(.)
点字符(.
)匹配除回车(\r
)、换行(\n
) 、行分隔符(\u2028
)和段分隔符(\u2029
)以外的所有字符。注意,对于码点大于0xFFFF
字符,点字符不能正确匹配,会认为这是两个字符。
/c.t/
上面代码中,c.t
匹配c
和t
之间包含任意一个字符的情况,只要这三个字符在同一行,比如cat
、c2t
、c-t
等等,但是不匹配coot
。
(2)位置字符
位置字符用来提示字符所处的位置,主要有两个字符。
^
表示字符串的开始位置$
表示字符串的结束位置
// test必须出现在开始位置
/^test/.test('test123') // true
// test必须出现在结束位置
/test$/.test('new test') // true
// 从开始位置到结束位置只有test
/^test$/.test('test') // true
/^test$/.test('test test') // false
(3)选择符(|
)
竖线符号(|
)在正则表达式中表示“或关系”(OR),即cat|dog
表示匹配cat
或dog
。
/11|22/.test('911') // true
上面代码中,正则表达式指定必须匹配11
或22
。
多个选择符可以联合使用。
// 匹配fred、barney、betty之中的一个
/fred|barney|betty/
选择符会包括它前后的多个字符,比如/ab|cd/
指的是匹配ab
或者cd
,而不是指匹配b
或者c
。如果想修改这个行为,可以使用圆括号。
/a( |\t)b/.test('a\tb') // true
上面代码指的是,a
和b
之间有一个空格或者一个制表符。
其他的元字符还包括``、*
、+
、?
、()
、[]
、{}
等,将在下文解释。
转义符 12 个
正则表达式中那些有特殊含义的元字符,如果要匹配它们本身,就需要在它们前面要加上反斜杠。比如要匹配+
,就要写成+
。
正则表达式中,需要反斜杠转义的,一共有 12 个字符:^
、.
、[
、$
、(
、)
、|
、*
、+
、?
、{
和``。需要特别注意的是,如果使用RegExp
方法生成正则对象,转义需要使用两个斜杠,因为字符串内部会先转义一次。
new RegExp("1+1")
.test("1+1")(
// false
new RegExp("1\+1")
)
.test("1+1");
// true
上面代码中,RegExp
作为构造函数,参数是一个字符串。但是,在字符串内部,反斜杠也是转义字符,所以它会先被反斜杠转义一次,然后再被正则表达式转义一次,因此需要两个反斜杠转义。
特殊字符(不能打印的)
正则表达式对一些不能打印的特殊字符,提供了表达方法。
\cX
表示Ctrl-[X]
,其中的X
是 A-Z 之中任一个英文字母,用来匹配控制字符。[\b]
匹配退格键(U+0008),不要与\b
混淆。\n
匹配换行键。\r
匹配回车键。\t
匹配制表符 tab(U+0009)。\v
匹配垂直制表符(U+000B)。\f
匹配换页符(U+000C)。\0
匹配null
字符(U+0000)。\xhh
匹配一个以两位十六进制数(\x00
-\xFF
)表示的字符。\uhhhh
匹配一个以四位十六进制数(\u0000
-\uFFFF
)表示的 Unicode 字符。
字符类 [ ]
字符类(class)表示有一系列字符可供选择,只要匹配其中一个就可以了。所有可供选择的字符都放在方括号内,比如 [xyz]
表示x
、y
、z
之中任选一个匹配。
/[abc]/.test('hello world') // false
/[abc]/.test('apple') // true
上面代码中,字符串hello world
不包含a
、b
、c
这三个字母中的任一个,所以返回false
;字符串apple
包含字母a
,所以返回true
。
有两个字符在字符类中有特殊含义。
(1)脱字符(^)
如果方括号内的第一个字符是[^]
,则表示除了字符类之中的字符,其他字符都可以匹配。比如,[^xyz]
表示除了x
、y
、z
之外都可以匹配。
/[^abc]/.test('bbc news') // true
/[^abc]/.test('bbc') // false
上面代码中,字符串bbc news
包含a
、b
、c
以外的其他字符,所以返回true
;字符串bbc
不包含a
、b
、c
以外的其他字符,所以返回false
。
如果方括号内没有其他字符,即只有[^]
,就表示匹配一切字符,其中包括换行符。相比之下,点号作为元字符(.
)是不包括换行符的。
var s = 'Please yes\nmake my day!';
s.match(/yes.*day/) // null
s.match(/yes[^]*day/) // [ 'yes\nmake my day']
上面代码中,字符串s
含有一个换行符,点号不包括换行符,所以第一个正则表达式匹配失败;第二个正则表达式[^]
包含一切字符,所以匹配成功。
注意,脱字符只有在字符类的第一个位置才有特殊含义,否则就是字面含义。
(2)连字符(-)
某些情况下,对于连续序列的字符,连字符(-
)用来提供简写形式,表示字符的连续范围。比如,[abc]
可以写成[a-c]
,[0123456789]
可以写成[0-9]
,同理[A-Z]
表示 26 个大写字母。
/a-z/.test('b') // false
/[a-z]/.test('b') // true
上面代码中,当连字号(dash)不出现在方括号之中,就不具备简写的作用,只代表字面的含义,所以不匹配字符b
。只有当连字号用在方括号之中,才表示连续的字符序列。
以下都是合法的字符类简写形式。
[0-9.,]
[0-9a-fA-F]
[a-zA-Z0-9-]
[1-31]
上面代码中最后一个字符类[1-31]
,不代表1
到31
,只代表1
到3
。
连字符还可以用来指定 Unicode 字符的范围。
var str = "\u0130\u0131\u0132";
/[\u0128-\uFFFF]/.test(str)
// true
上面代码中,\u0128-\uFFFF
表示匹配码点在0128
到FFFF
之间的所有字符。
另外,不要过分使用连字符,设定一个很大的范围,否则很可能选中意料之外的字符。最典型的例子就是[A-z]
,表面上它是选中从大写的A
到小写的z
之间 52 个字母,但是由于在 ASCII 编码之中,大写字母与小写字母之间还有其他字符,结果就会出现意料之外的结果。
/[A-z]/.test('\') // true
上面代码中,由于反斜杠('')的 ASCII 码在大写字母与小写字母之间,结果会被选中。
预定义模式(例:\d 数字)
预定义模式指的是某些常见模式的简写方式。
\d
匹配 0-9 之间的任一数字,相当于[0-9]
。\D
匹配所有 0-9 以外的字符,相当于[^0-9]
。\w
匹配任意的字母、数字和下划线,相当于[A-Za-z0-9_]
。\W
除所有字母、数字和下划线以外的字符,相当于[^A-Za-z0-9_]
。\s
匹配空格(包括换行符、制表符、空格符等),相等于[ \t\r\n\v\f]
。\S
匹配非空格的字符,相当于[^ \t\r\n\v\f]
。\b
匹配词的边界。\B
匹配非词边界,即在词的内部。
下面是一些例子。
// \s 的例子
/\s\w*/.exec('hello world') // [" world"]
// \b 的例子
/\bworld/.test('hello world') // true
/\bworld/.test('hello-world') // true
/\bworld/.test('helloworld') // false
// \B 的例子
/\Bworld/.test('hello-world') // false
/\Bworld/.test('helloworld') // true
上面代码中,\s
表示空格,所以匹配结果会包括空格。\b
表示词的边界,所以world
的词首必须独立(词尾是否独立未指定),才会匹配。同理,\B
表示非词的边界,只有world
的词首不独立,才会匹配。
通常,正则表达式遇到换行符(\n
)就会停止匹配。
var html = "<b>Hello</b>\n<i>world!</i>";
/.*/.exec(html)[0]
// "<b>Hello</b>"
上面代码中,字符串html
包含一个换行符,结果点字符(.
)不匹配换行符,导致匹配结果可能不符合原意。这时使用\s
字符类,就能包括换行符。
var html = "<b>Hello</b>\n<i>world!</i>";
/[\S\s]*/.exec(html)[0]
// "<b>Hello</b>\n<i>world!</i>"
上面代码中,[\S\s]
指代一切字符。
重复次数指定类 { }
模式的精确匹配次数,使用大括号({}
)表示。{n}
表示恰好重复n
次,{n,}
表示至少重复n
次,{n,m}
表示重复不少于n
次,不多于m
次。
/lo{2}k/.test('look') // true
/lo{2,5}k/.test('looook') // true
上面代码中,第一个模式指定o
连续出现 2 次,第二个模式指定o
连续出现 2 次到 5 次之间。
重复次数模糊类(量词符)
量词符用来设定某个模式出现的次数。
?
问号表示某个模式出现 0 次或 1 次,等同于{0, 1}
。*
星号表示某个模式出现 0 次或多次,等同于{0,}
。+
加号表示某个模式出现 1 次或多次,等同于{1,}
。
// t 出现0次或1次
/t?est/.test('test') // true
/t?est/.test('est') // true
// t 出现1次或多次
/t+est/.test('test') // true
/t+est/.test('ttest') // true
/t+est/.test('est') // false
// t 出现0次或多次
/t*est/.test('test') // true
/t*est/.test('ttest') // true
/t*est/.test('tttest') // true
/t*est/.test('est') // true
贪婪模式 加?
上一小节的三个量词符,默认情况下都是最大可能匹配,即匹配到下一个字符不满足匹配规则为止。这被称为贪婪模式。
var s = "aaa";
s.match(/a+/); // ["aaa"]
除了贪婪模式,还有非贪婪模式,即最小可能匹配。只要一发现匹配,就返回结果,不要往下检查。如果想将贪婪模式改为非贪婪模式,可以在量词符后面加一个问号。
var s = 'aaa';
s.match(/a+?/) // ["a"]
上面例子中,模式结尾添加了一个问号/a+?/
,这时就改为非贪婪模式,一旦条件满足,就不再往下匹配,+?
表示只要发现一个a
,就不再往下匹配了。
除了非贪婪模式的加号(+?
),还有非贪婪模式的星号(*?
)和非贪婪模式的问号(??
)。
+?
:表示某个模式出现 1 次或多次,匹配时采用非贪婪模式。*?
:表示某个模式出现 0 次或多次,匹配时采用非贪婪模式。??
:表格某个模式出现 0 次或 1 次,匹配时采用非贪婪模式。
"abb".match(/ab*/); // ["abb"]
"abb".match(/ab*?/); // ["a"]
"abb".match(/ab?/); // ["ab"]
"abb".match(/ab??/); // ["a"]
上面例子中,/ab*/
表示如果a
后面有多个b
,那么匹配尽可能多的b
;/ab*?/
表示匹配尽可能少的b
,也就是 0 个b
。
修饰符 g/i/m
修饰符(modifier)表示模式的附加规则,放在正则模式的最尾部。
修饰符可以单个使用,也可以多个一起使用。
// 单个修饰符
var regex = /test/i;
// 多个修饰符
var regex = /test/gi;
(1)g 修饰符
默认情况下,第一次匹配成功后,正则对象就停止向下匹配了。g
修饰符表示全局匹配(global),加上它以后,正则对象将匹配全部符合条件的结果,主要用于搜索和替换。
var regex = /b/;
var str = 'abba';
regex.test(str); // true
regex.test(str); // true
regex.test(str); // true
上面代码中,正则模式不含g
修饰符,每次都是从字符串头部开始匹配。所以,连续做了三次匹配,都返回true
。
var regex = /b/g;
var str = 'abba';
regex.test(str); // true
regex.test(str); // true
regex.test(str); // false
上面代码中,正则模式含有g
修饰符,每次都是从上一次匹配成功处,开始向后匹配。因为字符串abba
只有两个b
,所以前两次匹配结果为true
,第三次匹配结果为false
。
(2)i 修饰符
默认情况下,正则对象区分字母的大小写,加上i
修饰符以后表示忽略大小写(ignoreCase)。
/abc/.test('ABC') // false
/abc/i.test('ABC') // true
上面代码表示,加了i
修饰符以后,不考虑大小写,所以模式abc
匹配字符串ABC
。
(3)m 修饰符
m
修饰符表示多行模式(multiline),会修改^
和$
的行为。默认情况下(即不加m
修饰符时),^
和$
匹配字符串的开始处和结尾处,加上m
修饰符以后,^
和$
还会匹配行首和行尾,即^
和$
会识别换行符(\n
)。
/world$/.test('hello world\n') // false
/world$/m.test('hello world\n') // true
上面的代码中,字符串结尾处有一个换行符。如果不加m
修饰符,匹配不成功,因为字符串的结尾不是world
;加上以后,$
可以匹配行尾。
/^b/m.test('a\nb') // true
上面代码要求匹配行首的b
,如果不加m
修饰符,就相当于b
只能处在字符串的开始处。加上m
修饰符以后,换行符\n
也会被认为是一行的开始。
组匹配 ( )
(1)概述
正则表达式的括号表示分组匹配,括号中的模式可以用来匹配分组的内容。
/fred+/.test("fredd") / // true
fred +
/.test('fredfred') / / true;
上面代码中,第一个模式没有括号,结果+
只表示重复字母d
,第二个模式有括号,结果+
就表示匹配fred
这个词。
下面是另外一个分组捕获的例子。
var m = 'abcabc'.match(/(.)b(.)/);
m
// ['abc', 'a', 'c']
上面代码中,正则表达式/(.)b(.)/
一共使用两个括号,第一个括号捕获a
,第二个括号捕获c
。
注意,使用组匹配时,不宜同时使用g
修饰符,否则match
方法不会捕获分组的内容。
var m = 'abcabc'.match(/(.)b(.)/g);
m // ['abc', 'abc']
上面代码使用带g
修饰符的正则表达式,结果match
方法只捕获了匹配整个表达式的部分。这时必须使用正则表达式的exec
方法,配合循环,才能读到每一轮匹配的组捕获。
var str = 'abcabc';
var reg = /(.)b(.)/g;
while (true) {
var result = reg.exec(str);
if (!result) break;
console.log(result);
}
// ["abc", "a", "c"]
// ["abc", "a", "c"]
正则表达式内部,还可以用\n
引用括号匹配的内容,n
是从 1 开始的自然数,表示对应顺序的括号。
/(.)b(.)\1b\2/.test("abcabc")
// true
上面的代码中,\1
表示第一个括号匹配的内容(即a
),\2
表示第二个括号匹配的内容(即c
)。
下面是另外一个例子。
/y(..)(.)\2\1/.test('yabccab') // true
括号还可以嵌套。
/y((..)\2)\1/.test('yabababab') // true
上面代码中,\1
指向外层括号,\2
指向内层括号。
组匹配非常有用,下面是一个匹配网页标签的例子。
var tagName = /<([^>]+)>[^<]*</\1>/;
tagName.exec("<b>bold</b>")[1]
// 'b'
上面代码中,圆括号匹配尖括号之中的标签,而\1
就表示对应的闭合标签。
上面代码略加修改,就能捕获带有属性的标签。
var html = '<b class="hello">Hello</b><i>world</i>';
var tag = /<(\w+)([^>]*)>(.*?)</\1>/g;
var match = tag.exec(html);
match[1] // "b"
match[2] // " class="hello""
match[3] // "Hello"
match = tag.exec(html);
match[1] // "i"
match[2] // ""
match[3] // "world"
(2)非捕获组
(?:x)
称为非捕获组(Non-capturing group),表示不返回该组匹配的内容,即匹配的结果中不计入这个括号。
非捕获组的作用请考虑这样一个场景,假定需要匹配foo
或者foofoo
,正则表达式就应该写成/(foo){1, 2}/
,但是这样会占用一个组匹配。这时,就可以使用非捕获组,将正则表达式改为/(?:foo){1, 2}/
,它的作用与前一个正则是一样的,但是不会单独输出括号内部的内容。
请看下面的例子。
var m = 'abc'.match(/(?:.)b(.)/);
m // ["abc", "c"]
上面代码中的模式,一共使用了两个括号。其中第一个括号是非捕获组,所以最后返回的结果中没有第一个括号,只有第二个括号匹配的内容。
下面是用来分解网址的正则表达式。
// 正常匹配
var url = /(http|ftp)://([^/\r\n]+)(/[^\r\n]*)?/;
url.exec('http://google.com/');
// ["http://google.com/", "http", "google.com", "/"]
// 非捕获组匹配
var url = /(?:http|ftp)://([^/\r\n]+)(/[^\r\n]*)?/;
url.exec('http://google.com/');
// ["http://google.com/", "google.com", "/"]
上面的代码中,前一个正则表达式是正常匹配,第一个括号返回网络协议;后一个正则表达式是非捕获匹配,返回结果中不包括网络协议。
(3)先行断言
x(?=y)
称为先行断言(Positive look-ahead),x
只有在y
前面才匹配,y
不会被计入返回结果。比如,要匹配后面跟着百分号的数字,可以写成/\d+(?=%)/
。
“先行断言”中,括号里的部分是不会返回的。
var m = 'abc'.match(/b(?=c)/);
m // ["b"]
上面的代码使用了先行断言,b
在c
前面所以被匹配,但是括号对应的c
不会被返回。
(4)先行否定断言
x(?!y)
称为先行否定断言(Negative look-ahead),x
只有不在y
前面才匹配,y
不会被计入返回结果。比如,要匹配后面跟的不是百分号的数字,就要写成/\d+(?!%)/
。
/\d+(?!.)/.exec('3.14')
// ["14"]
上面代码中,正则表达式指定,只有不在小数点前面的数字才会被匹配,因此返回的结果就是14
。
“先行否定断言”中,括号里的部分是不会返回的。
var m = 'abd'.match(/b(?!c)/);
m // ['b']
上面的代码使用了先行否定断言,b
不在c
前面所以被匹配,而且括号对应的d
不会被返回。