Skip to content

面试题收录

1. 关于函数的 length 属性

360 面试过程遇到一个很有趣的问题,是关于函数的 length 属性的,题简写如下

js
(() => 1).length === 0; // 输出什么

我所理解的拥有 length 的对象一般都是数组或者类数组对象,或者定义了 length 属性的对象,所以我回答说这个应该是 false 吧,后来面试告诉我函数是有 length 属性的,函数的 length 属性就是函数参数的个数,瞬间我恍然大悟,函数的参数就是 arguments ,而 arguments 也是一个类数组对象所以他是有 length 属性的

js
// so
(() => 1).length === 0; // 输出 true
(a => a).length; // 输出 1

但也不完全是这样,函数的参数分为实参形参,实参的数量是 argument.length,而行参的数量是函数的length

js
function fn(a, b) {
  console.log(arguments.length, fn.length);
}
fn(1, 2, 3, 4) // 4,2

2. 数组中字符串键值的处理

在 JavaScript 中数组是通过数字进行索引,但是有趣的是他们也是对象,所以也可以包含 字符串 键值和属性,但是这些不会被计算在数组的长度(length)内

如果字符串键值能够被强制类型转换为十进制数字的话,它就会被当做数字索引来处理

js
const arr = [];
arr[0] = 1;
arr['1'] = '嘿嘿';
arr['cym'] = 'cym';
console.log(arr); // [1, '嘿嘿', cym: 'cym']
console.log(arr.length); // 2

3. 类型转换问题

原题:如何让 (a == 1 && a == 2 && a == 3) 的值为 true?

这个问题考查的数据类型转换, == 类型转换有个基本规则

  • NaN 与任何值都不相等,包括自己本身
  • undefinednull 相等(==),其他都不等
  • 对象与字符串类型做比较,会把对象转换成字符串然后做比较
  • 其他类型比较都要转换成 数字 做比较

那么这个问题我们重写 toString 或者 valueOf 方法就可以了

js
const a = {
  val: 1,
  toString() {
    return this.val++;
  },
};
if (a == 1 && a == 2 && a == 3) {
  console.log('ok');
}

还有一种方法实现

js
var i = 1;
Object.defineProperty(window, 'a', {
  get() {
    return i++;
  },
});

if (a == 1 && a == 2 && a == 3) {
  console.log('OK');
}

拓展一下 [] == ![] 为什么是 true

上面隐式类型转换规则中提到,其他类型比较都要转换成数字做比较,这个就是对应那条规则的

  • 首先 [].toString() 会得到一个 '' 字符串
  • ![] 得到一个布尔值 false
  • ''false 比较肯定要转换成数字比较
  • 那么 '' 转换则为 0false 转换也是 0
  • 所以这道题就是 true

3. 如何让 (a == 1 && a == 2 && a == 3) 的值为 true

  • 这是一道经典的面试题,主要考察是数据类型转换,我们重写 toString 或者 valueOf 方法即可解决
js
const n = {
  i: 1,
  toString() {
    return n.i++;
  },
  // 两个写一个即可
  valueOf() {
    return n.i++;
  },
};

if (n == 1 && n == 2 && n == 3) {
  console.log('通过');
}
  • 当然也有其他解决技巧
js
const n = 0;
!(n == 1 && n == 2 && n == 3); // true
  • 利用数组 toString 方法会调用本身的 join 方法,这里把自己的 join 方法改写为 shift 方法,每次返回第一个元素,而且每次数组删除第一个值,正好可以使判断成立。
js
var n = [1, 2, 3];
n.join = n.shift;
if (n == 1 && n == 2 && n == 3) {
  console.log('通过');
}

4. jsonp

当出现端口、协议、域名三者有一个不一样的时候就会出现跨域,跨域解决方案很多,这里实现一个 jsonp

jsonp 是利用 script、img、iframe、link 等带有的 src 属性请求可以跨域加载资源,而不受同源策略的限制。 每次加载时都会由浏览器发送一次 GET 请求,通过 src 属性加载的资源

js
// callbackName 要与后端返回的一致
function jsonp(url, query, callbackName = 'getData') {
  return new Promise((resolve, reject) => {
    const scriptEl = document.createElement('script');
    const queryObj = parseQuery(query);
    const onDone = () => {
      delete window[callbackName];
      document.body.removeChild(scriptEl);
    };
    url += `?callback=${callbackName}${queryObj && '&' + queryObj}`;
    scriptEl.src = url;
    window[callbackName] = res => {
      onDone();
      if (res) {
        resolve(res);
      } else {
        reject('没有获取到数据');
      }
    };
    scriptEl.onerror = () => {
      onDone();
      reject('脚本加载失败');
    };
    document.body.appendChild(scriptEl);
  });
}

function parseQuery(query) {
  let queryStr = '';
  for (const key in query) {
    if (Object.hasOwnProperty.call(query, key)) {
      queryStr += `${key}=${query[key]}&`;
    }
  }
  return queryStr.slice(0, -1);
}

// 使用
jsonp('http://localhost:3000/getData', {
  a: 1,
  b: 2,
})
  .then(res => {
    console.log('🚀 ~ jsonp ~ res', res);
  })
  .catch(err => {
    console.log('🚀 ~ jsonp ~ err', err);
  });

5. 图片懒加载

工作中经常会用到图片,当图片过多的时候,通常会做懒加载优化加载请求,懒加载就是优先加载可视区域内的内容,其他部分等进入了可视区域内在去加载

图片懒加载的原理很简单,需要做到两点即可实现:

  1. 图片是否要加载取决于它的 src 属性。在初始化的时候我们不给图片设置 src 属性,而给一个其他属性设置图片的真实地址,当图片需要加载时候在给图片的 src 设置属性,此时就可以做到懒加载

  2. 当图片进入可视区域的时候,我们就需要加载图片了。可视区域就是当图片元素的相对于 可视区域的高度 小于 可视区域的高度 的时候说明元素进入视口了

可视区域高度

可是区域就是浏览器中我们可以看见的高度,可以使用 window.innerHeight 或者 document.documentElement.clientHeight 获取到

当元素 顶边距离 距离小于 可视窗口 时说明元素要进入可视区域了

getBoundingClientRect

element.getBoundingClientRect() 返回值是一个 DOMRect 对象,这个对象是由该元素的 getClientRects() 方法返回的一组矩形的集合,就是该元素的 CSS 边框大小。返回的结果是包含完整元素的最小矩形,并且拥有 left, top, right, bottom, x, y, width, 和 height 这几个以像素为单位的只读属性用于描述整个边框。除了 width 和 height 以外的属性是 相对于视图窗口的左上角 来计算的。

我们可以用这个 api 来获取图片相对于可视区域左上角的高度,它永远是个相对高度,此时可以写一个是否进入可视区域的方法

js
const viewHeight = window.innerHeight || document.documentElement.clientHeight;

function isInViewport(el) {
  const { top } = el.getBoundingClientRect;
  return top <= viewHeight;
}

对于滚动这种高频事件我们一般都会做防抖处理,连续触发后只执行最后一次

js
function debounce(fn, delay = 500) {
  let timer;
  return function(...args) {
    if (timer) clearTimeout(timer);
    setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

贴上完整代码

js
const viewHeight = window.innerHeight || document.documentElement.clientHeight;
// 是否满足加载条件
function isInViewport(el) {
  const { top } = el.getBoundingClientRect();
  return top <= viewHeight;
}

// 防抖处理
function debounce(fn, delay) {
  let timer;
  return function(...args) {
    if (timer) clearTimeout(timer);
    setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

// 图片加载个数
let count = 0;

// 懒加载核心
function lazyLoad() {
  const imgs = document.getElementsByTagName('img');
  const len = imgs.length;
  for (let i = 0; i < len; i++) {
    const el = imgs[i];
    if (isInViewport(el)) {
      const src = el.getAttribute('data-src');
      if (src) {
        el.src = src;
        el.removeAttribute('data-src');
        if (++count === len) {
          // 图片都加载完成后移除事件
          removeEvent();
        }
      }
    }
  }
}

// 防抖处理懒加载函数,方便移除事件监听
function debounceLazyLoad() {
  return debounce(lazyLoad, 500)();
}

// 绑定事件函数
function bindEvent() {
  // 页面加载完成执行一次
  window.addEventListener('load', debounceLazyLoad);
  // 绑定滚动事件
  document.addEventListener('scroll', debounceLazyLoad);
}
// 满足条件后移除事件
function removeEvent() {
  window.removeEventListener('load', debounceLazyLoad);
  document.removeEventListener('scroll', debounceLazyLoad);
}
// 绑定事件
bindEvent();

6. Generator 问题

对象增加迭代器

类数组对象的特征:必须有长度、索引、能够被迭代,否则这个对象不可以使用 ... 语法转数组,我们可以使用 Array.from 转,当然我们也可以给对象添加一个迭代器

js
const obj = {
    0: 1,
    1: 2,
    2: 3,
    3: 4,
    length: 4,
    [Symbol.iterator]() {
        let idx = 0
        return {
            next() {
                return {
                    value: obj[idx],
                    done: idx++ >= obj.length,
                }
            }
        }
    }
}
// 此时对象就被添加了迭代器
[...obj] // 1 2 3 4
for (const val of obj) {
    console.log(val) // 1 2 3 4
}

上面的问题可以字节使用生成器来实现,生成器返回一个迭代器,迭代器有 next 方法,调用 next 方法可以返回 value 和 done

js
const obj = {
        0: 1,
        1: 2,
        2: 3,
        3: 4,
        length: 4,
        [Symbol.iterator]: function*() {
            let idx = 0
            while (idx !== this.length) {
                yield this[idx++]
            }
        }

实现一个字符串的迭代器

实现一个字符串的迭代器:传入一组字符串并返回单个字符的范例。一旦更新的字符串,输出也跟着替换掉旧的

js
function generator(str) {
  let idx = 0;
  return {
    next() {
      return {
        value: str[idx],
        done: idx++ >= str.length,
      };
    },
  };
}
// 测试
const str = 'as';
let gen = generator(str);
console.log(gen.next());
console.log(gen.next());
console.log(gen.next());
console.log(gen.next());
gen = generator('str');
console.log(gen.next());
console.log(gen.next());
console.log(gen.next());
console.log(gen.next());
// { value: 'a', done: false }
// { value: 's', done: false }
// { value: undefined, done: true }
// { value: undefined, done: true }
// { value: 's', done: false }
// { value: 't', done: false }
// { value: 'r', done: false }
// { value: undefined, done: true }

简单模拟 co

模拟一下 co 的实现

首先来看一则例子

js
const fs = require('fs');
const path = require('path');
const { promisify } = require('util');
const readFile = promisify(fs.readFile);

function* read() {
  const name = yield readFile(path.resolve(__dirname, 'name.txt'), 'utf8');
  const age = yield readFile(path.resolve(__dirname, name), 'utf8');
  return age;
}

const it = read();

let { value, done } = it.next();
value.then(data => {
  let { value, done } = it.next(data);
  // console.log(data, '???')
  value.then(data => {
    let { value, done } = it.next(data);
    console.log(value);
  });
});

使用 co 库可以很容易解决这个问题

js
const co = require('co');
// co 接受一个生成器
co(read()).then(data => {
  console.log(data);
});
// 那模拟一下
function _co(it) {
  // 首先返回一个 promise
  return new Promise((resolve, reject) => {
    // 因为可以传值的原因,不可以直接使用循环实现,需要使用 递归
    function next(data) {
      const { value, done } = it.next(data);
      if (done) return resolve(value);
      // 保证值是一个 promise
      Promise.resolve(value).then(data => {
        next(data);
      }, reject);
    }
    next();
  });
}

7. 斐波那契数列

  • 今天新东方的面试还提到了斐波那契数列,其实这个东西蛮很有趣,简单介绍一下
  • 1、1、2、3、5、8、13、21、34 ....
  • 这道题有个规律,第一项加上第二项永远等于第三项:1 + 1 = 2;1 + 2 = 3;2 + 3 = 5;3 + 5 = 8 ....
  • 要求是传入第几项,得到该值,根据这个规律来实现一下

经典写法

js
function fibonacci(n) {
  // 第一项和第二项都返回1
  if (n === 1 || n === 2) return 1;
  // 我们只要返回 n - 1(n的前一项)与 n - 2(n的前两项)的和便是我们要的值
  return fibonacci(n - 1) + fibonacci(n - 2);
}

缓存写法

上面的写法,求 20 次以内的总和运行会很快,50 次以上特别慢,100 次 以上可能就爆栈了,所以我们需要优化写法,缓存每次计算后的值

js
function feibo(n, sum1 = 1, sum2 = 1) {
  if (n === 1 || n === 2) return sum2;
  return feibo(n - 1, sum2, sum1 + sum2);
}

这种写法缓存了,每次计算后的值,执行效率会很高,100 次以上也会秒返回结果,这个也叫作尾递归优化

缓存写法

这中写法就有动态规划的意思了,利用 dp[n] = dp[n - 1] + dp[n - 2] 的递推公式,把所有计算结果缓存在 memo 里面,最后返回 memo[n] 即可

js
function memory(n, memo = []) {
  if (n === 1 || n === 2) {
    return 1;
  } else if (!memo[n]) {
    memo[n] = memory(n - 1) + memory(n - 2);
  }
  return memo[n];
}

动态规划

动态规划直接根据递推公式 dp[n] = dp[n - 1] + dp[n - 2] 写就可以

js
function dibDp(n) {
  const dp = [];
  dp[0] = 1;
  dp[1] = 1;
  for (let i = 2; i < n; i++) {
    dp[i] = dp[i - 1] + dp[i - 2];
  }
  return dp[n - 1];
}

8. 观察者与发布订阅

一直以来,我以为发布订阅和观察者是一个思路,一次偶然的机会我发现他们是两种不同的设计思路

虽然他们都是 实现了对象的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都将得倒通知,然后自动更新 。但是他们之间是有一定区别的。

观察者模式

观察者模式会有 观察者被观察者(观察目标) 两个对象存在,观察者可以有多个,观察目标可以添加多个观察者,可以通知观察者。观察者模式是面向与目标和观察者编程的,耦合目标和观察者

js
// 被观察者
class Subject {
  constructor() {
    this.observes = [];
  }
  add(ob) {
    this.observes.push(ob);
    return this;
  }
  notify(...args) {
    this.observes.forEach(ob => ob.update(...args));
    return this;
  }
}
// 观察者
let id = 0;
class Observer {
  constructor(name) {
    this.name = name || ++id;
  }
  update(...args) {
    console.log(`${this.name} 收到了通知:${args}`);
  }
}

// 使用
const o1 = new Observer('fecym');
const o2 = new Observer('ys');
const o3 = new Observer();
const o4 = new Observer();

const s = new Subject();
// 添加观察者
s.add(o1)
  .add(o2)
  .add(o3)
  .add(o4);
// 通知观察者
s.notify('你好');

发布订阅模式

发布订阅模式会有一个调度中心的概念。是面向调度中心编程的,对发布者与订阅者解耦,例如 node 中的 emitter

js
class Emitter {
  constructor() {
    this.callbacks = {};
  }
  on(type, fn) {
    if (!this.callbacks[type]) {
      this.callbacks[type] = [];
    }
    this.callbacks[type].push(fn);
    return this;
  }
  emit(type, ...args) {
    if (!this.callbacks[type]) return;
    this.callbacks[type].forEach(fn => fn(...args));
    return this;
  }
  off(type, fn) {
    if (!this.callbacks[type]) return;
    this.callbacks[type].find((handler, idx) => {
      if (fn === handler) {
        this.callbacks[type].splice(idx, 1);
      }
    });
    return this;
  }
  once(type, fn) {
    const wrapFn = (...args) => {
      fn(...args);
      this.off(type, fn);
    };
    this.on(type, wrapFn);
  }
}
const em = new Emitter();

const fn1 = (a, b) => console.log('哈哈哈哈哈第一次', a, b);
const fn2 = a => console.log('哈哈哈哈哈第二次', a);
const fn3 = a => console.log('测试 once', a);
em.on('fecym', fn1);
em.on('fecym', fn2);
em.emit('fecym', 1, 2);
em.off('fecym', fn2);
em.emit('fecym', 1, 132);
em.once('aaa', fn3(1));

9. 下面代码输出什么,为什么

js
var obj = {
  '2': 3,
  '3': 4,
  length: 2,
  splice: Array.prototype.splice,
  push: Array.prototype.push,
};
obj.push(1);
obj.push(2);
console.log(obj);

结果:输出 obj 是 [empty × 2, 1, 2, splice: ƒ, push: ƒ]

  • 一个对象如果有 length 属性,length 属性可以告诉我们对象的元素个数,基本上就满足一个类数组对象了

  • 当对象带有数组的 splice 方法并且 length 属性的值可以转为数值时,对象将会被当做数组打印。

  • obj 调用数组的 push 方法自身的 length 属性就会 ++,此时调用两次,length 就变成了 4

    1. 第一次 push:obj[2] = 1; obj.length += 1
    2. 第二次 push:obj[3] = 2; obj.length += 1
    3. 使用 console.log 输出的时候,因为 obj 具有 length 属性和 splice 方法,故将其作为数组进行打印,没有 splice 还是以对象形式打印
    4. 打印时因为数组未设置下标为 0 1 处的值,故打印为 empty,主动 obj[0] 获取为 undefined

10. 输出以下代码的执行结果并解释为什么

js
var a = {
  n: 1,
};
var b = a;
a.x = a = {
  n: 2,
};

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

结果: a.x 为 undefined;b.x 为 { n: 2 }

  1. 首先 . 的优先级要比 = 优先级要高,所以 a.x 要先执行;
  2. 相当于为 a(或者 b)所指向的 { n: 1 } 对象新增了一个属性 x,即此时对象将变为 { n: 1, x: undefined}
  3. 随后按照正常赋值从右往左进行赋值,在执行 a = { n: 2 } 时,a 的引用发生改变,指向了新对象,而不会对 b 造成影响
  4. 接着执行 a.x = { n:2 }的时候,并不会重新解析一遍 a,而是沿用最初解析 a.x 时候的 a,所以此时旧对象的 x 的值为 { n:2 },旧对象为 { n: 1, x: { n:2 } },它被 b 引用着。
  5. 所以最终打印结果:a.x 为 undefined;b.x 为 { n: 2 }

参考: 优先级和结合性

11. 打印出 1 - 10000 之间的所有对称数

对称数:121,1331 之类的,只要数字反转后等于原来值,就满足我们要的结果

js
let i = 10000;
const res = [];
const reverse = x =>
  Number(
    x
      .toString()
      .split('')
      .reverse()
      .join('')
  );
while (i >= 0) {
  if (i === reverse(i)) {
    res.push(i);
  }
  i--;
}
最近更新