new
运算符用于创建一个对象类型的实例或具有构造函数的内置对象的实例。
new
关键字会进行如下的操作:
{}
);this
;实现:
function myNew(Class, ...args){
var obj = {};
// obj 是实例,实例对象的原型应指向构造函数的原型
obj.__proto__ = Class.prototype;
var result = Class.call(obj, ...args);
return result instanceof Object ? result : obj;
}
上面代码中我们该判断了 result 的类型,在原生的 new
关键字上,如果你返回了一个对象,则接收时接收的会是这个对象,例如:
function Per(name, age){
this.name = name;
this.age = age;
this.getName = function(){
return this.name;
}
// 返回了一个对象
return {};
}
let p = new Per("XiaoMing", 18); // {}
如果 result 是一个对象类型的数据,则就返回它,不是的话就返回我们创建的对象实例。
apply
与 call
方法类似,不同的是 apply
的第二个参数一个数组。它们都是用一个指定的 this
值和单独给出的一个或多个参数来调用一个函数,该函数内部的 this 指向会变成我们指定的 this。
实现思路:
代码:
Function.prototype.myCall = function(context, ...args){
// context 是假值或者没传入时,让它是 window
context = context || window;
// 避免方法名重复,使用 symbol 类型作为方法名
var key = Symbol();
// 这里的 this 指代被调用的函数
context[key] = this;
// 执行这个方法,自然里面的 this 指向 context
const result = context[key](...args "key");
// 执行完后删掉这个方法
delete context[key];
// 返回最终的结果
return result;
}
apply
方法与之类似:
Function.prototype.myApply = function(context, args){
context = context || window;
var key = Symbol();
context[key] = this;
const result = context[key](...args "key");
delete context[key];
return result;
}
bind
方法与 call
方法类似,但它不会调用函数,只是绑定 this 和函数参数。如果使用 new
运算符构造绑定函数,则忽略传入的 this 参数。例如下面的代码:
function Per(name){
this.name = name;
this.age = 18;
return this.name + "\t" + this.age;
}
var P = Per.bind({}, "M");
var p = new P(); // bind 的第一个参数将会忽略
实现代码:
Function.prototype.myBind = function(context, ...args){
context = context || window;
// 把原来的函数保存起来
var fn = this;
// 返回一个函数
return function newFn(...newArgs){
// 如果是构造函数,this 将是 newFn 的实例
if(this instanceof newFn){
return new fn(...args, ...newArgs);
}
// 否则就绑定 this 并执行
return fn.call(context, ...args, ...newArgs);
}
}
考虑下面的代码,array 的结果是什么?
let array = [ ,1 , ,2 , ,3];
array = array.map(i => ++ i);
答案:
[ ,2 , ,3 , ,4]
array 的长度实际是 6,第一个、第三个、第五个元素都是空值(empty
)。map
函数在遍历数组时会跳过运算空值,但最终结果会保留它,因此有值的元素进行了运算,空值元素不变。
例如:
ary = [ ,1 , ,null ,undefined ,2]; // 长度是 6 四个空值
ary.forEach(i => { console.log(i) });
ary = [ ,1 , ,null ,undefined ,2];
var res1 = ary.join("");
var res2 = ary.join();
console.log("join(''): ", res1);
console.log("join(): ", res2);
ary = [ ,1 , ,null ,undefined ,2];
var res = ary.reduce((prev, cur) => prev + cur);
console.log("reduce: ",res);
ary = [ ,1 , ,null ,undefined ,2];
var res = ary.every(i => i < 4);
console.log("every: ",res);
console.log("toString: ",ary.toString());
打印结果:
1
null
undefined
2
join(''): 12
join(): ,1,,,,2
reduce: NaN
every: false
toString: ,1,,,,2
forEach
、every
、reduce
函数都会忽略数组中的空值,但 null
和 undefined
并不会认为是空值,所以 reduce
返回了 NaN
(undefined + 1 等于 NaN)。undefined
与数值比较结果总是 false
,因此 every
最终返回了 false
。
join
函数的参数如果省略,数组元素用逗号(,
)分隔。如果是空字符串(""
),则所有元素之间都没有任何字符。如果一个元素为 undefined
或 null
,它会被转换为空字符串。toString
的返回结果与 join 函数没有参数时返回结果一样。
其他的,如 filter
函数和 some
函数也会跳过数组中的空值,sort
函数在排序时 undefined
会排在数组后面,空值会排在 undefined
后面,无论做怎样的排序。当数组中有 null
时,就比较奇特了,例如下面的比较:
console.log("null < 1: ",null < 1); // true
console.log("null <= 2: ",null <= 2); // true
console.log("null >= 1: ",null >= 1); // false
console.log("null <= 0: ",null <= 0); // true
console.log("null >= 0: ",null >= 0); // true
console.log("null > 0: ",null > 0); // false
console.log("null < 0: ",null < 0); // false
console.log("null == 0: ",null == 0); // false
null 可以大于等于 0,可以小于等于 0,而去掉等号会返回 false,结果 null 最终判定不与 0 相等。。。当数组中有 0 和 null 时,给数组排序,0 可能出现在 null 之前,也可能出现在 null 之后。
扩展运算符浅复制一个数组,空值会被转成 undefined
。pop
、shift
操作一个空值时,返回的结果会被转成 undefined
。
一个经典的问题:
有一个字符串类型的数字,例如:10000000
,把这个数字变成下面的形式(从右往左,每个三个数字加一个逗号):
1000000 ---> 1,000,000
100000000 ---> 100,000,000
如果用正则表达式 + replace
函数如何实现?
答案:正则表达式可以写成:
/(?=(\B)(\d{3})+$)/g
。reaplce(reg , ",")
\B
表示匹配非单词边界(\b
表示匹配单词边界)。例如一个字符串 hello world
,h 之前有一个单词边界,lo
右边有一个单词边界,w
左边有一个单词边界,d
右边有一个单词边界。一个字符串两端会有单词边界,字符串中有空白字符时也会有单词边界。
var str = "hello world";
var reg = /\w+\b/g;
str.match(reg); // ["hello", "world"]
非单词边界就与单词边界相反了,如:
var str = "qwer";
// qw 之间有非单词边界;we 之间有非单词边界;er 之间有非单词边界
str.match(/\B/g);
// ["","",""]
x(?=y)
这样的形式被称为“先行断言”,它用来匹配 x
仅仅当 x
后面跟着 y
,但 y
并不是匹配结果的一部分,这一点很重要,也就是说我们匹配符合条件的 x
。比如下面的匹配案例:
var reg = /hello(?=\s\w+)/g;
// str 字符串里有符合条件的 "hello" 字符串
var str = "hello world";
console.log(str.match(reg)); // ["hello"]
例题中的先行断言没有 x
,只有 y
部分。也就是说我们匹配的是一个空字符串(什么都没有的),这个空字符串后面有一个非单词边界和三个连续的数字,三个连续的数字可能有多个(后面有一个 +
),我们要从右往左匹配,后面需要加一个 $
。把匹配到的空字符替换成 ,
,因为是全局匹配,因此就全部替换了。
var reg = /(?=(\B)(\d{3})+$)/g;
var str = "10000000";
console.log(str.match(reg)); // ["", ""]
\B
在这个题目中的作用是防止 ,
插入到最左边,例如一个数是:100000
,如果不加 \B
,替换后就变成了 ,100,000
。左边的 1 有单词边界,我们要匹配没有单词边界的数字。
x(?=y)
被称为 “先行断言”,除了先行断言之外还有下面几种断言:
(?<=y)x
匹配 x
,仅当 x
前面是 y
。这种叫做后行断言。x(?!y)
仅仅当 x
后面不跟着 y
时匹配 x
,这被称为正向否定查找。(?<!y)x
仅仅当 x
前面不是 y
时匹配 x
,这被称为反向否定查找。四个边界类断言:
^
匹配输入的开头。如果多行模式设为 true,^
在换行符后也能匹配;$
匹配输入的结束。如果多行模式设为 true,$
在换行符前也能匹配;\b
匹配单词边界;\B
匹配非单词边界;(?:x)
这种格式的匹配符与上面的断言很相似,但它不是断言。带有 ?:
的括号被称为“非捕获括号”,match
方法、exec
方法在不使用全局匹配时,都会返回匹配到的括号里的内容和全局内容。而如果加上了 ?:
的括号则不会被捕获。例如:
var reg = /(?:hello)\s?\w+(\d)$/;
var res = "hello world123".match(reg);
console.log(res); // { 0: "hello world123", 1: "3" }
// hello 并不会匹配到
replace 中的捕获标记符也不会捕获到有 ?:
的括号。如:
var reg = /(?:hello)\s?\w+(\d)$/; // 没有 ?: 时,$1 指向左边第一个括号
var res = "hello world123".replace(reg, "$1--aaa");
console.log(res); // 3--aaa
实现一个字符串的 trim 函数。
trim()
方法返回一个从两头去掉空白字符的字符串,并不影响原字符串本身。
例如:
var newStr = " hello world ".trim();
console.log(newStr); // "hello world"
最简单的实现:
function trim(str){
return str.replace(/^\s+|\s+$/g, "");
}
而在官方的 polyfill 中,是这么写的:
if (!String.prototype.trim) {
String.prototype.trim = function () {
return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
};
}
\s
后面还多了两个东西:\uFEFF
和 \xA0
。有些软件,在保存一个以UTF-8
编码的文件时,会在文件开始的地方插入三个不可见的字符(0xEF 0xBB 0xBF,即 BOM),转码后是\uFEFF
,因此我们在读取时需要自己去掉这些字符。\xA0
其实就是 HTML 中常见的
(一个空格,之所以要在 HTML 中使用
来转义空格字符,是因为在 HTML 文档中多个连续的空格字符会被合并成一个,而使用
转义字符就可以显示连续的空格符了)。
考虑下面的代码,都会打印出什么?
console.log(1 << 3);
console.log(12 >> 4);
console.log(24 >>> 4);
console.log(11 | 6);
console.log(15 ^ 20);
console.log(~13);
答案:8 0 1 15 27 -14
这些运算符在平时可能很少用到,而且每个语言中基本都有这些运算符(可能符号不一样,但功能是一样的)。
首先要说一下几个概念:源码、反码、补码、符号位。
在 JavaScript 中,按位操作符可以将操作数当作 32 位的比特序列进行运算,最左边的位是符号位,0 表示正数,1 表示负数,其他位用来表示数值。
+0
的补码等于 +0
的源码;-0
的补码等于 -0
的反码加一(算上符号位,也就是 -0 的补码与 +0 的补码一样)。在运算时,所有的按位操作符的操作数都会被转成 补码 形式的有符号 32 位整数。
按位操作的操作数有效范围是 -(2^31) ~ 2^31 - 1
。之所以是 31,是因为有一位是符号位。2^31 -1
是因为正整数表示不了 2^31
这个数,这个数会溢出。例如,假如一个 4 位的有符号数,它的最大表示数值为:0111
(补码等于源码),0
是符号位,数值位 111
化成十进制就是 7
,它是 2^3 - 1
,8
的补码等于 0000
,溢出的最高位会被丢弃,在 4 位的有符号数中,7
如果再加一结果就会变成 0
。因此呢,32 位的符号数,最大表示数值是 2^31 - 1
。
负数最高数值并没有减一,假如一个四位的有符号数是 -8
,那它的源码就是 1000
(溢出的 1
被截掉),它的反码就是 1111
(符号位不变,数值位取反),它的补码就是 1000
(反码加一),-8
可以表示出来,感觉 1000
好像是 -0
啊~,-0
的源码确实是这个,但它还要转成反码:1111
,它的补码等于反码加一(而且还算上符号位),结果等于 0000
(进位 1 被丢弃)。
<<
左移操作
1 化成二进制:1;将 1 往左移动三位,就变成了 1000
(右边补三个零)。1 << 31
会得到负数中最后一个操作数。>>
右移操作
12 化成二进制:1100;向右移动三位,右边删掉三个数位:1
。(2**31 - 1) >> 31
会得到 0
。>>>
无符号右移
向右被移出的位被丢弃,左侧用 0 填充。对于负数,符号位会被移动,前面补 0,变成一个整数,例如:
-9 : 11111111111111111111111111110111 -9 >>> 2 : 00111111111111111111111111111101 = 1073741821|
按位“或” 都是 0 时结果是 0,其他都是 1;^
按位“异或” 相同为 0,不同为 1;
a^a
等于 0;a^0
等于 a
;a^a^a
等于 a
。&
按位“与” 都是 1 时结果是 1,其他结果都是 0;~
按位“非” 0 变为 1,1 变为 0(包括符号位也会变);需要注意的是:~1
等于 -2
,~-1
等于 0
;~0
等于 -1
;~-2
等于 1
;~2
等于 -3
运算符优先级可以参考 MDN 上的表格:运算符优先级表格[1]
2 << 1
就等于 2*2
;3 << 4
就等于 3* (2**4)
。
8 >> 1
就等于 8 / 2
;12 >> 2
就等于 12 / 2 / 2
;
如果是小数右移,会将结果向下取整:
6.4 >> 2; // 1 --> (6.4 / 2 / 2) == 1.6 --> 1
9.6 >> 2; // 2 --> (9.6 / 4 = 2.4) --> 2
在计算数组某段的中间索引时,可以这样:
function getMidIdx(from, to){
// from 表示开始的索引
// to 表示结束的索引
return from + ((to - from) >> 1);
}
例如下面的例子,用异或交换两个整数类型的数字变量:
var n1 = -3, n2 = 4;
n1 ^= n2; // n1 与 n2 异或,然后把结果再赋给 n1
n2 ^= n1;
n1 ^= n2;
console.log(n1, n2); // 4 -3
这种方式只能交换整数,小数运算时小数部分会被截掉。
使用异或还可以判断两个数是不是异号的。例如:
var a1 = 3, a2 = -4;
function isDiffSign(n1, n2){
return (n1 ^ n2) < 0; // 符号位异或时,符号不同异或就会变成负数(不同为 1)
}
console.log(isDiffSign(a1, a2)); // true
在 leetcode 上有这么一道题目:
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次或偶数次。找出那个只出现了一次的元素。
我们就可以使用异或来做这道题。因为 0^a
总是等于 a
,而 a^a
总是等于 0,a^a^a
也就等于它本身了。代码如下:
/**
* @param {number[]} nums
* @return {number}
*/
var singleNumber = function(nums) {
let n = 0; // 初始是 0
for(let i = 0;i < nums.length;i ++){
n = n ^ nums[i]; // 异或运算
}
return n;
};
因为如果一个数是 2 的指数的话,它的二进制表示就绝对是只有一个 1 存在。在判断时,我们可以将这个数减去 1,减去 1 后的数的二进制表示就会变成全是 1,没有零。这俩在进行"与"操作,结果就是 0。
function isTwoIndex(number){
return number & (number - 1) === 0;
}
console.log(isTwoIndex(16)); // true
[1]
运算符优先级表格: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Operator_Precedence