# 1. 关于函数的 length 属性
360 面试过程遇到一个很有趣的问题,是关于函数的 length 属性的,题简写如下
(() => 1).length === 0; // 输出什么
我所理解的拥有 length
的对象一般都是数组或者类数组对象,或者定义了 length
属性的对象,所以我回答说这个应该是 false
吧,后来面试告诉我函数是有 length
属性的,函数的 length
属性就是函数参数的个数,瞬间我恍然大悟,函数的参数就是 arguments
,而 arguments
也是一个类数组对象所以他是有 length
属性的
// so
(() => 1).length === 0; // 输出 true
(a => a).length; // 输出 1
2
3
但也不完全是这样,函数的参数分为实参
和形参
,实参的数量是 argument.length
,而行参的数量是函数的length
function fn(a, b) {
console.log(arguments.length, fn.length);
}
fn(1, 2, 3, 4) // 4,2
2
3
4
# 2. 数组中字符串键值的处理
在 JavaScript 中数组是通过数字进行索引,但是有趣的是他们也是对象,所以也可以包含 字符串
键值和属性,但是这些不会被计算在数组的长度(length)内
如果字符串键值能够被强制类型转换为十进制数字的话,它就会被当做数字索引来处理
const arr = [];
arr[0] = 1;
arr['1'] = '嘿嘿';
arr['cym'] = 'cym';
console.log(arr); // [1, '嘿嘿', cym: 'cym']
console.log(arr.length); // 2
2
3
4
5
6
# 3. 类型转换问题
原题:如何让 (a == 1 && a == 2 && a == 3) 的值为 true?
这个问题考查的数据类型转换, ==
类型转换有个基本规则
NaN
与任何值都不相等,包括自己本身undefined
与null
相等(==),其他都不等- 对象与字符串类型做比较,会把对象转换成字符串然后做比较
- 其他类型比较都要转换成
数字
做比较
那么这个问题我们重写 toString
或者 valueOf
方法就可以了
const a = {
val: 1,
toString() {
return this.val++;
},
};
if (a == 1 && a == 2 && a == 3) {
console.log('ok');
}
2
3
4
5
6
7
8
9
还有一种方法实现
var i = 1;
Object.defineProperty(window, 'a', {
get() {
return i++;
},
});
if (a == 1 && a == 2 && a == 3) {
console.log('OK');
}
2
3
4
5
6
7
8
9
10
# 拓展一下 [] == ![] 为什么是 true
上面隐式类型转换规则中提到,其他类型比较都要转换成数字做比较,这个就是对应那条规则的
- 首先
[].toString()
会得到一个''
字符串 ![]
得到一个布尔值false
''
与false
比较肯定要转换成数字比较- 那么
''
转换则为0
,false
转换也是0
- 所以这道题就是
true
# 3. 如何让 (a == 1 && a == 2 && a == 3) 的值为 true
- 这是一道经典的面试题,主要考察是数据类型转换,我们重写 toString 或者 valueOf 方法即可解决
const n = {
i: 1,
toString() {
return n.i++;
},
// 两个写一个即可
valueOf() {
return n.i++;
},
};
if (n == 1 && n == 2 && n == 3) {
console.log('通过');
}
2
3
4
5
6
7
8
9
10
11
12
13
14
- 当然也有其他解决技巧
const n = 0;
!(n == 1 && n == 2 && n == 3); // true
2
- 利用数组
toString
方法会调用本身的join
方法,这里把自己的join
方法改写为shift
方法,每次返回第一个元素,而且每次数组删除第一个值,正好可以使判断成立。
var n = [1, 2, 3];
n.join = n.shift;
if (n == 1 && n == 2 && n == 3) {
console.log('通过');
}
2
3
4
5
# 4. jsonp
当出现端口、协议、域名三者有一个不一样的时候就会出现跨域,跨域解决方案很多,这里实现一个 jsonp
jsonp
是利用 script、img、iframe、link
等带有的 src
属性请求可以跨域加载资源,而不受同源策略的限制。 每次加载时都会由浏览器发送一次 GET 请求,通过 src
属性加载的资源
// 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);
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# 5. 图片懒加载
工作中经常会用到图片,当图片过多的时候,通常会做懒加载优化加载请求,懒加载就是优先加载可视区域内的内容,其他部分等进入了可视区域内在去加载
图片懒加载的原理很简单,需要做到两点即可实现:
图片是否要加载取决于它的
src
属性。在初始化的时候我们不给图片设置 src 属性,而给一个其他属性设置图片的真实地址,当图片需要加载时候在给图片的src
设置属性,此时就可以做到懒加载当图片进入可视区域的时候,我们就需要加载图片了。可视区域就是当图片元素的相对于
可视区域的高度
小于可视区域的高度
的时候说明元素进入视口了
# 可视区域高度
可是区域就是浏览器中我们可以看见的高度,可以使用 window.innerHeight
或者 document.documentElement.clientHeight
获取到
当元素 顶边距离
距离小于 可视窗口
时说明元素要进入可视区域了
# getBoundingClientRect
element.getBoundingClientRect()
返回值是一个 DOMRect 对象,这个对象是由该元素的 getClientRects() 方法返回的一组矩形的集合,就是该元素的 CSS 边框大小。返回的结果是包含完整元素的最小矩形,并且拥有 left, top, right, bottom, x, y, width, 和 height 这几个以像素为单位的只读属性用于描述整个边框。除了 width 和 height 以外的属性是 相对于视图窗口的左上角
来计算的。
我们可以用这个 api 来获取图片相对于可视区域左上角的高度,它永远是个相对高度,此时可以写一个是否进入可视区域的方法
const viewHeight = window.innerHeight || document.documentElement.clientHeight;
function isInViewport(el) {
const { top } = el.getBoundingClientRect;
return top <= viewHeight;
}
2
3
4
5
6
对于滚动这种高频事件我们一般都会做防抖处理,连续触发后只执行最后一次
function debounce(fn, delay = 500) {
let timer;
return function(...args) {
if (timer) clearTimeout(timer);
setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
2
3
4
5
6
7
8
9
贴上完整代码
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();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# 6. Generator 问题
# 对象增加迭代器
类数组对象的特征:必须有长度、索引、能够被迭代,否则这个对象不可以使用 ...
语法转数组,我们可以使用 Array.from 转,当然我们也可以给对象添加一个迭代器
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
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
上面的问题可以字节使用生成器来实现,生成器返回一个迭代器,迭代器有 next 方法,调用 next 方法可以返回 value 和 done
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++]
}
}
2
3
4
5
6
7
8
9
10
11
12
# 实现一个字符串的迭代器
实现一个字符串的迭代器:传入一组字符串并返回单个字符的范例。一旦更新的字符串,输出也跟着替换掉旧的
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 }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 简单模拟 co
模拟一下 co 的实现
首先来看一则例子
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);
});
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
使用 co 库可以很容易解决这个问题
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();
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 7. 斐波那契数列
- 今天新东方的面试还提到了斐波那契数列,其实这个东西蛮很有趣,简单介绍一下
- 1、1、2、3、5、8、13、21、34 ....
- 这道题有个规律,第一项加上第二项永远等于第三项:1 + 1 = 2;1 + 2 = 3;2 + 3 = 5;3 + 5 = 8 ....
- 要求是传入第几项,得到该值,根据这个规律来实现一下
# 经典写法
function fibonacci(n) {
// 第一项和第二项都返回1
if (n === 1 || n === 2) return 1;
// 我们只要返回 n - 1(n的前一项)与 n - 2(n的前两项)的和便是我们要的值
return fibonacci(n - 1) + fibonacci(n - 2);
}
2
3
4
5
6
# 缓存写法
上面的写法,求 20 次以内的总和运行会很快,50 次以上特别慢,100 次 以上可能就爆栈了,所以我们需要优化写法,缓存每次计算后的值
function feibo(n, sum1 = 1, sum2 = 1) {
if (n === 1 || n === 2) return sum2;
return feibo(n - 1, sum2, sum1 + sum2);
}
2
3
4
这种写法缓存了,每次计算后的值,执行效率会很高,100 次以上也会秒返回结果,这个也叫作尾递归优化
# 缓存写法
这中写法就有动态规划的意思了,利用 dp[n] = dp[n - 1] + dp[n - 2]
的递推公式,把所有计算结果缓存在 memo 里面,最后返回 memo[n] 即可
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];
}
2
3
4
5
6
7
8
# 动态规划
动态规划直接根据递推公式 dp[n] = dp[n - 1] + dp[n - 2]
写就可以
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];
}
2
3
4
5
6
7
8
9
# 8. 观察者与发布订阅
一直以来,我以为发布订阅和观察者是一个思路,一次偶然的机会我发现他们是两种不同的设计思路
虽然他们都是 实现了对象的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都将得倒通知,然后自动更新
。但是他们之间是有一定区别的。
# 观察者模式
观察者模式会有 观察者
与 被观察者(观察目标)
两个对象存在,观察者可以有多个,观察目标可以添加多个观察者,可以通知观察者。观察者模式是面向与目标和观察者编程的,耦合目标和观察者
// 被观察者
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('你好');
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# 发布订阅模式
发布订阅模式会有一个调度中心的概念。是面向调度中心编程的,对发布者与订阅者解耦,例如 node 中的 emitter
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));
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# 9. 下面代码输出什么,为什么
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);
2
3
4
5
6
7
8
9
10
结果:输出 obj 是 [empty × 2, 1, 2, splice: ƒ, push: ƒ]
一个对象如果有 length 属性,length 属性可以告诉我们对象的元素个数,基本上就满足一个类数组对象了
当对象带有数组的
splice
方法并且length
属性的值可以转为数值时,对象将会被当做数组打印。obj 调用数组的 push 方法自身的 length 属性就会 ++,此时调用两次,length 就变成了 4
- 第一次 push:
obj[2] = 1; obj.length += 1
- 第二次 push:
obj[3] = 2; obj.length += 1
- 使用 console.log 输出的时候,因为 obj 具有 length 属性和
splice
方法,故将其作为数组进行打印,没有splice
还是以对象形式打印 - 打印时因为数组未设置下标为 0 1 处的值,故打印为 empty,主动 obj[0] 获取为 undefined
- 第一次 push:
# 10. 输出以下代码的执行结果并解释为什么
var a = {
n: 1,
};
var b = a;
a.x = a = {
n: 2,
};
console.log(a.x);
console.log(b.x);
2
3
4
5
6
7
8
9
10
结果: a.x 为 undefined;b.x 为 { n: 2 }
- 首先
.
的优先级要比=
优先级要高,所以 a.x 要先执行; - 相当于为 a(或者 b)所指向的
{ n: 1 }
对象新增了一个属性 x,即此时对象将变为{ n: 1, x: undefined}
。 - 随后按照正常赋值从右往左进行赋值,在执行
a = { n: 2 }
时,a 的引用发生改变,指向了新对象,而不会对 b 造成影响 - 接着执行
a.x = { n:2 }
的时候,并不会重新解析一遍 a,而是沿用最初解析 a.x 时候的 a,所以此时旧对象的 x 的值为{ n:2 }
,旧对象为{ n: 1, x: { n:2 } }
,它被 b 引用着。 - 所以最终打印结果:
a.x 为 undefined;b.x 为 { n: 2 }
参考: 优先级和结合性 (opens new window)
# 11. 打印出 1 - 10000 之间的所有对称数
对称数:121,1331 之类的,只要数字反转后等于原来值,就满足我们要的结果
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--;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16