Bootstrap

前端Web面试题详解!绝对包懂!


面试题1 作用域  this指向 预解析

window.onload = function(){
  //考核点: 作用域  this指向 预解析
  var a =10;
  function test(){
    console.log(a);
    a=100;
    console.log(this.a);
    var a;
    console.log(a);
  }
  test();
}


//解析过程
//(1)预解析
var a;
function test(){};

//(2)逐行解析
a = 10;
test();

//(3)函数内部 预解析
var a;

//(4)逐行解析
console.log(a); //undefined
a=100;//重新赋值
console.log(this.a); //10
console.log(a); //100


//扩展 全局变量和局部变量同名的坑: 全局变量是不会作用于同名局部变量的作用域的
// (1)
// var a = 10;
// var a;
// console.log(a); //10

//(2)
var a = 10;
function fn(){
  console.log(a); //undefined
  var a;
};
fn()

//(3)
var a = 10;
function fn(){
  a = 10; //修改全局变量的值
  console.log(this.a); //10   this.a ==> window.a
};
fn()

//(4)
var a = 10;
function fn(){
  var a = 100; //全局变量和局部变量同名的坑
  console.log(this.a); //1   this.a ==> window.a
};
fn()

//(5)
var a = 10;
function test(){
  console.log(a); //10
  a=100;
  console.log(this.a); //100
  console.log(a); //100
}
test();

 例题:

//1️⃣例题1:考核点 全局变量
function f(){
    console .log( num );
    num = 200;
    console.log( num );
}
var num = 100;
f();
console.log(num);

// 解析过程:
//(1)预解析 var function(){ }
function f(){ //整个解析}
var num;

//(2)逐行解析
num = 100;
f();
//(3)函数执行
console.log( num );  //100
num = 200;  //修改全局变量的量
console.log( num ); //200

console.log( num );//200


//例如:
console.log(a);
var a = 100;
//过程如下:(1)预解析
var a;
//(2)逐行解析
console.log(a);
a = 100; 


//2️⃣例题2
function f(){
    console .log( num );
    var num = 200;
    console.log( num );
}
var num = 100;
f();
console.log(num);


//解析过程:(1)预解析
function f(){}
var num;

//(2)逐行解析
num: = 100;
f();

//(3)函数执行解析
//(4)预解析
var num;

//(5)逐行解析
console.log( num ); //num同名了,这里的是局部变量,因此是undefined
num = 200;
console.log( num ); //200
console.log( num ); //全局变量 100


//例题解析:
var a = 100;
var a;
console.log(a); //100

//规则: 全局变量和局部变量同名时,全局变量是不会作用于局部变量的作用域的
var a = 100;
function f(){
    var a;
    console.log(a) //undefined
};
f();


//3️⃣例题3:
function f (num){
    console.log( num );
    var num = 200;
    console.log( num );
}
var num = 100;
f( num );
console.log( num );

//解析过程:(1)预解析
function f(){}
var num;

//(2)逐行解析
num: = 100;
f(num);

var num = 100;
//(3)⭕如果有形参,先给形参赋值

//(4)函数执行解析
//(5)预解析
var num;

//(6)逐行解析
console.log( num ); //100
num = 200;
console.log( num ); //200
console.log( num ); //全局变量 100

//4️⃣例题4:  this,apply,arguments
var x = 1;
function f( y ) { //函数声明
    return this.x + y
}
var obj = {
    x: 2
}
var f2 = function() {  //函数表达式
    return f.apply( obj, arguments)  //arguments 是f2(3)传进来的参数3 作为一个集合
    // 且f在obj中执行,因此this指向了obj中的x: 2,且y又是3
}
var z = f2(3);
console.log(z); // 5

(1)call() 方法接收一个目标对象和一组参数,将函数内部的 this 指向目标对象,并传入参数列表。

function greeting() {
  console.log(`Hello, ${this.name}!`);
}

const person = { name: 'Alice' };
greeting.call(person); // 输出:Hello, Alice!

(2)apply() 方法与 call() 方法类似,也是将函数内部的 this 指向目标对象,但是它接收一个目标对象和一个参数数组,而不是一组单独的参数。

function greeting(greeting, punctuation) {
  console.log(`${greeting}, ${this.name}${punctuation}`);
}

const person = { name: 'Bob' };
greeting.apply(person, ['Hi', '!']); // 输出:Hi, Bob!

call() 和 apply() 方法的作用非常相似,只是传递参数的方式有所不同。通常情况下,我们可以根据具体的需求来选择使用它们中的任意一个。如果要传递的参数已经存在于一个数组或类似数组的对象中,那么使用 apply() 会更加方便;如果需要手动指定每一个参数,那么使用 call() 可能更为自然。

//apply() call()
function Person(name, age){
    this .name = name;
    this .age = age;
}

var p1 = new Person('p1',18);
var p2 = {}; //对象
//person对象在p2对象中执行,作用域发生改变

// Person.apply(p2,['p2',19]); //通过数组方式传递值
Person.call(p2,'p2',19);
console.log(p2)

//5️⃣例题5  异步任务中的微任务  宏任务
//JS是单线程的
setTimeout(()=>{ //异步任务 宏任务
    console.log(1)
},100)

setTimeout(()=>{  ///异步任务 宏任务
    console.log(2)
},0);
console.log(3)  //同步任务

let p = new Promise((resolve, reject) =>{
    resolve(4);
    console.log(5);  //同步任务
});
p.then(res => {
    console.log(res)  //异步任务 微任务
})
console.log(6) // 同步任务



//例如:
setTimeout(()=>{
    console.log(0) //异步任务 宏任务
}.100)

console.log(1); //同步任务
let p = new Promise((resolve, reject) => {
    //resolve(4);
    console.log(3); //同步任务
});
p.then(res=>{
    console.log(res)// 异步任务  微任务
})

console.log(2); //同步任务
//1 3 2 4 0


//6️⃣例题6: await 关键字
function f2(){
    console.log(1);
    setTimeout(()=>{
        console.log(2)
    },100)
}
async function f(){
    console.log(3)
    await f2();
    console.log(4)
}
f();
console.log(5);

Promise

Promise 是 JavaScript 中的一种异步编程方式,它用于管理异步操作的状态,可以更加优雅地处理回调函数嵌套的问题。一个 Promise 表示一个异步操作的最终完成或失败,并返回一个包含异步操作结果的值。(同步容易导致堵塞)

Promise 有以下三种状态:

  • pending:表示 Promise 实例初始化后的初始状态,既不是成功(fulfilled)也不是失败(rejected)状态。
  • fulfilled:表示 Promise 实例已经成功地完成了异步操作并返回了值。
  • rejected:表示 Promise 实例已经遇到了错误并返回了错误原因。

Promise 支持链式调用,可以把多个异步操作串联在一起依次执行,使用 .then() 方法和 .catch() 方法来处理 Promise 实例的状态变化。.then() 方法用于处理 Promise 实例从 pending 状态到 fulfilled 状态时的回调函数,.catch() 方法用于处理从 pending 状态到 rejected 状态时的回调函数。

//promise 解决JS中回调难以维护和控制  类似一个容器或对象
var id = 1;
$.ajax({
    url:'',
    data:{},
    success:function(res){
        //res
        id = res.id;
        $.ajax({ //回调地狱
            url:'',
            data:{},
            success:function(res){
                //res
                id = res .id;
            }
        });
    }
});
console.log(id)




var p = new Promise(function(resolve,reject){
resolve();//异步操作成功的结果
reject();//异步操作失败的结果
});
//promise实例生成后,可以通过then方法指定成功或失败状态 的回调函数
p.then(function(res){
    //res =>异步操作成功的结果
},function(err){
    //err =>异步操作失败的结果
});

 宏任务(macro task)和微任务(micro task)

宏任务指的是传给 JavaScript 引擎的任务队列中的任务,而微任务指的是微任务队列中的任务。

(1)常见的宏任务包括:

  • setTimeout、setInterval、setImmediate
  • I/O 操作、UI 渲染
  • 跨域通信、postMessage、MessageChannel

(2)常见的微任务包括:

  • Promise.then、catch、finally, async, await
  • Object.observe、MutationObserver
  • process.nextTick

在每次事件循环中,当当前宏任务执行完毕后,JavaScript 引擎会立即处理所有微任务。当所有微任务都被处理完毕后,才会进入下一个宏任务。在 JavaScript 中,微任务的优先级比宏任务高,所以微任务总是会在下一个宏任务前执行完毕。

 await 关键字

await 关键字用于暂停异步函数的执行,等待一个 Promise 对象 fulfilled(或者 rejected)后继续执行异步函数并返回 Promise 对象的值。可以将 await 视为一个让出线程的标志,它告诉 JavaScript 引擎在等待 Promise 返回结果时暂停执行该函数的代码,直到 Promise 完成或被拒绝。

使用 await 关键字可以简化异步代码的编写,避免了回调地狱和复杂的 Promise 链式调用。

this指向规则

运行一个函数时,有没有调用者? 没有调用者,默认指向全局window,严格模式下undefined 。有调用者,指向调用者

window.onload = function(){
  fn(); //无调用者
  obj.fn(); //有调用者
  
  //情况一:
  var length = 1
  function fn() {
    console.log( this.length ) //this.length ===> window.length
  }
  fn(); //无调用者  为1
  
  
  //情况二:
  var length = 1;
  function fn() {
    console.log( this.length ) //this.length ===> obj.length
  };
  var obj = {
    length: 100,
    f1:fn
  }
  //obj.f1(); //100 有调用者
  
  var x = obj.f1;
  x(); //1 无调用者 this.length ===> window.length


  //情况三:
  var length = 1;
  function fn(){
    console.log( this.length ) //this.length ===> arr.length
  };
  var lists = [fn,2,3,4,5,6];
  lists[0](); //有调用者,  调用者为arr  即输出6
  var f = list[0];
  f(); // 输出1
}

阿里面试题✍🏻

window.onload = function(){
  //阿里面试题  考核点: this指向 arguments ES6扩展运算符
  var length = 1;
  function fn(){
    console.log(this.length)
  }
  var obj ={
    length: 100,
    action: function(callback){
      callback();
      //callback() == > fn()  callback( )没有调用者,指向全局。拿到1
      arguments[0]();
      //arguments[0]==>fn
      //arguments[0]() ==>fn()  this.length==>arguments .length
    }
  }
  var arr =[1,2,3,4];
  obj.action(fn, ···arr);  // 1和5
}

arguments

用来访问传递给函数的参数列表。 arguments对象具有以下方法:

  • length:返回传递给函数的参数个数。
  • callee:返回正在执行的函数本身。
  • caller:返回调用当前函数的函数。

⭕箭头函数没有自己的 arguments 对象

剩余参数

在ES6中引入了剩余参数(rest parameters)语法,它允许我们将函数的多个参数表示为一个数组。与arguments对象不同,剩余参数是真正的数组,它可以使用数组方法,并且可以在函数定义时声明。

//写一个函数 findMax() 接受任意数量的数字参数,并返回其中最大的那个数。要求使用剩余参数语法。
function findMax(...numbers) {
  //使用 ...numbers 的语法来接收任意数量的数字参数
  let max = Number.NEGATIVE_INFINITY;
  for (let i = 0; i < numbers.length; i++) {
    if (numbers[i] > max) {
      max = numbers[i];
      //遍历输入数组,如果当前数字比 max 大,则将其替换为新的最大值
    }
  }
  return max;
}

console.log(findMax(1, 5, 2, 7, -3)); // 输出 7
console.log(findMax(100, 200, 150)); // 输出 200

在ES6之前,通常使用-Infinity代替Number.NEGATIVE_INFINITY(表示负无穷大)。从ECMAScript 2015 (ES6)开始,Number.MIN_SAFE_INTEGERNumber.MAX_SAFE_INTEGER也被添加到JavaScript中,分别表示安全的最小和最大整数。

函数 声明 / 表达式

在 JavaScript 中,函数可以通过函数声明或函数表达式的方式来定义。

(1)函数声明:使用 function 关键字声明一个函数,通常出现在代码的顶部,可以在函数声明之前调用它们。

function add(x, y) {
  return x + y;
}

(2) 函数表达式:将一个函数赋值给变量,函数表达式不会被提升(hoist),只能在赋值之后使用。

const add = function(x, y) {
  return x + y;
};

函数表达式还有两种变体:

2.1 命名函数表达式(Named Function Expression,NFE):函数表达式中指定一个函数名,可以在函数内部以及函数之后引用它自身。

const add = function sum(x, y) {
  return x + y;
};
console.log(add(1, 2)); // 输出:3
console.log(sum(1, 2)); // 报错:sum is not defined

2.2 箭头函数表达式:使用箭头符号 => 来定义一个函数,它是匿名函数,并且不能作为构造函数使用。

const add = (x, y) => x + y;

⭕通常情况下,函数表达式更加灵活,可以用来创建匿名函数或者作为函数参数传递,而函数声明更适合用于定义顶级函数(即不在其他函数内部定义的函数)。


面试题2  作用域

//面试题2:作用域的坑
var a = 10;
var obj = {
  a:99,
  f:test
};

function test(){
  console.log(a);  //undefined
  a=100;
  console.log(this.a); //99
  var a;
  console.log(a); //100
}
obj.f();


面试题3  作用域 预解析

/美团面试题
//考核点: 作用域 预解析
var a = 10;
function f1(){
  var b = 2 * a;
  a = 20;
  var c = a+1;
  console.log(b);
  console.log(c);
}
f1()


//解析过程
//(1)预解析
var a;
function f1(){} //整个函数,并没有调用

//(2)逐行解析 由上而下
a = 10;
f1();

//(3)f1函数 预解析  (全局变量与局部变量是否同名)
var b;
var a;
var c;

//(4) f1函数 逐行解析
b = 2*a;
a = 20;
c = a+1;
console.log(b);  //2*undefined = NaN
console.log(c);  //21



ES6+

ECMAScript 6.0 (以下简称 ES6) 是JavaScript 语言的下一代标准,已经在 2015 年 6月正式发布了它的目标,是使得JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。

ECMAScript与JS的关系

ECMAScript和Javacript 的关系是,前者是后者的规格,后者是前者的一种实现 (另外的ECMAScript方言还有JScript 和ActionScript) 。日常场合,这两个词是可以互换的。

Babel 转码器

Babel是一个广泛使用的 ES6 转码器,可以将 ES6 代码转为 ES5代码,从而在老版本的浏览器执行。这意味着,可以用 ES6 的方式编写程序,又不用担心现有环境是否支持。

//转码前
var $ = name => document .querySelector(name);

//转码后
var $ = function(name){
  return document .querySelector(name)
}

解构

  • ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构 (Destructuring)
  • 解构是ES6的新特性, 比ES5代码简洁,清晰,减少代码量

 (1)数组解构

window.onload = function(){
  var a = 1;
  var b = 2;
  var c = 3;
  var a = 1,b = 2,c = 3;
  //ES5中的为变量赋值,只能直接指定值
  
  //数组解构
  let [x,y,z] = [1,2,3];
  //这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值

  var [a,b,c] = [1,2];
  //a = 1; b = 2 ; c = undefined
  //即是ES5中的  var a = 1, b = 2,c;


    //ES5
    var id = 1;
    var arr = [1,2,3];
    var obj = (title:'obj'};
    var flag = true;


    //ES6 即为
    var [id,arr,obj,flag] = [1,[1,2,3],[title:'obj'},true];
    var[a,[b,d],c] = [1,[2,3],4];
    console.log(b,d,c); // 2 3 4
    //如果解构不成功,变量的值就等于undefined.    


    //设置默认值 —— 解构赋值允许指定默认值
    let arr = [88,99];
    var a = arr[0] || 1; //没有时默认为1
    var b = arr[1] || 1;

    //ES6 写法
    var [a=1,b=1] = arr;
    var [a=1,b=1] = [10,30];
    var [a=1,b=1] = [null,undefined]; //a = nul1, b = 1
    //undefined 时默认值是生效的 
    //只有当一个数组成员严格等于undefined,默认值才会生效
    console.log(a,b);


    //特殊
    function fn(){
        return 100
    };
    var [a=1,b=fn()] = [10,20];
    console.log(b); //20  惰性函数求值:默认值生效了才会执行函数
    //但是已经有匹配的值所以默认值的函数并不会被执行
}

这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值

(2)对象解构


对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。


对象解构的写法

//对象解构的写法:
let [a,b] = {b:1,a:2};

//原本写法
const obj = {
    title:'obj',
    age:18,
    id:1,
    result:[10,20,30]
};

var title = obj.title;
var id = obj.id;
var result = obj.result;
//对象解构处理  位置无所谓,而可以有别名
//即result:res  = 匹配模式 ===>变量名
var {id,title,result:res} = obj;
console.log(res);

别名 + 默认值
变量名与属性名不一致,name是匹配模式,n是变量

    var [name:n, age:a] = [name:'123', age:18];

    //解构赋值允许指定默认值
    var (id,title:t,result:res=[1,2]} = obj;


    //特殊的  将对象 {x:100} 的属性 x 的值赋给变量 x
    var x;
    ({x}= {x:100});

    在括号中使用了花括号的语法来表示一个对象字面量,而不是代码块。
    接着,在等号的左侧使用了对象解构的语法,指定了要从右侧对象中提取的属性 x。
    由于对象解构的语法需要将整个表达式放在圆括号中
    因此整个表达式需要用一对空圆括号进行包裹。

//根据解构赋值的规则,变量名与对象属性名相同的情况下才会发生赋值。
var {id=1,result:res=2} = {result:88,id:100,title:99};
console.log(id,res); //100  88


//将obj对象中的id和result数组的值累加
function sum({id=1,result:arr=[]}){
    return arr.reduce((total, cur)=>total+cur, id);
}
console.log(sum(obj)); //61


//默认值生效的条件是,对象的属性值严格等于 undefined
let {x:a = 10,y:b = 20} = {x: undefined};
//解构赋值表达式左侧的对象属性 a 和 b 都设置了默认值
//右侧对象包含 x 属性,但其值为 undefined,则 a 的值会被设置为 10,b 的值会被设置为 20

//⭕这里的默认值只针对对象属性值为 undefined 的情况
//‼️非严格相等(即 ==)的情况不会触发默认值
let {x:a = 10, y:b = 20} = {x: null};
// a 和 b 的值都不会被设置为默认值,而是分别为 null 和 20。

JS的reduce方法是用于对数组元素进行累计计算的高阶函数。它可以将数组中的每一个元素依次传入一个回调函数中,并在每次传入后将回调函数的返回值进行累计操作,最终得到一个单一的结果。

Reduce方法接收两个参数:一个回调函数和一个可选的初始值。回调函数有两个参数:累计值(也称为累加器)和当前元素值。在每次调用回调函数时,累计值都会被更新为上一次回调返回的值。

该方法的调用方式为:array.reduce(callback[, initialValue])。如果提供了初始值,则累加器的初始值为该值;否则,累加器的初始值为数组第一个元素。如果数组为空且未提供初始值,则会抛出错误。

使用reduce方法可以非常方便地实现对数组元素的求和、求积、字符串连接等操作。

相等运算符包括严格相等运算符(===)和非严格相等运算符(==)。

(1)严格相等运算符会比较两个值的类型和值是否完全相等。只有当两个操作数的类型和值都相同时,才会返回 true。‼️不同的对象即使具有相同的属性和值,它们也不是严格相等的,因为它们是不同的引用。

1 === 1 // true
"hello" === "hello" // true
null === null // true


let obj1 = {a: 1};
let obj2 = {a: 1};
obj1 === obj2 // false

 (2)非严格相等运算符通常可以进行类型强制转换,并将两个操作数转换为相同的类型,然后再进行比较。这种转换可能会导致一些意想不到的行为,因此通常建议使用严格相等运算符。

"1" == 1 // true (字符串 "1" 被转换为数字 1)
null == undefined // true (它们被认为相等)
"true" == true // true (字符串 "true" 被转换为布尔值 true)

‼️在使用非严格相等运算符时,如果一个操作数是 null 或 undefined,则会发生类型转换,而另一个操作数也必须是 null 或 undefined 才会返回 true。

null == null // true
undefined == undefined // true
null == undefined // true
0 == null // false (0 不是 null 或 undefined)
"" == null // false (空字符串不是 null 或 undefined)


惰性求值(Lazy Evaluation)

是一种编程技巧,它延迟计算表达式的值,直到真正需要时才进行计算。这种技巧可以优化程序的性能,并减少不必要的计算和内存占用。

在JavaScript中,可以使用惰性求值来避免重复计算某个值或执行某个函数。

1. 使用闭包缓存结果

使用闭包缓存结果是一种常见的惰性求值技巧。它通过创建一个闭包,将结果保存在闭包中,并在下一次调用时返回已缓存的结果,而不是重新计算。

例如,以下代码使用闭包缓存getScrollTop()函数的结果,在第一次调用后返回已缓存的结果,从而提高了性能:

var getScrollTop = (function() {
  var scrollTop; // 定义变量保存结果
  return function() {
    if(scrollTop !== undefined) { // 如果结果已经存在,则直接返回
      return scrollTop;
    }
    scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop; // 计算结果
    return scrollTop; // 返回结果
  }
})();

console.log(getScrollTop()); // 第一次调用,计算并返回结果
console.log(getScrollTop()); // 第二次调用,返回已缓存的结果

2. 使用惰性函数

惰性函数是一种只有在需要时才被执行的函数。它通常用于处理特定的浏览器或环境差异,并根据需要加载相关代码。

例如,以下代码使用惰性函数来检测浏览器是否支持addEventListener( )方法,并在需要时加载相应的代码:

var addEvent = function(elem, eventType, handler) {
  if(elem.addEventListener) { // 如果浏览器支持addEventListener()方法,则直接调用
    elem.addEventListener(eventType, handler, false);
  } else if(elem.attachEvent) { // 如果浏览器支持attachEvent()方法,则添加适当的前缀并调用
    elem.attachEvent('on' + eventType, handler);
  } else { // 如果浏览器都不支持,则返回null
    elem['on' + eventType] = handler;
  }
};

var addEventIE = function(elem, eventType, handler) {
  elem.attachEvent('on' + eventType, handler);
};

var addEventW3C = function(elem, eventType, handler) {
  elem.addEventListener(eventType, handler, false);
};

var addEventLazy = function(elem, eventType, handler) {
  if(window.addEventListener) { // 如果浏览器支持addEventListener()方法,则直接调用
    addEventLazy = addEventW3C;
  } else if(window.attachEvent) { // 如果浏览器支持attachEvent()方法,则添加适当的前缀并调用
    addEventLazy = addEventIE;
  } else { // 如果浏览器都不支持,则返回null
    addEventLazy = null;
  }
  addEventLazy(elem, eventType, handler); // 调用相应的函数
};

addEventLazy(document.body, 'click', function(){ console.log('click'); }); // 第一次调用,检测浏览器支持情况并执行相应的函数
addEventLazy(document.body, 'mouseover', function(){ console.log('mouseover'); }); // 第二次调用,直接执行已缓存的函数

set

ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值

set 本身是一个构造函数,用来生成 Set 数据结构

var arr = [1,2,3];
const s1 = new Set();
//s1.add(1);
//s1.add(2);
//s1.add(3);
//s1.add(1) .add(2) .add( 3 ) .add(2) .add(1) ;

s1.add(1) .add(2) .add(3 ) .add(2) .add( '1' );
console.log(s1);//set(3) { 1, 2,3 }
//set(3) { 1, 2, 3,'1' };


//Set 构造函数会根据这个可迭代对象中的每个元素创建一个新的 Set 对象

let mySet = new Set([1, 2, 3]);

{
  let mySet = new Set(['a', 'b', 'c']);
  console.log(mySet); // 输出 Set(3) {"a", "b", "c"}
}

console.log(mySet); // 输出 Set(3) {1, 2, 3}
//在大括号内部使用 let 关键字定义的变量 mySet 只在该代码块内有效,不影响外部作用域中的同名变量。


//⭕只有在需要在代码块中定义临时变量、常量或函数等时才需要使用大括号,否则可以直接省略。
let mySet = new Set();
mySet.add(1).add(2).add(3);
// 等同于
let mySet = new Set([1, 2, 3]);


//简洁定义
const s2 = new Set([1, 1, 2, 3, 2,1,2,3,2]);
//数组进行去重
var arr = [1,2,3,2,1,2,3,2,1];
const s3 = new Set(arr);
console.log(s3); // set(3){ 1,2,3 }


//类型的转换
//1、Array.from();
console.log(Array.from(s3));//[ 1, 2, 3 ]

//2、...扩展运算符
console.log([...s3]); //[ 1, 2,3 ]

练习题✍🏻

var items =[10, 34,26,64, 24,34, 26,44, 67,24,67,44,78,26,34, 67, 78, 34];
//求出大于50的数据且去重的处理
//分析
//1、求出大于50
//let filterNum = items .filter(v=>v>50);

//2、去重
//let  s5= new Set(filterNum);

//3、类型转换
// let a = [...s5];

//4、代码合并
//求出大于50的数据且去重的处理
console.log([...new Set(items .filter(v=>v>50))]);
//[ 64,67, 78 ]

map


定义

  • JavaScript 的对象 (Object) ,本质上是键值对的集合,但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。
  • ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是”键"”的范围不限于字符串,各种类型的值 (包括对象)都可以当作键。
//map的数据结构
const m = new Map() ;
//链式写法
m. set('name' , 'value ') . set('age' ,18) . set('a' , 'a') ;

console.log(m);
// Map(3) { 0: {"name" => "value "} 1: {"age" => 18} 2: {"a" => "a"} }



//这里的set 表示map的添加方法
var a = {id:2};
m. set(a, 'value') ;


const m1 = new Map();
m1.set('title', 'hello') .set( 'id',1);
//console.log(m1); 
//Map(2){ 'title' => 'hello', 'id' => 1}

const msg = {name: 'tom'};
m1. set(msg,'abc');
console.log(m1); 
//Map(3) { 'title' => 'hello','id'=> 1, { name:'tom' } =>'abc' }


//简洁写法
const m2 = new Map([['a',1],['b',2],['c',3]]);


//⭕获得键值对对比

//方式一:
var obj = {
    a:1,
    b:2,
    c:3
};
//求出obj对象的key和value
//0bject.keys(obj);
for(var k in obj){
    console.log(k)//abc
    console.log(obj[k]); //123
}

//方式二:
for(let [k,v] of m2){  // [k,v] 相当于解构赋值 
    console.log(k); //abc
    console.log(v); //123
}

常见面试题✍🏻

//常见面试题
const m3 = newMap([['a',l],['b',2],['c', 3]]);

//1、map结构转为object
var o = { };
for(let [k,v] of m3){
    o[k] = v;
}
console.log(o) //{ a: 1, b: 2,c: 3}

//2、object转为map结构
var obj = {
    a:1,
    b:2,
    c:3
}

const m4 = new Map();
for(var k in obj){
    m4.set(k,obj[k])
}
//  ⭕ const m4 =new Map(Object.entries(obj));
console.log(m4);  //Map(3) { "a"=> 1,"b"=> 2,"c"=> 3 }




const lists = [{count:10,name: 'a'},{count:45,name: 'b'},{count:27,name: 'c'},{count:5,name: 'd'}]
//3、过滤掉count为3和3的倍数,进行去重处理

const lists = [{count:10,name: 'a'},{count:45,name: 'b'},{count:27,name: 'c'},{count:5,name: 'd'}];

const filteredLists = lists
  .filter(item => item.count % 3 !== 0)
  .map(item => ({name: item.name, count: item.count}))
const uniqueLists = Array.from(new Set(filteredLists.map(item => JSON.stringify(item)))).map(item => JSON.parse(item));

console.log(uniqueLists);
// 输出结果: [{count: 10, name: 'a'}, {count: 45, name: 'b'}, {count: 5, name: 'd'}]
//JSON.stringify() 将每个对象转换成字符串形式,以便于 Set 进行去重

//(1)使用 Array.filter() 方法过滤出 count 不为3及其倍数的元素。
//(2)使用 Array.map() 方法将剩余的元素转换成 {name, count} 格式的新对象。
//(3)使用 Set 数据结构去重后的新对象数组。
//(4)将去重后的结果转回到普通数组形式,并返回它。

Object.entries( ) 用于将一个对象转换为键值对数组。这个方法返回一个数组,其中包含了指定对象中所有可枚举属性的键值对(即 [key, value] 数组)。

//对一个对象进行遍历和操作
const object1 = { a: 'somestring', b: 42 };
for (const [key, value] of Object.entries(object1)) {
  console.log(`${key}: ${value}`);
}
// 输出结果: 'a: somestring' 和 'b: 42'

使用 for...of 循环遍历 object1 对象的键值对数组,将每个键值对保存到 [key, value] 数组中。在循环的每一次迭代中,打印出键和值的信息。


作用域

作用域是指程序源代码中定义的范围

作用域规定了如何设置变量,也就是确定当前执行代码对变量的访问权限

JavaScript采用词法作用域,也就是静态作用域

  • 所谓词法作用域就是在函数定义的时候 就已经确定了
var value = 1;
function foo() {
  console.1og(value);  //1
}
function bar() {
  var value = 2;
  foo();
}

bar();   // 1

①变量对象

变量对象是当前代码段中,所有的变量(变量函数形参 arguments )组成的一个对象

变量对象是在执行上下文中被激活的,只有变量对象被激活了,在这段代码中才能使用所有的变量

变量对象分为全局变量对象局部变量对象

全局简称为 Variable Object VO; 函数由于执行才被激活称为 Active object AO(以上面代码为例)

②作用域链

在JS中, 函数存在一个隐式属性[[scopes]], 这个属性用来保存当前函数的执行上下文环境,由于在数据结构上是链式的,因此也被称作是作用域链, 我们可以把它理解为一个数组

可以理解为是一系列的AO对象所组成的一个链式结构

function a(){ }
console.dir(a) // 打印结构

  • 当函数被调用后
function a(){
  console.dir(a)
}
a( )

因此可以得出一个结论: [[scopes]] 属性在函数声明时产生在函数调用时更新即: 在函数被调用时,将该函数的AO对象压入到[[scopes]]中

作用域链的作用

作用域链的作用是保证执行环境里有权访问的变量和函数是有序的,作用域链的变量只能向上访问,变量访问到window对象即被终止,作用域链向下访问变量是不被允许的

最直观的表现就是:

  • 函数内部可以访问到函数外部声明的变量
var a = 100
function fn(){
  console.1og(a)
}
fn() // 100
  • 函数外部访问不到函数内部的变量
function fn(){
  var a = 100
}
fn()
console.1og(a) // a is not defined

✍🏻画出以下代码执行的作用域链

var global
function a(){
  //AO
  var aa = 123
  function b() {
    //AO
    var bb = 234
  }
  b()
}
a()

①a函数生成

 ②a函数被调用

 b函数生成 (a函数调用导致b函数生成)

 ④b函数调用

 函数执行完就销毁所谓销毁就是断掉图中的线。

思考 a函数调用和b函数生成是不是同一个作用域链?

  var global
  function a() {
  // 到这个里面来了
    var aa = 123
    // b函数定义
    function b() {
      // 到这里面来了
      var bb = 234
      aa = 0
    }
    // b执行
    b()
    console.1og(aa);
  }
  // a执行
  a()
  // 打印aa的结果 如果是00 说明是同一个 没有分离

③闭包

定义:

  1. 能够读取其他函数内部变量的函数
  2. 能够实现 函数外部 访问到函数内部的变量
//例如:
var global
function a() {
  // 到这个里面来了
  var aa = 123
  // b函数定义
  function b() {
    conso1e.1og(aa);
  }
  return b
}
// a执行
var res = a()
res()
window.onload = function(){
//闭包是能够读取其它函数内部变量的函数
//是“定义在一个函数内部的函数”,将函数内部和函数外部连接起来
  var a = 10;
  function f1(){
    var a = 109;
    function f2()[
      console.log(a)
    };
    f2();
  };
  f1();
  
  function f1(){
    var a = 100;
    return function f2()[
      console.log(a)
    };
  };
  f1()();
  
  //目的就是为了获取局部变量a
  function f1(){
    var a = 100;
    return a;
  };
  f1();
  
  //(1)
  var a = 10; //全局变量 存在window对象下
  function f1(){
    a++;
    console.log(a)
  }
  f1(); //11
  f1(); //12
  
  //(2)
  function f1(){
    var a = 10; //局部变量
    a++;
    console.log(a)
  };
  f1(); //11
  f1(); //11
  
  //🌟总结:局部变量无法共享和长久的保存,而全局变量可能造成变量污染,
  //闭包:既可以长久的保存变量又不会造成全局污染。
  
  //闭包的写法
  function f1(){
    var a = 10; //局部变量
    function f2(){
      a++;
      console.log(a)
    };
    return f2
  };
  var f = f1();  // f1()执行的结果就是闭包
  //返回的是一个函数,并且这个函数对局部变量存在引用,形成了闭包的包含关系
  f(); //11
  f(); //12
    
  function f1(){//局部变量
    var a = 10;
    return function(){
      a++;
      console.log(a)
    };
  };
  
  var f = f1();
  f(); //11
  f(); //12
  
  var x = f1();
  x(); //11
  x(); //12
  x(); //13

}

闭包的实战应用

(1)回调函数

function add(num1, num2, ca1back) {
  var sum = numl + num2;
  if (typeof cal1back ==='function') {//类型判断,确认是一个函数
    ca11back(sum);
  }
}
add(1,2,function (sum) {
  console.log(sum);
})
// 是一个闭包吗?
// 含义: 能够读取其他函数内部变量的函数。

手写实现 bind

// getName 的 this指向foo对象
let foo = {
  name:'ji11'
}

function getName () {
  consoTe.1og(this.name)
}

Function.prototype.myBind = function (obj) {
  // 将当前函数的this 指向目标对象
  let _self = this
  return function () {
    return _self.ca11(obj)
  }
}
let getFooName = getName.myBind(foo)
getFooName() // ji11

(2)防抖函数

 防抖函数可以让某个函数在一定时间内多次触发时,只执行最后一次触发的函数。当调用 debounce 返回的函数时,如果上一次调用还没有到达时间间隔,就会清除上一次的计时器,然后重新开始计时,直到时间间隔到了才会执行传入的函数。

/**
 * 防抖函数
 * @param {Function} fn - 要执行的函数
 * @param {Number} delay - 时间间隔
 */
function debounce(fn, delay) {
  let timer;
  return function(...args) {
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

当持续触发事件,一定时间内没有再触发事件,事件处理函数才会执行一次如果设定的时间到来之前,又一次触发了事件,就重新开始延时

触发事件,一段时间内,没有触事件执行,肯定是定时器

(在设定的时间内 又一次触发了事件 重新开始延时 代表的就是重新开始定时器)

(那么意味着上一次还没有结束的定时器要清除掉 重新开始)

在JavaScript中,debounce是一种常用的技术,用于限制某个函数反复触发的频率。这种技术通常使用一个计时器和一个中间函数来实现。当需要限制函数的调用频率时,可以将该函数传递给中间函数,并指定一个时间间隔(例如500毫秒)。当用户触发事件时,计时器会启动并开始倒计时。如果在指定的时间内再次触发事件,则计时器将重新开始计时。只有在计时器完全结束后才会调用传入的原始函数。这样可以确保函数仅在用户完成操作或停止输入一段时间后才被调用,从而提高性能和用户体验。

应用场景:

ar input = document.getElementById('input')
// 防抖的函数
function debounce(delay,callback) [
  let timer
  return function(value){
    clearTimeout(timer)
    //我们想清除的是setTimeout 我们应该存储这个timer的变量
    //timer变量需要一直保存在内存当中
    //不想立即打印之前已经输入的结果 清除以前触发的定时器
    //存储这个timer的变量一直要在内存当中——>内存的泄露 <——闭包
    //闭包:函数里面return出函数
    timer = setTimeout(function () {
      //console.log(value) 不在函数内输出,而是在外面输出
      callback(value)
    },delay)
  }
}

function fn(value){
  cnsole.log(value)
}
var debounceFunc = debounce(1000,fn)
//首先 输入框的结果只出现一次 是在我键盘抬起不再输入后的1秒后输出
input.addEventListener('keyup', function (e){
  🌟debounceFunc(e.target.value)
})

debounceFunc 通常是指一个函数防抖的实现。所谓函数防抖(Debouncing)就是在函数被频繁调用时,只有等到某个连续时间段内没有新的调用发生时,才真正去执行该函数。这样可以避免函数被频繁调用导致性能问题或者其他不必要的行为。

实际的应用

改变浏览器宽度的时候,希望重新渲染使用Echarts时,Echarts的图像,可以使用此函数,提升性能。 (虽然Echarts里有自带的resize函数)

典型的案例就是输入搜索: 输入结束后n秒才进行搜索请求,n秒内又输入的内容,就重新计时解决搜索的bug

(3)节流函数

节流函数可以让某个函数在一定时间间隔内只执行一次。当调用 throttle 返回的函数时,如果前一次调用距离这一次调用的时间还没有到达时间间隔,就不会执行传入的函数。只有等到时间间隔到了,才会执行传入的函数。

/**
 * 节流函数
 * @param {Function} fn - 要执行的函数
 * @param {Number} delay - 时间间隔
 */
function throttle(fn, delay) {
  let timer;
  return function(...args) {
    if (!timer) {
      timer = setTimeout(() => {
        fn.apply(this, args);
        timer = null;
      }, delay);
    }
  };
}

即 当持续触发事件的时候 保证一段时间内 只调用一次事件处理函数段时间内 只做一件事情。

(1)什么是 JS 节流函数?它有什么作用?

JS 节流函数是一种在特定时间间隔内执行某个函数的技术,可以有效地控制函数的执行速率并避免过度消耗计算资源。节流函数将某个函数的执行按照固定时间间隔进行划分,每隔一段时间便执行一次,从而避免了频繁触发的消耗和重复计算。常见的应用场景包括用户输入、页面滚动、鼠标移动等。 

 (2)请实现一个 JS 节流函数。

function throttling(func, delay) {
  let lastTime = 0;
  
  return function(...args) {
    const nowTime = new Date().getTime();
    
    if (nowTime - lastTime > delay) {
      func.apply(this, args);
      lastTime = nowTime;
    }
  }
}

(3)请说明节流实现原理,并与防抖进行比较。

JS 节流的实现原理是每隔一定时间后执行一次函数。其原理是通过记录上一次函数执行的时间戳和当前时间戳之差来判断是否需要执行函数。当两者之差大于设定的时间间隔时,执行函数并更新时间戳。如果两者之差小于设定时间间隔,则不执行函数,等待下一定时器时间到来再次尝试执行函数。

在功能上,节流和防抖都可以用于控制某个函数的执行,从而避免性能问题和其他影响。但是,它们的实现方式略有不同。防抖是在连续的多次触发过程中,只有最后一次触发的函数最终被执行;而节流是在连续的多次触发过程中,每隔一段时间执行一次函数,而未到达时间间隔的其他触发则不会执行。

(4)单例模式

保证一个类只有一个实例,并提供一个访问它的全局访问点。

/**
 * 单例模式
 */
const Singleton = (function() {
  let instance;
  function Singleton() {
    if (!instance) {
      instance = this;
    }
    return instance;
  }
  return Singleton;
})();
//每次创建新对象时,会先检查是否已经存在实例,如果已经存在就返回实例,
//否则就创建一个新实例并返回。这样可以确保该类在整个应用中只有一个实例。

(1)什么是 JavaScript 的单例模式?它有什么作用?

JavaScript 的单例模式是一种只允许创建唯一一个实例对象的设计模式,可以确保整个系统中只有一个特定对象,并且提供了全局访问点以便于在整个系统中进行操作。单例模式主要用于管理资源、控制命名空间、全局配置和状态等。

(2)请实现一个 JavaScript 单例模式。

const Singleton = (function () {
  let instance;
  
  function createInstance() {
    const object = new Object({name: 'Singleton Object'});
    return object;
  }
  
  return {
    getInstance: function () {
      if (!instance) {
        instance = createInstance();
        //如果 instance 不存在,则调用 createInstance() 方法创建一个新的实例对象,并将其赋值给 instance 变量。
        //如果 instance 已经存在,则直接返回该对象。
      }
      return instance;
      //可以确保只有一个实例对象被创建和使用
    }
  };
})();

// 调用示例
const singletonObj1 = Singleton.getInstance();
const singletonObj2 = Singleton.getInstance();

console.log(singletonObj1 === singletonObj2); // 输出结果为 true

 (3)单例模式可以应用于哪些场景?

单例模式可以应用于需要全局访问和控制的场景,例如浏览器中的常用组件、数据库连接池、全局配置等。同时,单例模式还可以用于保护全局变量和方法,避免相互干扰和污染命名空间,提高代码的可维护性、可读性和可扩展性。

✍🏻笔试题

// 请补全下面的 JavaScript 代码中的 add 方法
function add(n) {
  /*TODO*/
}
/*补全 add 函数,输出对应结果 */
add(1)(2)();/*=>3 */
add(1)(2)(3)(4)(); /* =>10 */
add(1)(1)(1)(1)(1)(1)(1)(1)(1)(1)(); /* =>10 */


//答案:
function add(n){
  if (!n) { return res } // 要是整个结果 6 10
  res = n
  return function (n) {
  // 这一次传的参数 应该是 上面几次的和
    return add(res + n)
   }
}
console.1og(add(1)(2)(3)()); // 6
console.1og(add(1)(2) (3)(4)()); // 10


//定时器传参数 如下:
function fn(a) {
  return function ({
    console.1og(a)
  }
}
  
setTimeout(fn(123),1000)

(5)私有变量

var getter,setter
(function(){
  var privateA = 0
  getter = function(){
    return privateA
  }
  setter = function(){
    if(typeof newVal !== 'number'){
      throw new Error('不是number类型')
    }
    privateA = newVa1
  }
})();
//privateA 变量和 getter 函数、setter 函数都被定义在同一个闭包内
//因此它们之间的作用域是相同的,并且可以互相访问彼此的变量和函数

利用闭包判断数据类型

function isType(type) [
  return function (target) [
      return  '[object ${type}]'=== object.prototype.toString.ca11(target)
  }
}
const isArray = isType('Array')
console.log(isArray([1,2,3]));// true
console.1og(isArray({}));

无限级目录树多种实现


var data = [
    {   
        name:'手机 / 运营商 / 数码',
        child:[
            {
                name:'热门分类',
                child:[
                    {name:'手机卡'},
                    {name:'宽带'},
                    {name:'5G套餐'}
                ] 
            },

            {
                name:'品牌墙',
                child:[ 
                    {
                        name:'中国移动',
                        child:[
                            {name:'北京移动'},
                            {name:'广东移动'},
                            {name:'湖北移动'}
                        ]
                    }
                ]
            }
        ]
    }
]


问答题📜

padding与margin有什么不同?

  • padding:指的是元素内部内容和边框之间的距离,常用于扩大元素的点击区域、增加元素的内部空白等。
  • margin:指的是元素与周围元素之间的距离,常用于调整元素之间的间距、居中元素等。

⭕padding 是控制元素内部与边框的距离,而margin是控制元素与周围元素的距离。

  • 浮动元素的宽度和高度会受到其内部内容和padding的影响,可能会导致布局错乱。此时可以使用清除浮动、强制设置宽度、使用伪元素等方法来解决问题。

vw与百分比

  1. 相对参照物不同:百分比是相对于父元素的宽度进行计算的,而vw是相对于视口宽度(Viewport Width)进行计算的。
  2. 计算方式不同:使用百分比时,元素的宽度是相对于父元素的宽度计算的,即width: 50%表示元素宽度为父元素宽度的50%(即 %有继承关系)。而使用vw时,元素的宽度是相对于视口宽度的大小计算的,即width: 50vw表示元素宽度为视口宽度的50%。(vw只和设备的宽度有关系)
  3. 兼容性不同:vw是CSS3新增的长度单位,目前还不被一些旧版本的浏览器支持,而百分比则是CSS中老旧的单位,被广泛应用于各种情况下。

行内元素和块级元素 区别:

  1. 盒模型不同:块级元素具有盒模型(Box Model),即每个块级元素都有自己的外边距、边框、内边距和内容区域;而行内元素没有盒模型,它们的尺寸是由内容决定的。
  2. 元素特性不同:块级元素通常用于结构布局,例如<div>、<p>、<h1>等标签,它们独占一行或多行,并且可以设置宽度、高度、内边距、外边距等属性。行内元素则通常用于表示文本或其他行内内容,例如<a>、<span>、<strong>等标签,它们不会独占一行,并且不能直接设置宽度、高度等属性,但可以设置padding和margin来调整间距。
  3. 布局方式不同:块级元素默认情况下从上到下排列,每个元素都独占一行;而行内元素默认情况下从左到右排列,不会独占一行,如果空间不够,会被压缩或换行。

HTML5中新增了一些元素,例如<section>、<article>、<nav>等,它们可以看作是特殊的块级元素,具有一些额外的语义和默认样式。此外,CSS中也可以通过设置display属性来改变元素的显示方式,将一个块级元素转变为行内元素。

display: inline-block:如果需要保留元素的块级特性(盒模型特性),同时又要将其转换为行内元素。

让谷歌浏览器支持小字体

transform: scale(0.5);
-webkit-transform: scale(0.5)

 JS坑

//一、声明提升

console.log(name) // undefined
var name ="凤敏同学";

虽然 name 使用了 var 关键字声明,但直到下一行才被赋值。因此当执行 console.log(name) 时,name 尚未定义。

var 和 let 的区别:

  1. 作用域

使用 var 声明的变量存在函数级作用域(Function scope),而使用 let、const 声明的变量存在块级作用域(Block scope)。

  • 函数级作用域:变量在整个函数内都可用,包括函数内的任何代码块。
  • 块级作用域:变量仅在当前代码块(如循环或花括号)内部可用。
    • 函数作用域更适合在整个函数内共享变量,而块级作用域则更适合在代码块内部限制变量的作用范围。同时,在 ES6 中,引入了 let 和 const 关键字来实现块级作用域,它们可以代替传统的 var 关键字来声明变量。

        2.变量提升

使用 var 声明的变量会被提升到函数作用域的顶部,并且可以在定义之前访问它们。这种现象被称为“变量提升”。

而使用 let 声明的变量不会发生变量提升。如果您尝试在定义 let 变量之前访问它,则会抛出一个 ReferenceError 异常。

console.log(x); // 输出 undefined  var 声明的 x 可以在定义之前访问
var x = 10;

//let 和 const 声明的变量不能在声明之前使用,这种现象称为“暂时性死区”
//而使用 var 声明的变量则可以在声明之前使用,但是值为 undefined

console.log(y); // 抛出 ReferenceError  它已经被定义了但未赋值
let y = 20;
//二、没有局部作用域  i 只在 for 循环的块级作用域中定义

function fn2( ){
  for(var i = 0; i < 5; i++){
  
  }
  console.log(i)
}
fn2()

let 和 const 都是用来声明变量的关键字,但它们之间有以下区别:

  1. 声明后是否可以重新赋值:使用 let 声明的变量可以在后续代码中重新赋值,而使用 const 声明的变量则不能被重新赋值。如果尝试对一个 const 声明的变量重新赋值,JavaScript 会抛出一个错误。
  2. 是否需要立即初始化:使用 let 声明的变量可以不需要立即初始化,而使用 const 声明的变量必须在声明时就进行初始化,否则 JavaScript 会抛出一个错误。
  3. 作用域范围:使用 let 和 const 声明的变量都是块级作用域,也就是只有在声明变量的大括号 { } 内部才能访问到该变量。

因此,建议在声明变量时遵循以下原则:

  • 如果变量的值不需要改变,则使用 const。
  • 如果变量的值需要改变,则使用 let。
  • 在声明之前确定变量的值,并且该值不需要改变,则使用 const。
  • 在声明之前无法确定变量的值,或者该值需要改变,则使用 let。
//三、声明覆盖 
let name2 ="大郎";
let name2 ="西门庆";
console.log(name2)

;