🔥JavaScript中的经典面试题(上)

125 阅读9分钟

+++第一题:

let num = parseFloat('width:100px'); //NaN
if (num == 100) { alert(1); }
else if (num == NaN) { alert(2); } //NaN!==NaN  Object.is(NaN,NaN)===true
else if (typeof num == 'number') { alert(3); }
else { alert(4); }
答案:3
+++++基于parseInt(val)/parseFloat(val)数据转换
  • val一定是一个字符串,如果不是则先转换为字符串
  • 然后从字符串左侧第一个字符开始查找,遇到一个非有效数字字符则结束查找,把找到的有效数字字符串转换为数字,一个找不到结果则是NaN

+++第二题:

let result = 100 + true + 21.2 + null + undefined + "Tencent" + [] + null + 9 + false;
console.log(result);
答案:"NaNTencentnull9false"
+++++把其它数据类型隐式转换为number类型:Number([val])
  1. 字符串:空字符串转换为0,只要字符串出现非有效数字,结果就是NaN
  2. 布尔类型:true -> 1 false -> 0
  3. null - > 0 undefined -> NaN
  4. symbol会报错 BigInt会去掉'n'(可能出现科学计数法)
  5. 对象:Symbol.toPrimitive -> valueOf -> toString -> 转换为数字

+++第三题:

var a = { n: 1 }; 
var b = a; 
a.x = a = { n: 2 };
console.log(a.x);
console.log(b);
答案:undefined  {n:1,x:{n:2}}
+++++解析:

var a={n:1}此时的a为一个对象用0x000来代表其堆内存地址,则a=0x000 {n:1}

var b=a此时b的空间地址也是0x000

a.x=a={n:2}考察的为连等式从右向左开始,例如:a=b=13 => b=13 a=13但是此时的a.x成员访问优先级很高,所以a.x=0x001 {n:2} a=0x001 {n:2}

因为是先执行的a.x=0x001 {n:2},所以空间地址0x000的内容发生变化,成为了{n:1,x:0x001}==>{n:1,x:{n:2}}

当代码执行到console.log(a.x)的时候a所指向的空间地址为0x001 {n:2}里边没有x这个属性,所以结果为undefined.

当代码执行到console.log(b)的时候b所指向的空间地址依旧为0x000,但是内容变成了{n:1,x:0x001}==>{n:1,x:{n:2}}

+++第四题:

a等于什么值会使条件成立「两种方案」
var a = ?;
a.toString = a.shift;
if (a == 1 && a == 2 && a == 3) {
	console.log('OK');
}
+++++方案一:利用 对象==数字 会把对象转换为数字「我们重写转换的某一步骤」
var a = {
  i: 0
};
a[Symbol.toPrimitive] = function () {
  return ++this.i;
};
if (a == 1 && a == 2 && a == 3) {
  console.log('OK');
}
var a = [1, 2, 3];
a.toString = a.shift;
if (a == 1 && a == 2 && a == 3) {
  console.log('OK');
}
+++++方案二:利用 Object.defineProperty 实现数据劫持
var i = 0;
Object.defineProperty(window, 'a', {
  get() {
    return ++i;
  }
});
if (a == 1 && a == 2 && a == 3) {
  console.log('OK');
}

+++第五题:

let arr = [27.2, 0, '0013', '14px', 123];
arr = arr.map(parseInt);
console.log(arr);
答案:[27,NaN,1,1,27]
+++++parseInt([val],[radix])
  • [radix]不写或者是0:则默认是十进制「如果val字符串是以0x开始的,则默认值是16」
  • [radix]有效范围2~36,不再这个范围内,则结果是NaN
  • 从[val]字符串右侧第一个字符查找,找到所有符合[radix]进制的值(遇到一个不符合的则结束查找),最后转换为10 进制的数字
  • 把其它进制转换为十进制:按权展开求和

arr.map(parseInt)==>map(function(item.index))==>map(parseInt(item,index))所以返回数组的第一项可以看做是parseInt('27.2',0) -> parseInt('27.2',10)==>27;

第二项:parseInt(0,1)因为[radix]的取值范围是2~36,所以第二项为NaN

第三项:parseInt('0013',2) -> '001' 0*2^2+0*2^1+1*2^0==>1

第四项:parseInt('14px',3) -> '1' 1*3^0==>1

第五项:parseInt(123,4) -> parseInt('123',4) '123' 1*4^2+2*4^1+3*4^0=>16+8+3+>27

+++第六题:

/*下⾯代码是否可以,每隔1000MS依次输出 0 1 2 3 4 5 ? 如果不可以,说明为啥?以及如何解决?「三种⽅案处理」 */
for (var i = 0; i < 5; i++) { 
  setTimeout(function () {
    console.log(i); 
  }, (i + 1) * 1000); 
}
答案:不可以,因为var不会产生块级上下文,所以代码都在全局上下文中执行的;循环是同步任务、定时器是异步任务;先执行同步任务再执行异步任务,执行完循环之后全局的i已经变为了5,所以这样输出的结果都是5
+++++解决方案一:闭包
for (var i = 0; i < 5; i++) {
  // 第一轮 全局i=0
  // 第一轮 全局i=1
  // ...
  // 全局i=5 循环结束
  (function (i) {
    // 闭包1 私有i=0
    // 闭包2 私有i=1
    // ...
    setTimeout(function () {
      // 找的是对应闭包中的i
      console.log(i);
    }, (i + 1) * 1000);
  })(i);
} 
+++++解决方案二:let的原来还是闭包
for(let i=0;i<5;i++){
  setTimeout(function(){
    console.log(i)
  },(i+1)*1000);
}
+++++解决方案三:setTimeout(回调函数,时间,给回调函数预传递的实参值「当定时器到达事件,执行回调函数的时候,可以获取到预先传递的值,其实定时器内部也是基于“闭包”实现的!」)
for(var i=0;i<5;i++){
  setTimeout(function(){
    console.log(i);
  },(i+1)*1000,i);
}

+++第七题:

let a = 0,
  b = 0;
function A(a) {
  A = function (b) {
    alert(a + b++);
  };
  alert(a++);
}
A(1);
A(2);
答案:1 4

+++第八题:

console.log(foo);
{
  console.log(foo);
  function foo() { }
  foo = 1;
  console.log(foo);
}
console.log(foo);
答案:undefined  函数foo(){}  1  函数foo(){}

第九题:

let x = 5;
function fn(x) {
  return function (y) {
    console.log(y + (++x));
  }
}
let f = fn(6);
f(7);
fn(8)(9);
f(10);
console.log(x);
答案: 14  18  18  5

+++第十题:

const curring = function curring() {
  //编写代码实现一下输出
};

let add = curring();
let res = add(1)(2)(3);
console.log(+res); //->6

add = curring();
res = add(1, 2, 3)(4);
console.log(+res); //->10

add = curring();
res = add(1)(2)(3)(4)(5);
console.log(+res); //->15
+++++答案及解析:
// curring:柯理化函数,闭包的应用「保存应用」
const curring = function curring() {
  let arr = [];
  const add = (...params) => {
    // 把每一次add传递的值都保留起来
    arr = arr.concat(params);
    // 每一次add执行,保证其返回的还是add:这样就可以一直执行下去了
    return add;
  };
  add[Symbol.toPrimitive] = () => {
    // 把每一次传递进来的值,基于数组求和,计算出最后的结果
    return arr.reduce((res, item) => res + item, 0);
  };
  return add;
};

+++第十一题:

// 实现plus和minus,完成以下输出 
let n = 10; 
let m = n.plus(10).minus(5); 
console.log(m);//=>15(10+10-5)
+++++ n数字是Number类的实例,可以访问Number.prototype上的方法「基于内置类原型扩展方法」 n.push(10) 方法中的this -> n num ->10
const checkNum = num => {
  num = +num;
  if (isNaN(num)) return 0;
  return num;
};
Number.prototype.plus = function plus(num) {
  num = checkNum(num);
  return this + num;
};
Number.prototype.minus = function minus(num) {
  num = checkNum(num);
  return this - num;
};

第十二题:

function Foo() {
  getName = function () { console.log(1); };
  return this;
}
Foo.getName = function () { console.log(2); };
Foo.prototype.getName = function () { console.log(3); };
var getName = function () { console.log(4); };
function getName() { console.log(5); }
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();
答案:2 4 1 1 2 3 3

+++第十三题:重写内置new的方法

function Dog(name) {
  this.name = name;
}
Dog.prototype.bark = function () { console.log('wangwang'); }
Dog.prototype.sayName = function () { console.log('my name is ' + this.name); }

function _new(ctor) {
 //=>完成你的代码
}
let sanmao = _new(Dog, '三⽑');
sanmao.bark(); //=>"wangwang" 
sanmao.sayName(); //=>"my name is 三⽑" 
console.log(sanmao instanceof Dog); //=>true 
+++++内置new的方法
function _new(ctor) {
  // 4.对ctor的格式进行校验:必须是一个函数,但是还不能是Symbol/BigInt
  if (typeof ctor !== "function") throw new TypeError("ctor is not a constructor");
  if (ctor === Symbol || ctor === BigInt) throw new TypeError("Symbol/BigInt is not a constructor");

  // 1.创建一个实例对象「空对象、对象.__proto__===Dog.prototype」
  var obj = Object.create(ctor.prototype);

  // 2.像普通函数一样执行,需要让函数中的this指向创建的实例对象
  var params = [].slice.call(arguments, 1);
  var result = ctor.apply(obj, params);

  // 3.检测函数执行的返回值,如果返回的是原始值类型的值,则默认把实例对象返回
  if (obj !== null && /(object|function)/i.test(typeof result)) return result;
  return obj;
}

+++第十四题:

let obj = {
  2: 3, 
  3: 4, 
  length: 2,
  push: Array.prototype.push
};
obj.push(1);
obj.push(2);
console.log(obj);
答案:{2: 1, 3: 2, length: 4, push: ƒ}
+++++鸭子类型

obj是个“伪/类数组”,不能直接使用数组原型上的方法,如果想使用

  1. 调用数组原型上的方法,让方法中的this指向类数组,实现方法的借用,例如:[].slice.call(arguments)
  2. 把需要借用的方法赋值给对象的私有属性
  1. 把类数组转化为数组,例如:Array.from([value])

此题需要了解push方法的内置操作

Array.prototype.push = function push(val) { 
  // this->arr  val->10
  this[this.length]=val;
  this.length++;
  return this.length;
};
arr.push(10); 

通过上方的代码我们就可以清除的了解到这道题中obj.push(1)的一个大概过程:

let obj = {
  2: 3, //属性值3就变成了1
  3: 4, //属性值3就变成了2
  length: 2,//length经过两次的obj.length++变为了4
  push: Array.prototype.push
};
obj.push(1);
//this->obj  val->1
//=> obj[obj.length]=1; => obj[2]=1;
//=> obj.length++;
obj.push(2);
//this-> obj  val->2
//=> obj[obj.length]=2; => obj[3]=2;
//=> obj.length++;
console.log(obj);

+++第十五题:基于JS重写内置call方法

Function.prototype.call = function call(context, ...params) {
  // this->fn  context->obj  params->[10, 20]
  let key = Symbol("KEY"),
    result;
  // @4 对context做格式校验:要求context必须是个对象类型值、如果是null/undefined则让其为window
  if (context == null) context = window;
  if (!/(object|function)/i.test(typeof context)) context = Object(context);
  // @1 给context设置一个属性,让其属性值等于要被执行的方法“this”「设置的属性名,不要和现有对象的属性名发生冲突:symbol类型的属性即可」
  context[key] = this; //Reflect.set(context, key, this);
  // @2 让 context.xxx 执行,这样即把方法执行了,同时也让this改为context了
  result = context[key](...params);
  // @3 把新设置的属性移除,并且把函数执行的结果返回
  delete context[key]; //Reflect.deleteProperty(context, key);
  return result;
};
+++++想要重写内置call就要了解需求
const fn = function fn(x, y) {
  console.log(this, x, y);
};
let obj = {
  name: 'zhufeng'
};
// obj.fn = fn;
// obj.fn(10, 20);
fn.call(obj, 10, 20); 
  • fn执行,让方法中的this指向obj,并且传递10/20
  • 让方法执行,让方法中的this指向某个对象,给THIS对象设置一个属性,让其属性值是要执行的函数即可

+++第十六题:基于JS重写内置bind方法

Function.prototype.bind = function bind(context, ...params) {
  // this->fn  context->obj  params->[10,20]
  let self = this;
  return function (...args) {
    // this->body  args->[ev]
    self.call(context, ...params.concat(args));
  };
};
+++++重写内置bind的需求:点击body执行fn,让fn中的this指向obj,并且分别传递10/20/事件对象
const fn = function fn(x, y, ev) {
  console.log(this, x, y, ev);
};
let obj = {
  name: 'zhufeng'
};
let body = document.body;
// body.onclick = fn; //this->body  x->事件对象  y/ev->undefined
// body.onclick = fn.call(obj, 10, 20); //这样会立即把fn执行,等不到点击操作,且把fn执行的结果赋值给事件绑定,body.onclick=undefined!
// 思路:先给事件绑定匿名函数,当点击的时候执行匿名函数;在匿名函数执行的时候,我们基于call把fn执行,实现我们想要的需求即可!!
// body.onclick = function (ev) {
//   // this->body  ev->事件对象 
//   fn.call(obj, 10, 20, ev);
// };
body.onclick = fn.bind(obj, 10, 20); 

+++第十七题:检测是否为纯对象:标准普通对象「proto===Object.prototype」、Object.create(null)

const hasOwn = Object.prototype.hasOwnProperty;
const isPlainObject = function isPlainObject(obj) {
  let proto, Ctor;
  // 如果obj是null或者undefined,再或者检测数据类型的结果不是对象,则obj一定不是纯粹对象
  if (!obj || Object.prototype.toString.call(obj) !== "[object Object]") return false;
  // 获取obj指向的原型对象
  proto = Object.getPrototypeOf(obj);
  // 匹配 Object.create(null)
  if (!proto) return true;
  // 有原型对象则检测其是否为Object.prototype
  Ctor = hasOwn.call(proto, 'constructor') && proto.constructor;
  return typeof Ctor === "function" && Ctor === Object;
}; 

+++第十八题:实现数组和对象的深拷贝deepClone

++++@1 JSON.parse(JSON.stringify(obj))先转换为字符串,再把字符串转换为对象,这样涉及的所有内存空间都会重新的开辟,实现深拷贝的效果!
  • 处理不了BigInt类型值 TypeError:Do not know how to serialize a BigInt
  • symbol/undefined/function类型的值会丢失
  • 正则和错误对象会变为{}
  • 日期对象会直接变为字符串,就转不回来了
  • ...

原因:JSON.stringify无法对这些类型的值进行处理「可以正常处理的:数字、字符串、普通对象、数组对象、null等格式」

const hasOwn = Object.prototype.hasOwnProperty;
const isPlainObject = function isPlainObject(obj) {
  let proto, Ctor;
  if (!obj || Object.prototype.toString.call(obj) !== "[object Object]") return false;
  proto = Object.getPrototypeOf(obj);
  if (!proto) return true;
  Ctor = hasOwn.call(proto, 'constructor') && proto.constructor;
  return typeof Ctor === "function" && Ctor === Object;
};
// 实现对数组和对象的迭代
const each = function each(obj, callback) {
  if (typeof callback !== "function") throw new TypeError("callback is not a function");
  if (Array.isArray(obj)) {
    for (let i = 0; i < obj.length; i++) {
      let item = obj[i];
      let res = callback(item, i);
      if (res === false) break;
    }
  }
  if (isPlainObject(obj)) {
    let keys = Reflect.ownKeys(obj);
    for (let i = 0; i < keys.length; i++) {
      let key = keys[i],
        value = obj[key];
      let res = callback(value, key);
      if (res === false) break;
    }
  }
  return obj;
};

const deepClone = function deepClone(obj, already) {
  // 预防死递归
  if (typeof already === "undefined") already = [];
  if (already.includes(obj)) return obj;
  already.push(obj);
  // 如果obj是原始值类型(或函数),则直接返回对应的值即可
  if (obj == null || !/object/i.test(typeof obj)) return obj;
  let isArray = Array.isArray(obj),
    isObject = isPlainObject(obj),
    ctor = obj.constructor,
    newObj;
  if (!isArray && !isObject) {
    // 既不是数组也不是纯粹的对象:创造相同构造函数的不同实例即可
    return new ctor(obj);
  }
  // 是数组或者对象:则循环进行克隆「递归,只要符合条件,都需要处理」
  newObj = new ctor();
  each(obj, (value, key) => {
    newObj[key] = deepClone(value, already);
  });
  return newObj;
};

+++第十九题:

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
    console.log('async2');
}
console.log('script start');
setTimeout(function () {
    console.log('setTimeout');
}, 0)
async1();
new Promise(function (resolve) {
    console.log('promise1'); 
    resolve();
}).then(function () {
    console.log('promise2');
});
console.log('script end');
答案:'sctipt start'  'async1 start'  'async2'  'promise1'  'script end'  'async1 end'  'promise2'  'steTimeout'

+++第二十题:

let body = document.body;
body.addEventListener('click', function () {
    Promise.resolve().then(() => {
        console.log(1);
    });
    console.log(2);
});
body.addEventListener('click', function () {
    Promise.resolve().then(() => {
        console.log(3);
    });
    console.log(4);
});
答案:2 1 4 3

+++第二十一题:

console.log('start');
let intervalId;
Promise.resolve().then(() => {
    console.log('p1');
}).then(() => {
    console.log('p2');
});
setTimeout(() => {
    Promise.resolve().then(() => {
        console.log('p3');
    }).then(() => {
        console.log('p4');
    });
    intervalId = setInterval(() => {
        console.log('interval');
    }, 3000); 
    console.log('timeout1');
}, 0); 
答案:'start'  'p1'  'p2'  'timeout1'  'p3'  'p4'  'interval'每三秒打印一次

+++第二十二题:实现一个sleep函数的定义,让sleep的功能setTimeout类似,但是是promise风格的使用方式

function sleep(time) {
    // 具体实现代码 
}
sleep(2000).then(function () {
    console.log('logged after 2 seconds.');
});

答案:

const sleep = function sleep(time) {
    time = +time || 0;
    if (time < 0) time = 0;
    return new Promise(resolve => {
        setTimeout(() => {
            resolve();
        }, time);
    });
};

拓展:不能使用循环的方式实现每隔一秒打印出的数字累加1

(async function () {
    console.log(1);
    await sleep(1000);
    console.log(2);
    await sleep(1000);
    console.log(3);
    await sleep(1000);
    console.log(4);
})();