每日一题 js基础

126 阅读11分钟

以下的题来自 高级前端进阶博文 | 木易杨前端进阶 (muyiy.cn)

  1. 实现对象的 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. ['1', '2', '3'].map(parseInt) what & why ?

map的参数是[item,index,arr] parseInt的参数是[string,radix],string为要解析的值,radix为把string当成是多少进制的数(2-36)之间.

如果 radixundefined0或未指定的,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
  1. 什么是防抖和节流?有什么区别?如何实现?

防抖:动作发生一定时间(如: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);
});
  1. 介绍下 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可以是任何值,而objectkey只能是StringSymbol,( 其他的类型使用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); //{}
  1. ES5/ES6 的继承除了写法以外还有什么区别(不全面)

  • Class声明和letconst类似可能会导致暂时性死区
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 来调用
  • ES5ES6 子类 this 生成顺序不同,ES5的继承实质上是先创建子类的实例对象this,然后再调用父类的构造函数(Super.apply(this))修饰thisES6中的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()
  }
}
  1. 有以下 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 用于检测构造函数bprototype属性是否出现在实例对象a的原型链上面
console.log([] instanceof Array) //true
  • Array.isArray(a) 直接判断变量a是否为数组类型
console.log(Array.isArray([]))//true
  1. 关于 const 和 let 声明的变量不在 window 上,那到底在哪里?如何去获取?

image.png 从上图可以看出,var 定义的属性c是全局变量,而letconst,定义的属性啊啊ab只是脚本上面的变量,是一个块级的作用域 8. ## 下面的代码打印什么内容,为什么?

var b = 10;
(function b() {
  b = 20;
  console.log(b) //ƒ b() { b = 20;console.log(b)}
})()

解释是:具名自执行函数的变量为只读属性,不可修改(不太确定)

  1. 简单改造下面的代码,使之分别打印 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); 
})();
  1. 下面代码输出什么

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 = 20a变量提升 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
})()
  1. 使用 sort() 对数组 [3, 15, 8, 29, 102, 22] 进行排序,输出结果

默认没有函数 是按照 UTF-16 排序的,对于字母数字 你可以利用 ASCII 进行记忆

[3, 15, 8, 29, 102, 22].sort() //[102,15,22,29,3,8]
  1. call 和 apply 的区别是什么,哪个性能更好一些

  • call分别接受参数
  • apply接受数组作为参数 经过测试call的性能比较好,可能是因为少了数组解构的过程
  1. 输出以下代码的执行结果并解释为什么

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 = undefinedb.x={n:2}
  1. 箭头函数与普通函数(function)的区别是什么?构造函数(function)可以使用 new 生成实例,那么箭头函数可以吗?为什么?

  • 箭头函数没有自己的this,它的this是定义时所在的上层作用域的this,所以this的指向是固定的
  • 箭头函数没有arguments对象
  • 箭头函数不可以使用yield命令,所以不能用做Generator函数
  • 箭头函数没有prototype属性
  • 箭头函数不可以使用new生成实例
    • 没有自己的this,所以无法在实例化时将自己的this指向实例对象`
    • 没有prototype属性,而new命令需要将构造函数的prototype赋值给新对象的__proto__属性)
  1. a.b.c.d 和 a['b']['c']['d'],哪个性能更高?

a.b.c.d更高,从两种形式的结构来看,显然编译器解析前者要比后者容易些,自然也就快一点。 下图是两者的 AST 对比:

image.png

  1. ES6 代码转成 ES5 代码的实现思路是什么

    1. 将代码字符串转成ast语法树
    2. 将ast语法树按照一定的规则转成对应es5的ast树
    3. 将es5的ast树转成对应的代码
  2. 为什么普通 for 循环的性能远远高于 forEach 的性能,请解释其中的原因

  • 本人测试的时候10万级别的时候forEach的速度远远比for更快,是for的10倍,应该是forEach的底层改过了
  • forEachfor多了很多执行上下文,参数声明等
  1. 数组里面有10万个数据,取第一个元素和第10万个元素的时间相差多少

    消耗时间差不多,差异可以忽略不计
  • js中的数组并不是使用连续的内存空间的(现在有些JavaScript引擎已经在为同种数据类型的数组分配连续的存储空间了),而是一种哈希映射关系,可以根据键名key直接计算出值存储的位置,所以查起来都是O(1)
  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'
  1. 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);
  1. var、let 和 const 区别的实现原理是什么

  • var 会直接在栈内存中分配空间,等到语句执行时(在语句执行前已经完成了声明和初始化),赋值对应的变量,如果是引用类型的话则会在堆内存中开辟一个内存空间存储实际内容,栈内则会存储一个指向堆内存的指针
  • let 不会在栈内存中预分配空间,而且在栈内存分配变量时(声明已经提升,但是没有初始化),做一个检查,如果已经有相同变量名存在就会报错(暂时性死区)
  • const 也不会预分配空间,行为和let一样,不过const 变量不可修改,对于基本类型来说,不能修改变量的值,对于应用类型来说,无法修改栈内存分配的指针,但是可以修改指针指向的对象的值
  1. 写出如下代码的打印结果

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",是给对象ositeUrl地址赋值,所以此时的对象为{ siteUrl: 'http://www.baidu.com' },o = new Object()时,把o指向了另一个对象,此时o.siteUrl = "http://www.google.com"是新对象的赋值,与传进函数的对象无关

  1. 请写出如下代码的打印结果

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}
  1. 分别写出如下代码的返回值

String('11') == new String('11');
String('11') === new String('11');

true String('11')返回的是string类型,new String('11')返回的是object类型,==会做隐式转换,两个操作数比较的时候,new String('11').toString()==='11' false 两边类型不一样

  1. 请写出如下代码的打印结果

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

  1. 为什么 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的次数不一样

  1. 输出以下代码执行结果

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多一点

  1. 输出以下代码执行结果,大致时间就好(不同于上题)

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()执行完毕之后继续运行