首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

JavaScript 笔试题(二)

1. 实现一个 new 方法

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

new 关键字会进行如下的操作:

  1. 创建一个空的简单的 JavaScript 对象(即:{});
  2. 设置该对象的构造函数到另一个对象上;
  3. 将步骤 1 新创建的对象作为 this 的上下文;
  4. 如果该对象没有返回对象,则返回 this

实现:

代码语言:javascript
复制
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 关键字上,如果你返回了一个对象,则接收时接收的会是这个对象,例如:

代码语言:javascript
复制
function Per(name, age){
    this.name = name;
    this.age = age;

    this.getName = function(){
        return this.name;
    }
    // 返回了一个对象
    return {};
}

let p = new Per("XiaoMing", 18);   // {}

如果 result 是一个对象类型的数据,则就返回它,不是的话就返回我们创建的对象实例。

2. 实现 apply、call 方法

applycall 方法类似,不同的是 apply 的第二个参数一个数组。它们都是用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数,该函数内部的 this 指向会变成我们指定的 this。

实现思路:

  1. 给传入的 this 动态定义一个方法,这个方法就是要调用的函数;
  2. 执行 this 上的这个方法;
  3. 删掉 this 上的这个方法;

代码:

代码语言:javascript
复制
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 方法与之类似:

代码语言:javascript
复制
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 函数

bind 方法与 call 方法类似,但它不会调用函数,只是绑定 this 和函数参数。如果使用 new 运算符构造绑定函数,则忽略传入的 this 参数。例如下面的代码:

代码语言:javascript
复制
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 的第一个参数将会忽略

实现代码:

代码语言:javascript
复制
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);
    }
}

3. 数组中的空值

考虑下面的代码,array 的结果是什么?

代码语言:javascript
复制
let array = [ ,1 , ,2 , ,3];
array = array.map(i => ++ i);

答案:[ ,2 , ,3 , ,4]

? 解析

array 的长度实际是 6,第一个、第三个、第五个元素都是空值(empty)。map 函数在遍历数组时会跳过运算空值,但最终结果会保留它,因此有值的元素进行了运算,空值元素不变。

? 数组中的迭代方法都是怎样对待空值的?

例如:

代码语言:javascript
复制
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());

打印结果:

代码语言:javascript
复制
1
null
undefined
2
join(''):  12
join():  ,1,,,,2
reduce:  NaN
every:  false
toString:  ,1,,,,2

forEacheveryreduce 函数都会忽略数组中的空值,但 nullundefined 并不会认为是空值,所以 reduce 返回了 NaN(undefined + 1 等于 NaN)。undefined 与数值比较结果总是 false,因此 every 最终返回了 false

join 函数的参数如果省略,数组元素用逗号(,)分隔。如果是空字符串(""),则所有元素之间都没有任何字符。如果一个元素为 undefinednull,它会被转换为空字符串。toString 的返回结果与 join 函数没有参数时返回结果一样。

其他的,如 filter 函数和 some 函数也会跳过数组中的空值,sort 函数在排序时 undefined 会排在数组后面,空值会排在 undefined 后面,无论做怎样的排序。当数组中有 null 时,就比较奇特了,例如下面的比较:

代码语言:javascript
复制
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 之后。

扩展运算符浅复制一个数组,空值会被转成 undefinedpopshift 操作一个空值时,返回的结果会被转成 undefined

4. 正则表达式

一个经典的问题:

有一个字符串类型的数字,例如:10000000,把这个数字变成下面的形式(从右往左,每个三个数字加一个逗号):

代码语言:javascript
复制
1000000 ---> 1,000,000
100000000 ---> 100,000,000

如果用正则表达式 + replace 函数如何实现?

答案:正则表达式可以写成:/(?=(\B)(\d{3})+$)/g。reaplce(reg , ",")

? 解析

\B 表示匹配非单词边界(\b 表示匹配单词边界)。例如一个字符串 hello world,h 之前有一个单词边界,lo 右边有一个单词边界,w 左边有一个单词边界,d 右边有一个单词边界。一个字符串两端会有单词边界,字符串中有空白字符时也会有单词边界。

代码语言:javascript
复制
var str = "hello world";
var reg = /\w+\b/g;
str.match(reg);     // ["hello", "world"]

非单词边界就与单词边界相反了,如:

代码语言:javascript
复制
var str = "qwer";
// qw 之间有非单词边界;we 之间有非单词边界;er 之间有非单词边界
str.match(/\B/g);
// ["","",""]

x(?=y) 这样的形式被称为“先行断言”,它用来匹配 x 仅仅当 x 后面跟着 y ,但 y 并不是匹配结果的一部分,这一点很重要,也就是说我们匹配符合条件的 x。比如下面的匹配案例:

代码语言:javascript
复制
var reg = /hello(?=\s\w+)/g;
// str 字符串里有符合条件的 "hello" 字符串
var str = "hello world";
console.log(str.match(reg));    // ["hello"]

例题中的先行断言没有 x,只有 y 部分。也就是说我们匹配的是一个空字符串(什么都没有的),这个空字符串后面有一个非单词边界和三个连续的数字,三个连续的数字可能有多个(后面有一个 +),我们要从右往左匹配,后面需要加一个 $。把匹配到的空字符替换成 ,,因为是全局匹配,因此就全部替换了。

代码语言:javascript
复制
var reg = /(?=(\B)(\d{3})+$)/g;
var str = "10000000";
console.log(str.match(reg));    // ["", ""]

\B 在这个题目中的作用是防止 , 插入到最左边,例如一个数是:100000,如果不加 \B,替换后就变成了 ,100,000。左边的 1 有单词边界,我们要匹配没有单词边界的数字。

? 其他几种断言

x(?=y) 被称为 “先行断言”,除了先行断言之外还有下面几种断言:

  1. (?<=y)x 匹配 x,仅当 x 前面是 y。这种叫做后行断言。
  2. x(?!y) 仅仅当 x 后面不跟着 y 时匹配 x,这被称为正向否定查找。
  3. (?<!y)x 仅仅当 x 前面不是 y 时匹配 x,这被称为反向否定查找。

四个边界类断言:

  1. ^ 匹配输入的开头。如果多行模式设为 true,^ 在换行符后也能匹配;
  2. $ 匹配输入的结束。如果多行模式设为 true,$ 在换行符前也能匹配;
  3. \b 匹配单词边界;
  4. \B 匹配非单词边界;

(?:x) 这种格式的匹配符与上面的断言很相似,但它不是断言。带有 ?: 的括号被称为“非捕获括号”,match 方法、exec 方法在不使用全局匹配时,都会返回匹配到的括号里的内容和全局内容。而如果加上了 ?: 的括号则不会被捕获。例如:

代码语言:javascript
复制
var reg = /(?:hello)\s?\w+(\d)$/;
var res = "hello world123".match(reg);
console.log(res);   // { 0: "hello world123", 1: "3" }
// hello 并不会匹配到

replace 中的捕获标记符也不会捕获到有 ?: 的括号。如:

代码语言:javascript
复制
var reg = /(?:hello)\s?\w+(\d)$/;   // 没有 ?: 时,$1 指向左边第一个括号
var res = "hello world123".replace(reg, "$1--aaa");
console.log(res);   // 3--aaa

5. trim 函数

实现一个字符串的 trim 函数。

trim() 方法返回一个从两头去掉空白字符的字符串,并不影响原字符串本身。

例如:

代码语言:javascript
复制
var newStr = "   hello world ".trim();
console.log(newStr);    // "hello world"

最简单的实现:

代码语言:javascript
复制
function trim(str){
    return str.replace(/^\s+|\s+$/g, "");
}

而在官方的 polyfill 中,是这么写的:

代码语言:javascript
复制
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 中常见的 &nbsp;(一个空格,之所以要在 HTML 中使用 &nbsp; 来转义空格字符,是因为在 HTML 文档中多个连续的空格字符会被合并成一个,而使用 &nbsp; 转义字符就可以显示连续的空格符了)。

6. 位操作符

考虑下面的代码,都会打印出什么?

代码语言:javascript
复制
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 - 18 的补码等于 0000,溢出的最高位会被丢弃,在 4 位的有符号数中,7 如果再加一结果就会变成 0。因此呢,32 位的符号数,最大表示数值是 2^31 - 1

负数最高数值并没有减一,假如一个四位的有符号数是 -8,那它的源码就是 1000(溢出的 1 被截掉),它的反码就是 1111(符号位不变,数值位取反),它的补码就是 1000(反码加一),-8 可以表示出来,感觉 1000 好像是 -0 啊~,-0 的源码确实是这个,但它还要转成反码:1111,它的补码等于反码加一(而且还算上符号位),结果等于 0000(进位 1 被丢弃)。

  1. << 左移操作 1 化成二进制:1;将 1 往左移动三位,就变成了 1000(右边补三个零)。1 << 31 会得到负数中最后一个操作数。
  2. >> 右移操作 12 化成二进制:1100;向右移动三位,右边删掉三个数位:1(2**31 - 1) >> 31 会得到 0
  3. >>> 无符号右移 向右被移出的位被丢弃,左侧用 0 填充。对于负数,符号位会被移动,前面补 0,变成一个整数,例如: -9 : 11111111111111111111111111110111 -9 >>> 2 : 00111111111111111111111111111101 = 1073741821
  4. | 按位“或” 都是 0 时结果是 0,其他都是 1;
  5. ^ 按位“异或” 相同为 0,不同为 1; a^a 等于 0;a^0 等于 aa^a^a 等于 a
  6. & 按位“与” 都是 1 时结果是 1,其他结果都是 0;
  7. ~ 按位“非” 0 变为 1,1 变为 0(包括符号位也会变);

需要注意的是:~1 等于 -2~-1 等于 0~0 等于 -1~-2 等于 1~2 等于 -3

运算符优先级可以参考 MDN 上的表格:运算符优先级表格[1]

? 位运算符使用技巧

1. 乘除运算

2 << 1 就等于 2*23 << 4 就等于 3* (2**4)

8 >> 1 就等于 8 / 212 >> 2 就等于 12 / 2 / 2

如果是小数右移,会将结果向下取整:

代码语言:javascript
复制
6.4 >> 2;   // 1 --> (6.4 / 2 / 2) == 1.6 --> 1
9.6 >> 2;   // 2 --> (9.6 / 4 = 2.4) --> 2

在计算数组某段的中间索引时,可以这样:

代码语言:javascript
复制
function getMidIdx(from, to){
    // from 表示开始的索引
    // to 表示结束的索引
    return from + ((to - from) >> 1);
}

2. 两个数字交换

例如下面的例子,用异或交换两个整数类型的数字变量:

代码语言:javascript
复制
var n1 = -3, n2 = 4;
n1 ^= n2;   // n1 与 n2 异或,然后把结果再赋给 n1
n2 ^= n1;
n1 ^= n2;
console.log(n1, n2);    // 4 -3

这种方式只能交换整数,小数运算时小数部分会被截掉。

使用异或还可以判断两个数是不是异号的。例如:

代码语言:javascript
复制
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 也就等于它本身了。代码如下:

代码语言:javascript
复制
/**
 * @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;
};

3. 判断一个数是不是 2 的指数

因为如果一个数是 2 的指数的话,它的二进制表示就绝对是只有一个 1 存在。在判断时,我们可以将这个数减去 1,减去 1 后的数的二进制表示就会变成全是 1,没有零。这俩在进行"与"操作,结果就是 0。

代码语言:javascript
复制
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

下一篇
举报
领券