JavaScript 笔试题(三)

2020-06-30 10:15:11 浏览数 (1)

1. 引用类型

一个经典的例题:

代码语言:javascript复制
var a = { n: 1 };   // 1
var b = a;          // 2
a.x = a = { n: 2 };     // 3

console.log(a.x);   // ?
console.log(b);     // ?

答案:a.x 是 undefined;b 是 {n: 1, x: {n: 2}}

首先,a 是对象,是个引用类型,存储的指针指向 {n: 1} 这个对象。

第二行把指针 a 赋给 b,a 和 b 都指向 {n: 1};

第三行,首先会对 a.x 进行查找,没有找到就会先赋值 undefined,即:{n: 1, x: undefined}。此时 a 和 b 都指向同一个对象。然后 a 变量又赋值成一个新的对象:{n: 2},最后把新的 a 赋值给 x(前面的 a. 已经被替换成了原来的 a 所指向的那个内存中的对象),x 就有值了,b 就变成了:

代码语言:javascript复制
{
    n: 1,
    x: { n: 2 }
}

这个题目最大的疑惑可能就是在第三行,a 变量的指向已经变了,而 a.x 会误认为新的 a 中有变量 x 并且它指向 a。事实上在执行第三行代码时,先处理了 a.x,让 x 先等于 undefined(属性访问优先级要比赋值优先级高),我觉得可以把此时的 a.x 看作是一个整体,它是旧的 a 指向的对象中的一个变量,正在等待赋值,当赋值时,这个变量指向 {n:2}。

2. 判断对象类型

因为 typeof null === "object" 成立,而 null 并不是对象。

代码语言:javascript复制
function isObject(obj){
    return Object.prototype.toString.call(obj) === "[object Object]";
}

Object.prototype.toString.call(null) 的结果是 [object Null]

或者(这种方式不好的一点是不能过滤掉 Date、Array 等类型的数据):

代码语言:javascript复制
function isObject(obj){
    return typeof obj === "object" && obj !== null;
}

3. isInteger 函数

封装一个 isInteger 函数,用于检测传入的值是整数。

代码语言:javascript复制
function isInteger(num){
    return typeof num === "number"
                    && isFinite(num)
                    && value % 1 === 0;
}

上面代码中,isFinite 是一个全局的函数,用来判断一个数字是不是有限的,例如:

代码语言:javascript复制
// 下面三个都是无限的
isFinite(Infinity);  // false
isFinite(NaN);       // false
isFinite(-Infinity); // false

isFinite(0);     // true
// 对于非数值的参数会转换成数值
isFinite("0");  // true

Number 对象中也有一个 isFinite 函数,与全局的 isFinite 不同的是:它不会强制将一个非数值的参数转换成数值,这就意味着,只有数值类型的值,且是有穷的。

4. isNaN2 函数

isNaN 用来判断传入的参数是不是 NaN,是就返回 true。它的 polyfill 如下:

代码语言:javascript复制
function isNaN(value){
    var n = Number(value);
    // NaN !== NaN 将返回 true,它是一个自身不等于自身的值
    return n !== n;
}

isNaN 的不足:如果它的参数既不是 NaN,也不是数字,而是一个其他的类型变量,例如:一个字符串,这个字符串不能转化成数字,返回的结果不是 false,而是 true。我们要做的是传入参数的是 NaN 才被判定是 true,不然就返回 false。代码如下:

代码语言:javascript复制
function isNaN2(value){
    return typeof value === 'number' && isNaN(value);
}

typeof NaN 的结果是 number

5. 小数相乘

编写一个函数,能让两个并不大的小数正确相乘。

在 JavaScript 这门语言中的 Number 类型是IEEE 754的双精度数值,IEEE 754 标准就是一个对实数进行计算机编码的标准。这个标准在进行小数运算时精度可能会有不足,使用了 IEEE 754 标准的语言进行小数运算时会出现精度问题,这种问题不止 JS 这门语言独有。

例如下面的运算并没有得到预期的结果:

代码语言:javascript复制
15.2 * 3.5  // 结果:53.199999999999996,期望结果:53.2
0.1   0.2   // 结果:0.30000000000000004,期望结果:0.3

回到问题,编写一个 accmul 函数,让两个并不大的小数正确相乘。代码如下:

代码语言:javascript复制
function accMul(n1, n2){
    var m = 0,
        s1 = n1.toString(),
        s2 = n2.toString();

    m  = s1.split(".")[1].length;
    m  = s2.split(".")[1].length;
    var result = Number(s1.replace(".", "")) * Number(s2.replace(".", ""));
    result = result / Math.pow(10, m);
    return result;
}

如果 n1n2 都是小数,先用 toString 将两个小数转成字符串,调用 split 方法分隔整数部分与小数部分,然后拿到小数部分的长度,相乘后的结果的小数位数等于相乘前两个小数的小数位数相加。result 是两个小数转成整数后相乘,再根据总的小数长度得到最终的运算结果。

6. 属性继承

如何判断对象中的某个属性是继承来的?

在 js 对象中,使用对象里的某个属性或者方法时,这个属性或者方法不一定存在于这个对象当中,也可能是继承来的。例如:

代码语言:javascript复制
var obj = {};   // 该对象没有任何属性和方法
console.log(obj.toString());    // 却可以调用 toString 方法

如何判断对象中的某个属性是继承来的?可以利用 in 运算符和 hasOwnProperty 方法来判断。

  • in 运算符可以判断属性是否在指定的对象或其原型链中;
  • hasOwnProperty 方法可以判断对象自身属性中是否具有指定的属性;

代码如下:

代码语言:javascript复制
function isInherit(prop, object){
    return prop in object && !object.hasOwnProperty(prop);
}

7. 实现 instanceof 功能

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。例如:

代码语言:javascript复制
function C(){}
var o = new C();
o instanceof C;     // true
o instanceof Object;    // true
function D(){}
o instanceof D;     // false

实现:

代码语言:javascript复制
function myInstanceof(object, constructor){
    let proto = Object.getPrototypeOf(object);
    let prototype = constructor.prototype;
    while(true){
        // 原型链的最上层是 null
        if(proto === null)  return false;
        if(proto === prototype) return true;
        proto = Object.getPrototypeOf(proto);
    }
}

getPrototypeOf 方法可以返回指定对象的原型,相当于 object.__proto__。如果没有继承,则返回 null

Object.prototype.proto === null // true

8. ES5 继承

常见的一种是“借用构造函数”,例如:

代码语言:javascript复制
function Person (a, b) {
    this.a = a;
    this.b = b;
}
Person.prototype.getA = function(){
    return this.a;
}

function Sub (a, b) {
    // 执行 Persion,让其内部的 this 指向 Sub 的实例
    Person.call(this, a, b);
}
// 把 Sub 的原型指向 Person 的实例,这样 Sub 就可以用到 Person 原型上的属性或方法了
Sub.prototype = new Person;
// 把构造函数改回来
Sub.prototype.constructor = Sub;

这种方式会调用 Person 函数两次,另一种常用的方式是“寄生组合式”继承。通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。例如:

代码语言:javascript复制
function Person (a, b) {
    this.a = a;
    this.b = b;
}
Person.prototype.getA = function(){
    return this.a;
}

function Sub (a, b) {
    Person.call(this, a, b);
}

// 专门用于继承的函数
function inherit (P, C) {
    // P 是父类,C 是子类
    // 调用 create 方法后,prototype.__proto__ === P.prototype
    var prototype = Object.create(P.prototype);
    prototype.constructor = C;
    C.prototype = prototype;
}
inherit(Person, Sub);
// Sub 的实例对象的 __proto__ 是 Sub.prototype;
// 而 Sub.prototype 是空的,它只有一个 constructor,指向 Sub;
// Sub.prototype.__proto__ 指向 P.prototype

9. 实现 reduce 函数

reduce 是 ES6 中数组的一个方法,它可以接收两个参数,一个是回调函数,一个是初始值(可选参数)。回调函数有四个参数:

  • accumulator 累计器累计回调的返回值;
  • currentValue 数组中正在处理的元素;
  • index 数组中正在处理的当前元素的索引,可选参数;
  • array 调用 reduce 的数组,可选参数;

reduce 如果没有第二参数,将使用数组中的第一个元素作为初始值,在没有初始值的空数组上调用 reduce 将报错。

比如下面的例子,求数组值的总和并加一。

代码语言:javascript复制
let res = [1,2,3,4].reduce((acc, elem) => {
    acc  = elem;    // 每次累加器都等于前一个累加器加上当前的数组元素
    return acc;     // 返回累加器,确保下一次迭代继续使用
},1);   // 累加器 acc 的初始值设置成 1
console.log(res);   // 11

实现

代码语言:javascript复制
function reduce (ary, cb, initialVal) {
    if(Object.prototype.toString.call(ary) !== '[object Array]')
        throw new TypeError(ary   ' is not an Array.');
    if(typeof cb !== 'function')
        throw new TypeError(cb   ' is not a function.');

    var len = ary.length;
    var value, idx = 0;
    if(initialVal === undefined) {
        if(!len)
            // 空数组,没有初始值时 报错
            throw new Error('Reduce of empty array with no initial value.');
        // 不是空数组,累加器初始值设置成数组的第一个元素
        value = ary[idx   ];
    }else
        // 否则,初始值设置成 initialVal
        value = initialVal;

    while(idx < len) {
        // 迭代
        value = cb(value, ary[idx], idx, ary);
        idx   ;
    }
    return value;
}

10. 实现 filter 函数

实现如下:

代码语言:javascript复制
function filter (ary, cb, thisArg) {
    if(Object.prototype.toString.call(ary) !== '[object Array]')
        throw new TypeError(ary   ' is not an Array.');
    if(typeof cb !== 'function')
        throw new TypeError(cb   ' is not a function.');

    var len = ary.length,
        res = [],
        idx = 0;

    if (thisArg === undefined) {
        // 如果没有传入第三个参数
        while (idx < len) {
            if(cb(ary[idx], idx, ary)) {
                res.push(ary[idx]);
            }
            idx   ;
        }
    } else {
        while (idx < len) {
            // 传入了第三个参数就要给 cb 绑定 this
            if( cb.call(thisArg, ary[idx], idx, ary)) {
                res.push(ary[idx]);
            }
            idx   ;
        }
    }
    return res;
}

验证:

代码语言:javascript复制
var obj = { num: 3 };
// 绑定 this
let res = filter([4,7,9,11,44,16], function (elem) {
    return elem % this.num === 1;
}, obj);
console.log(res);   // [4, 7, 16]

需要注意的是,如果绑定 this,最好不要使用箭头函数。因为给箭头函数绑定 this 会被忽略。

相关文章:

JavaScript笔试题(一)

JavaScript笔试题(二)

JavaScript严格模式

0 人点赞