以下的题来自 高级前端进阶博文 | 木易杨前端进阶 (muyiy.cn)
-
实现对象的 Map 函数类似
Array.prototype.map
- 直接的方法
Object.prototype.map = function (fn) {
if(typeof fn !== 'function'){
throw new TypeError(`${fn} is not a function !`);
}
let res = {}
Object.keys(this).forEach((key)=>{
res[key] = fn(this[key],key,this)
})
return res
}
- 后来发现别人使用
JSON.stringify()
,JSON.stringify
的第二个参数replacer
可以是方法(使用方式如下)和数组(数组的值就代表了将被序列化成 JSON 字符串的属性名)
Object.prototype.map = function (fn) {
if (typeof fn !== "function") {
throw new TypeError(`${fn} is not a function !`);
}
return JSON.parse(
JSON.stringify(this, (key, val) => {
if (key !== "") {
return fn.call(this, val, key, this);
} else {
return val;
}
})
);
};
-
['1', '2', '3'].map(parseInt)
what & why ?
map
的参数是[item,index,arr]
parseInt
的参数是[string,radix]
,string
为要解析的值,radix
为把string
当成是多少进制的数(2-36)
之间.
如果 radix
是 undefined
、0
或未指定的,JavaScript
会假定以下情况:
- 如果输入的
string
以'0x'
或'0X'
(一个0
,后面是小写或大写的X
)开头,那么radix
被假定为16
,字符串的其余部分被当做十六进制数去解析。 - 如果输入的 string以
'0'
(0)开头,radix
被假定为8
(八进制)或10
(十进制)。具体选择哪一个radix
取决于实现。ECMAScript 5
澄清了应该使用10
(十进制)
['1', '2', '3'].map(parseInt)
// [ 1, NaN, NaN ]
parseInt('1',0) // 所以这里是10进制转换 = 1
parseInt('2',1) // radix 不在2-36之间,规定 === NAN
parseInt('3',2) // radix === 2,为二进制,但是二进制的数不能大于2(string===3),所以 ===NAN
-
什么是防抖和节流?有什么区别?如何实现?
防抖:动作发生一定时间(如:500)后触发事件,在此期间(500以内)再次触发方法,事件则再等待一定时间(500)后再触发
function debounce(fn, delay = 500) {
let timer;
return function () {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn.apply(this, arguments);
}, delay);
};
}
节流:动作发生一定时间(如:500)内只能触发一次事件
function throttle(fn, delay = 500) {
let timer;
return function () {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, arguments);
}, delay);
}
};
}
let fn = (...args) => {
console.log(args);
};
let fn2 = debounce(fn);
// let fn2 = throttle(fn);
let arr = [1, 2, 3];
arr.forEach((e) => {
fn2(e);
});
-
介绍下 Set、Map、WeakSet 和 WeakMap 的区别
Set
一种类似数组的结构,但是里面的值不能重复
let set = new Set();
set.add(1); //{1 }
set.add(2); //{1,2}
console.log([...set.entries()]); //[[1,1],[2,2]] 为了对应map的结构
set.delete(1); //{1}
set.has(1); //false
set.size; //1
set.forEach; //同Array.prototype.forEach
set.keys(); //[2]
set.values(); //[2]
set.clear(); //{}
set.add(NaN); //{NaN}
set.add(NaN); //{NaN}
WeakSet
类似Set
,但是只能存储对象,存储的对象的值都是弱引用的,如果没有其他的变量或属性引用这个对象,则这个对象会被垃圾回收掉,WeakSet无法被遍历
let weakSet = new WeakSet();
let a = { a: 1 };
weakSet.add(a); //{{a:1}}
weakSet.has(a); //true
weakSet.delete(a); //{}
Map
一种字典与Object类似,但是Map的key可以是任何值,而object
的key
只能是String
,Symbol
,( 其他的类型使用
key是经过
toString`后的)
let map = new Map();
map.set(1, 2); //{1=>2}
map.get(1); //2
map.keys(); //[1]
map.values(); //[2]
console.log(...map.entries()); //[1,2]
map.size; //1
WeakMap
是一组键值对的集合,其中key
是弱引用的对象,而值则不限制,若果key
引用的对象没有其他的引用,这个对象会被回收,所以WeakMap的key
是不可枚举的,WeakMap
不能遍历
let weakMap = new WeakMap();
let b = { b: 1 };
weakMap.set(b, 2); //{{b:1}=>2}
weakMap.get(b); //2
weakMap.has(b); //true
weakMap.delete(b); //{}
-
ES5/ES6 的继承除了写法以外还有什么区别(不全面)
Class
声明和let
,const
类似可能会导致暂时性死区
const Foo = function() {}
{
const foo = new Foo(); //Uncaught ReferenceError: Cannot access 'Foo' before initialization
class Foo{}
}
Class
声明内部会使用严格模式
const Foo = function() {
foo2 = 1
}
const foo = new Foo()
class Bar{
constructor() {
bar2 = 1 //bar2 is not defined
}
}
const bar = new Bar()
Class
的所有方法(包括静态方法和示例方法)都是不可枚举的Class
的所有方法(包括静态方法和示例方法)都没有prototype
对象所以也没有[[construct]]
,不能使用new
来调用ES5
和ES6
子类this
生成顺序不同,ES5
的继承实质上是先创建子类的实例对象this
,然后再调用父类的构造函数(Super.apply(this)
)修饰this
,ES6
中的Class
会先创建父类的this
,子类继承父类的this
(super()
),然后在子类的构造函数修饰this
// ES5
function Super () {
}
function Sub () {
// 已经创建了Sub的this实例了,再调用父类的构造函数修饰this
Super.apply(this)
}
Sub.prototype = new Super()
Sub.prototype.constructor =Sub
//ES6
class Super2{
constructor(){
console.log(1)
}
}
class Sub2 extends Super2{
constructor(){
console.log(this)
// Must call super constructor in derived class before accessing 'this' or returning from derived constructor
// 必须在派生类访问'this'或从派生构造函数返回之前 ,调用super()
super()
}
}
-
有以下 3 个判断数组的方法,请分别介绍它们之间的区别和优劣
Object.prototype.toString.call()
、instanceof
以及Array.isArray()
Object.prototype.toString.call()
以[object type]
的形式返回传入的变量的类型的字符串,如果type===Array
,则变量为数组,这个方法可以判断所有的基本类型
console.log(Object.prototype.toString.call([]))//[object Array]
a instanceof b
用于检测构造函数b
的prototype
属性是否出现在实例对象a
的原型链上面
console.log([] instanceof Array) //true
Array.isArray(a)
直接判断变量a
是否为数组类型
console.log(Array.isArray([]))//true
-
关于 const 和 let 声明的变量不在 window 上,那到底在哪里?如何去获取?
从上图可以看出,var
定义的属性c
是全局变量,而let
,const
,定义的属性啊啊a
,b
只是脚本上面的变量,是一个块级的作用域
8. ## 下面的代码打印什么内容,为什么?
var b = 10;
(function b() {
b = 20;
console.log(b) //ƒ b() { b = 20;console.log(b)}
})()
解释是:具名自执行函数的变量为只读属性,不可修改(不太确定)
-
简单改造下面的代码,使之分别打印 10 和 20。
var b = 10;
(function b(){
b = 20;
console.log(b);
})();
打印10
var b = 10;
console.log(b)
(function b(){
b = 20;
console.log(b);
})();
打印20
var b = 10;
(function b(){
let b = 20;
console.log(b);
})();
-
下面代码输出什么
var a = 10;
(function () {
console.log(a)
a = 5
console.log(window.a)
var a = 20;
console.log(a)
})()
var a = 10;
(function () {
// var a = 20中a变量提升 var a
console.log(a) // undefined
//这里上面提升的变量var a 赋值为5 var a = 5(所以这里的a已经是局部变量了)
a = 5
console.log(window.a)//10,window.a这个a是全局的window的属性
变量重新赋值var a = 20
var a = 20;
console.log(a)//20
})()
-
使用 sort() 对数组
[3, 15, 8, 29, 102, 22]
进行排序,输出结果
默认没有函数 是按照 UTF-16
排序的,对于字母数字 你可以利用 ASCII
进行记忆
[3, 15, 8, 29, 102, 22].sort() //[102,15,22,29,3,8]
-
call 和 apply 的区别是什么,哪个性能更好一些
call
分别接受参数apply
接受数组作为参数 经过测试call
的性能比较好,可能是因为少了数组解构的过程
-
输出以下代码的执行结果并解释为什么
var a = {n: 1};
var b = a;
a.x = a = {n: 2};
console.log(a.x)
console.log(b.x)
- 先获取左侧的
a.x
,由于x
并不存在,于是js引擎会为对象a
创建一个新成员x,初始值为undefined
,创建完成后,目标指针已经指向了这个新成员x
,并会先挂起,等号右侧的内容有结果了,便完成赋值 - 接着执行赋值语句的右侧,这时变量
a
作为新的对象{n:2}
的引用,和{n:1}
没关系了,然后把新的对象{n:2}
的引用赋值给x
(此时的x
属于对象{n:1}
) - 此时
a = {n:2}
,b={n:1,x:{n:2}}
,a.x = undefined
,b.x={n:2}
-
箭头函数与普通函数(
function
)的区别是什么?构造函数(function
)可以使用new
生成实例,那么箭头函数可以吗?为什么?
- 箭头函数没有自己的
this
,它的this
是定义时所在的上层作用域的this
,所以this
的指向是固定的 - 箭头函数没有
arguments
对象 - 箭头函数不可以使用
yield
命令,所以不能用做Generator
函数 - 箭头函数没有
prototype
属性 - 箭头函数不可以使用new生成实例
- 没有自己的
this
,所以无法在实例化时将自己的this
指向实例对象` - 没有
prototype
属性,而new命令需要将构造函数的prototype
赋值给新对象的__proto__
属性)
- 没有自己的
-
a.b.c.d
和a['b']['c']['d']
,哪个性能更高?
a.b.c.d
更高,从两种形式的结构来看,显然编译器解析前者要比后者容易些,自然也就快一点。 下图是两者的 AST 对比:
-
ES6 代码转成 ES5 代码的实现思路是什么
- 将代码字符串转成ast语法树
- 将ast语法树按照一定的规则转成对应es5的ast树
- 将es5的ast树转成对应的代码
-
为什么普通
for
循环的性能远远高于forEach
的性能,请解释其中的原因
- 本人测试的时候
10
万级别的时候forEach
的速度远远比for
更快,是for
的10倍,应该是forEach
的底层改过了 forEach
比for
多了很多执行上下文,参数声明等
-
数组里面有10万个数据,取第一个元素和第10万个元素的时间相差多少
消耗时间差不多,差异可以忽略不计
js
中的数组并不是使用连续的内存空间的(现在有些JavaScript
引擎已经在为同种数据类型的数组分配连续的存储空间了),而是一种哈希映射关系,可以根据键名key
直接计算出值存储的位置,所以查起来都是O(1)
-
输出以下代码运行结果
// example 1
var a={}, b='123', c=123;
a[b]='b';
a[c]='c';
console.log(a[b]);
---------------------
// example 2
var a={}, b=Symbol('123'), c=Symbol('123');
a[b]='b';
a[c]='c';
console.log(a[b]);
---------------------
// example 3
var a={}, b={key:'123'}, c={key:'456'};
a[b]='b';
a[c]='c';
console.log(a[b]);
- 对象的键名只能是字符串和
Symbol
(任何一个symbol
类型的值都是不相等的) 类型。 - 其他类型的键名会被转换成字符串类型。
- 对象转字符串默认会调用
toString
方法=== [object Object]
。
// example 1
var a={}, b='123', c=123;
a[b]='b';
a[c]='c';
//[123].toSring()='123'
console.log(a[b]);//'c'
---------------------
// example 2
var a={}, b=Symbol('123'), c=Symbol('123');
a[b]='b';
a[c]='c';
// symbol
console.log(a[b]);//'b'
---------------------
// example 3
var a={}, b={key:'123'}, c={key:'456'};
a[b]='b';
a[c]='c';
console.log(a[b]);//'c'
-
input 搜索如何防抖,如何处理中文输入
<input id='myinput'>
function throttle(timeout){
var timer;
function input(e){
if(e.target.composing){
return ;
}
if(timer){
clearTimeout(timer);
}
timer = setTimeout(() => {
console.log(e.target.value);
timer = null;
}, timeout);
}
return input;
}
function onCompositionStart(e){
e.target.composing = true;
}
function onCompositionEnd(e){
//console.log(e.target)
e.target.composing = false;
var event = document.createEvent('HTMLEvents');
event.initEvent('input');
e.target.dispatchEvent(event);
}
var input_dom = document.getElementById('myinput');
input_dom.addEventListener('input',jeiliu(1000));
input_dom.addEventListener('compositionstart',onCompositionStart);
input_dom.addEventListener('compositionend',onCompositionEnd);
-
var、let 和 const 区别的实现原理是什么
var
会直接在栈内存中分配空间,等到语句执行时(在语句执行前已经完成了声明和初始化),赋值对应的变量,如果是引用类型的话则会在堆内存中开辟一个内存空间存储实际内容,栈内则会存储一个指向堆内存的指针let
不会在栈内存中预分配空间,而且在栈内存分配变量时(声明已经提升,但是没有初始化),做一个检查,如果已经有相同变量名存在就会报错(暂时性死区)const
也不会预分配空间,行为和let
一样,不过const
变量不可修改,对于基本类型来说,不能修改变量的值,对于应用类型来说,无法修改栈内存分配的指针,但是可以修改指针指向的对象的值
-
写出如下代码的打印结果
function changeObjProperty(o) {
o.siteUrl = "http://www.baidu.com"
o = new Object()
o.siteUrl = "http://www.google.com"
}
let webSite = new Object();
changeObjProperty(webSite);
console.log(webSite.siteUrl); //http://www.baidu.com
对象作为参数,传进去的是这个对象的引用地址,o.siteUrl = "http://www.baidu.com"
,是给对象o
的siteUrl
地址赋值,所以此时的对象为{ siteUrl: 'http://www.baidu.com' }
,o = new Object()
时,把o
指向了另一个对象,此时o.siteUrl = "http://www.google.com"
是新对象的赋值,与传进函数的对象无关
-
请写出如下代码的打印结果
function Foo() {
Foo.a = function() {
console.log(1)
}//{0}
this.a = function() {
console.log(2)
}//{1}
}
Foo.prototype.a = function() {
console.log(3)
}//{2}
Foo.a = function() {
console.log(4)
} //{3}
Foo.a();
let obj = new Foo();
obj.a();
Foo.a();
4
,Foo.a
()执行的时候,{0}
,{1}
还没有执行,此时执行的是{3}
2
,obj.a()
执行的时候,{0}
,{1}
已经执行,且Foo
是构造函数,this
指向返回的obj
实例,所以此时运行的时{1}
1
, 最后一个Foo.a()
执行的时候,{0}
已经执行,这时Foo.
a的赋值为{0}
-
分别写出如下代码的返回值
String('11') == new String('11');
String('11') === new String('11');
true
String('11'
)返回的是string
类型,new String('11')
返回的是object
类型,==
会做隐式转换,两个操作数比较的时候,new String('11').toString()==='11'
false
两边类型不一样
-
请写出如下代码的打印结果
var name = 'Tom';
(function() {
if (typeof name == 'undefined') {
var name = 'Jack';
console.log('Goodbye ' + name);
} else {
console.log('Hello ' + name);
}
})();
Goodbye Jack
声明 var name = 'Jack'
时,因为var没有块级作用域,所以var name
的声明会提升至,函数作用域的顶层,此时var name = undefined
-
为什么 for 循环嵌套顺序会影响性能?
var t1 = new Date().getTime()
for (let i = 0; i < 100; i++) {
for (let j = 0; j < 1000; j++) {
for (let k = 0; k < 10000; k++) {
}
}
}
var t2 = new Date().getTime()
console.log('first time', t2 - t1)
for (let i = 0; i < 10000; i++) {
for (let j = 0; j < 1000; j++) {
for (let k = 0; k < 100; k++) {
}
}
}
var t3 = new Date().getTime()
console.log('two time', t3 - t2)
//first time 1958
//two time 10131
for(let i =0; i<5; i++){//循环体}
for 循环的顺序
1. let i = 0
2. i<5,条件为真时则继续执行
3. 循环体
4. i++
5. 然后回到第二步
所以两次循环初始化的j
,k
的次数不一样
-
输出以下代码执行结果
function wait() {
return new Promise(resolve =>
setTimeout(resolve, 10 * 1000)
)
}
async function main() {
console.time();
const x = wait();
const y = wait();
const z = wait();
await x;
await y;
await z;
console.timeEnd();
}
main();
default: 10.009s
new Promise(xx)
相当于同步任务, 会立即执行,所以想想x
,y
,z
三个任务几乎是同时执行的,最后的时间是10*1000
多一点
-
输出以下代码执行结果,大致时间就好(不同于上题)
function wait() {
return new Promise(resolve =>
setTimeout(resolve, 10 * 1000)
)
}
async function main() {
console.time();
await wait();
await wait();
await wait();
console.timeEnd();
}
main();
default: 30.027s
在async
中使用 await
修饰符,主流程必须等到wait()执行完毕之后继续运行