JavaScript-面试题

算法

递归求和

const recursiveSum=function(arr,index){
	return index < 0 ? 0 : arr[index] + recursiveSum(arr,index - 1);
}

技巧

数组

数组求交集:filter() + includes()

const intersection = (arr1, arr2) => { arr1.filter(value => arr2.includes(value)) }

数组求和:reduce()

const sum=array.reduce((preNum,currentNum)=>preNum+currentNum,0)

清空数组

arr=[];
arr.splice(0);
arr.length=0;

ES6

const obj={
    name:'zhangsan',
    sayHi1:()=>{
        console.log(this.name);
    },
    sayHi2(){
        (()=>{
            console.log(this.name);
        })()
    }
}

obj.sayHi1(); // 指向全局对象,浏览器中则显示空字符串,node中则显示undefined
obj.sayHi2(); // zhangsan

代码题

考查【在for循环里0被看作false】

let count = 0;
const nums = [0, 1, 2, 3];
nums.forEach(num => {
    if (num) count += 1;
});
console.log(count); // 输出 3,因为只有 1, 2, 3 是真值

考查【参数传递】、【数据类型】

function fn(obj, val) {
    // 在函数内部,直接修改传入的对象obj的name属性
    obj.name = 1;
    // val是一个基本类型值,这里只是复制了一份值,并不会影响外部的num变量
    val = 1;
}

var obj = { name: 0 }; // 创建一个对象,name属性初始值为0
var num = 0; // 创建一个基本类型数值,初始值为0
fn(obj, num); // 调用fn函数,将obj和num作为参数传递进去
console.log(obj, num); // 输出修改后的obj对象和原始的num值, { name: 1 } 0

考查【对象引用】

var a = { num: 1, count: 2 }; // 创建一个对象a,它有两个属性:num和count
b = a; // 将对象a的引用赋给变量b,此时a和b都指向同一个对象
b = { num: 3, count: 4 }; // 开辟了新对象,b指向新对象,与a不再指向同一个对象
console.log(a.num, a.count, b.num, b.count); // 输出结果为:1 2 3 4

var c = { num: 1, count: 2 }; // 创建一个对象c,它有两个属性:num和count
d = c; // 将对象c的引用赋给变量d,此时c和d都指向同一个对象
d.num = 3; // 修改了同一个对象的属性,将对象c的num属性改为3
d.count = 4; // 修改了同一个对象的属性,将对象c的count属性改为4
console.log(c.num, c.count, d.num, d.count); // 输出结果为:3 4 3 4

考查【对象引用复制】

let person = { name: "Lydia" };
const members = [person];
person = null;
console.log(members); // [{ name: "Lydia" }],因为 person 是一个对象,将引用复制给了 members。当将 person 设为 null 时,members 仍然引用这个对象

考查【JSON】

const obj = {
    a: 3,
    b: 4,
    c: null,
    d: undefined,
    get e() {
        return 5; // 提供一个返回值
    }
};

console.log(JSON.stringify(obj));
// {"a":3,"b":4,"c":null,"e":5}

考查【作用域】、【变量提升】

var a = 0;

function demo1() {
    console.log(a); // 0,打印全局作用域中的a,由于非严格模式下this指向全局对象
    console.log(this.a); // 0 (如果是严格模式下,this为undefined,会执行到这里报错Uncaught TypeError: Cannot read properties of undefined (reading 'a'))
    a = 1; // 修改全局作用域中的a
    console.log(a); // 1
    console.log(this.a); // 1
}

function demo2() {
    console.log(a); // undefined,因为var a=2的声明提升
    console.log(this.a); // 1    
    var a = 2;
    console.log(a); // 2
    console.log(this.a); // 1
}

demo1();
demo2();

考查【调用链】

// 创建一个调用链,使用多次 call 方法将 console.log 函数的上下文设置为全局对象
const demo = console.log.call.call.call.call.call.call.apply(
    (a) => a, // 一个简单的箭头函数,接受一个参数并返回它
    [1, 2] // apply 方法将这个数组作为参数传递给箭头函数,但箭头函数并未使用这些参数
);

console.log(demo); // 2

考查【this指向】

let length = 1;
function fn() {
    console.log(this.length);
}
let arr = [fun, 'a', 'b'];
arr[0](); // 3 (this指向arr)
let fun2 = arr[0];
fun2(); // 1 (this指向window)
let name='hello world'
let obj={
    name:'zhangSan',
    say:function(){
        console.log(this.name)
    }
}
obj.say() // zhangSan
setTimeout(obj.say,1000) // hello world(obj.say 是一个单独的函数调用,它的 this 指向会被设置为全局对象(在浏览器环境下通常是 window))
setTimeout(() => obj.say(), 1000); // 这样会输出 zhangSan
setTimeout(obj.say.bind(obj), 1000); // 这样也会输出 zhangSan

考查【this指向】、【箭头函数】

const shape = {
    radius: 10,
    diameter() { return this.radius * 2 },
    perimeter: () => 2 * Math.PI * this.radius
}
shape.diameter(); // 20
shape.perimeter(); // NaN,因为 this 指向的是全局对象,而全局对象没有 radius 属性

考查【属性名的类型】

const arr = [1, 2];
arr[0]++;
arr["1"]++;
console.log(arr[0], arr["1"]); // 2 3
// arr:{"0":1,"a":2,length:2}

考查【类型转换】、【操作符优先级】

console.log(3>2>1); // false (3>2)=>true=>0
console.log(3<2<1); // true (3<2)=>false=>0

考查【闭包】

在这里插入图片描述
在这里插入图片描述

考查【事件循环】、【作用域】、【异步】、【闭包】

for (var i = 0; i < 3; i++) {
    console.log(i);
    setTimeout(() => console.log(i), i * 1000);
}
// 直接输出 0 1 2 ,然后在 0 1 2 秒后输出 3 3 3
// 因为 setTimeout 是异步的,所以在执行 setTimeout 的时候,for 循环已经执行完毕,此时 i 的值为 3

考查【异步函数】

在这里插入图片描述

通用版数组分类方法

const people = [
    { name: 'Alice', age: 30, sex: 'female' },
    { name: 'Bob', age: 25, sex: 'male' },
    { name: 'Char', age: 30, sex: 'male' },
    { name: 'Diana', age: 25, sex: 'female' },
    { name: 'Eva', age: 25, sex: ' female' },
    { name: 'Frank', age: 25, sex: 'male' },
    { name: 'Grace', age: 20, sex: ' female' }
]

function groupBy(arr, generateKey) {
    if (typeof generateKey === 'string') {
        const propName = generateKey;
        generateKey = (item) => item[propName]
    }
    const result = {};
    for (const item of arr) {
        const key = generateKey(item)
        if (!result[key]) {
            result[key] = [];
        }
        result[key].push(item);
    }
    return result
}

console.debug(groupBy(people, "age"));
console.debug(groupBy(people, (item) => `${item.age}-${item.sex}`));

动态执行JS

变量作用域同/异步
eval()当前同步
setTimeout()全局异步
Function全局同步
script全局同步
let a = 1;
function exec(code) {
    let a =ieCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
    eval(code) // a:2 sync (当前作用域 同步执行)
    setTimeout(code, 0) // sync a:1 (全局作用域 异步执行)
    const fn = new Function(code)
    fn() // a:1 sync (全局作用域 同步执行)
    const script = document.createElement("script")
    script.innerHTML = code
    document.body.appendChild(script) // a:1 sync (全局作用域 同步执行)
}

exec('console.log("a:",a)')
console.log(`%csync`, 'font-weight: bold; color: white; background: black;');

解构赋值

const obj = {
    // name: 'demo',
    age: 20,
    // sex: "男"
}
let { name = "hello", age: ageObj, sex: sexObj = "未知" } = obj;
console.debug(name); // hello
console.debug(ageObj); // 20
console.debug(sexObj); // 未知

页面滚动到指定元素

使用原生html的锚点跳转<a href="#id名" />有2个缺点:1. 影响到地址栏,会和Router起冲突 2. 指定元素只能置顶不能指定位置

因此可以使用以下JS:

// 平滑滚动到页面中间
$0.scrollIntoView({behavior:'smooth',block:'center'})

手动解析DOM树

DOM解析器DOMParser

function removeTag(fragment) {
  return (
    new DOMParser().parseFromString(fragment, "text/html").body.textContent ||
    ""
  );
}

console.log(
  removeTag(`<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div>
      <ul>
        <li>苹果</li>
        <li>雪梨</li>
        <li>香蕉</li>
      </ul>
    </div>
  </body>
</html>
`)
);
/* 苹果
雪梨
香蕉 */

面试题

大整数相加

/**
 * 大整数相加
 * @param {string} a 
 * @param {string} b
 * @return {string} 结果
 * @example 
 * add('99', '1') => '100'
*/
function sum(a, b) {
    // 确定两个数中较长的数字长度
    const len = Math.max(a.length, b.length);
    // 将较短的数用 0 补齐到与较长数相同的长度
    a = a.padStart(len, '0');
    b = b.padStart(len, '0');
    // 初始化进位为 0
    let carry = 0;
    // 存储相加的结果
    let result = '';
    // 从最后一位开始逐位相加
    for (let i = len - 1; i >= 0; i--) {
        // 将当前位的数相加并加上上一次的进位
        const sum = +a[i] + +b[i] + carry;
        // 计算当前位的数字
        const r = sum % 10;
        // 更新进位
        carry = Math.floor(sum / 10);
        // 将当前位的数字加入到结果的开头
        result = r + result;
    }
    // 如果最高位还有进位,需要将进位加到结果的开头
    if (carry) {
        result = carry + result;
    }
    // 返回最终结果
    return result;
}

一些面试题

["1","2","3"],map(parse)

因为map()只要传入function 就会默认传递3个参数;而parse()则会根据第二个参数来把第一个参数转成不同的进制(只能传入0或者2到36之间,0则转成十进制)
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

代码题

考核Promise的封装

已知有一个异步接口函数为:

function getData(a, b, callback) {
  setTimeout(function () {
    callback(a + b);
  }, 1000);
}

请用此定义一个新函数getData2实现:

getData2(1, 2).then((result) => {
  console.log(result);
});

答案:

function getData(a, b, callback) {
  setTimeout(function () {
    callback(a + b);
  }, 1000);
}

function getData2(a, b) {
  return new Promise(function (resolve, reject) {
    getData(a, b, function (result) {
      resolve(result);
    });
  });
}

// 使用 getData2
getData2(1, 2).then((result) => {
  console.log(result);
});

考核类型转换

知识点:
以下值可以自动转换为布尔值false

  • 数字0
  • NaN
  • 空字符串""
  • null / undefined
console.log([] == 0); // true——空数组 [] 被转换为空字符串 "",然后转换为数字 0,最终比较为 0 == 0
console.log([] == false); // true——隐式类型转换,它将空数组 [] 转换为一个空字符串 "",然后将空字符串转换为数字 0,最后与 false 进行比较,因此结果是 0 == false
console.log(![] == false); // true——[] 是一个空数组,在布尔上下文中被视为真值,![] 将真值取反,变成 false。
console.log(null == undefined); // true
console.log(null == 0); // false——null、undefined与其他任何值都不相等,包括 0
console.log(undefined == 0); // false——null、undefined与其他任何值都不相等,包括 0
console.log("[object Object]" == {}); // true——"[object Object]" 是一个字符串,{} 是一个空对象。当将对象与字符串进行比较时,JavaScript 会尝试将对象转换为字符串。
console.log(Number("abc") == Number("aaa")); // false——比较 NaN == NaN

考核解析url

提取 url 变量中的数据,生成 data 对象,并能够根据url 的变化得到相应的数据

// 地址
const url = "https://www.baidu.com?keyword=lzy&page=1&size=10";

// 需要生成的对象
const data = {
  protocol: "https",
  host: "baidu.com",
  origin: "https://www.baidu.com",
  query: {
    keyword: "lzy",
    page: 1,
    size: 10,
  },
};
const url = "https://www.baidu.com?keyword=lzy&page=1&size=10";

// 解析 URL
const urlObj = new URL(url);

// 提取数据
const data = {
  protocol: urlObj.protocol.replace(":", ""),
  host: urlObj.hostname,
  origin: urlObj.origin,
  query: {},
};

// 将查询参数添加到 query 对象中
urlObj.searchParams.forEach((value, key) => {
  if (!isNaN(value)) {
    value = parseInt(value);
  }
  data.query[key] = value;
});
console.log("原url:");
console.table(data);

// 模拟 URL 变化并更新数据
function updateDataFromUrl(newUrl) {
  const newUrlObj = new URL(newUrl);
  data.protocol = newUrlObj.protocol.replace(":", "");
  data.host = newUrlObj.hostname;
  data.origin = newUrlObj.origin;
  data.query = {};

  newUrlObj.searchParams.forEach((value, key) => {
    if (!isNaN(value)) {
      value = parseInt(value);
    }
    data.query[key] = value;
  });
  console.log("新url:");
  console.table(data);
}

// 模拟 URL 变化
updateDataFromUrl("https://www.example.com?keyword=test&page=2&size=20");

怎么判断变量a 是否为空对象{}

(typeof a === 'object') && (a !== null) && (Object.keys(a).length === 0)

考核形参

let user = {
  name: "小A",
};

function changeName(user) {
  user.name = "小B";
  user = {
    name: "小C",
  };
  console.log("内部user.name:", user.name);
}
changeName(user);
console.log("外部user.name:", user.name);
/* 内部user.name: 小C
外部user.name: 小B */

考核事件循环+闭包

for (var i = 0; i < 5000; i+=1000) {
  setTimeout(function () {
    console.log(i);
  }, i);
}
// 主线程结束后分别在第0、1、2、3、4秒后打印“5”

考核事件循环:主线程、微任务、宏任务

// 1 4 10 12 14 16 暂停5s 11 5 15 6 13 7 2 3 9 8

console.log(1);

setTimeout(() => {
  console.log(2);
  Promise.resolve().then(() => {
    console.log(3);
  });
});

new Promise((resolve, reject) => {
  console.log(4);
  resolve(5);
}).then((data) => {
  console.log(data);
  Promise.resolve()
    .then(() => {
      console.log(6);
    })
    .then(() => {
      console.log(7);
      setTimeout(() => {
        console.log(8);
      }, 0);
    });
});

setTimeout(() => {
  console.log(9);
});

console.log(10);

process.nextTick(function () {
  console.log(11);
});

async function async1() {
  console.log(12);
  await async2();
  console.log(13);
}

async function async2() {
  console.log(14);
  await async3();
  console.log(15);
}

async function async3() {
  console.log(16);
}

async1();

function delay(duration) {
  let start = Date.now();
  console.log("暂停5s");
  while (Date.now() - start < duration) {}
}

delay(5000);

setTimeout(
    () => console.log(1) // 宏仁务
    , 0
);

(function () {
    new Promise((resolve, reject) => resolve(2, 3))
        .then((arg1, arg2, arg3) => {
            console.log(arg1, arg2, arg3); // 微任务; 2 undefined undefined;因为resolve只能接受一个参数
            return arg1;
        })
        .finally(() => {
            const args = Array.prototype.slice.apply(arguments);
            console.log(args.reverse()); // 微任务; [];因为finally不能接受参数
        });
    console.log(arguments[0]); // 主线程
})(4);
console.log(5); // 主线程

// 所以输出顺序是:4 5 2 undefined undefined [] 1
console.log("1");
const p = new Promise((resolve) => {
    console.log("2");
    resolve();
});
setTimeout(() => {
    p.then(() => console.log("3"));
    console.log("4");
}, 0);
Promise.resolve().then(() => console.log("5"));
console.log("6");

// 1, 2, 6, 5, 4, 3

考核函数作用域+undefined拼接数字/字符串

var foo = "Hello";
var num = 1;
var demo;
(function () {
  var bar = "World";
  console.log(foo + bar);
})();
console.log(foo + demo);
console.log(num + demo);
console.log(foo + bar);
// HelloWorld
// Helloundefined(字符串拼接undefined)
// NaN(数字拼接undefined)
// Uncaught ReferenceError: bar is not defined(函数作用域)

考核闭包

已知页面中有 5个button 元素,现执行以下代码,按照预期,当点击某个button 时,应该弹出一个对话框显示其序号(从0开始)。请问此时点击第 2个button,弹出什么内容?为什么?怎么解决?

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <button>Click</button>
    <button>Click</button>
    <button>Click</button>
    <button>Click</button>
    <button>Click</button>
    <script>
      var add_the_handlers = function (nodes) {
        var i;
        for (i = 0; i < nodes.length; i++) {
          nodes[i].onclick = function (e) {
            alert(i);
          };
        }
      };
      var buttons = document.getElementsByTagName("button");
      add_the_handlers(buttons);
    </script>
  </body>
</html>

解决办法:使用闭包,将每个循环迭代的值保存在一个闭包作用域中。可以通过将循环的索引作为参数传递给一个自执行的函数,从而在每个迭代中创建一个新的闭包作用域。

for (i = 0; i < nodes.length; i++) {
    (function (i) {
      nodes[i].onclick = function (e) {
        alert(i);
      };
    })(i);
  }

考核嵌套函数作用域

var foo = function () {
  var a = 3,
      b = 5;
  var bar = function () {
      var b = 7,
          c = 11;
      console.log(`一:a:${a} b:${b} c:${c}`);
      a += b + c;
      console.log(`二:a:${a} b:${b} c:${c}`);
  };
  console.log(`三:a:${a} b:${b} c:${c}`);
  bar();
  console.log(`四:a:${a} b:${b} c:${c}`);
};
foo();

// 三:a:3 b:5 c:undefined
// 一:a:3 b:7 c:11
// 二:a:21 b:7 c:11
// 四:a:21 b:5 c:undefined

考核变量作用域和 this 指向

var name = "张三"; // 全局变量

var person = (function () {
    var name = "李四"; // 函数作用域内的局部变量
    return {
        name: "王五",
        sayName: function () {
            var name = "赵六"; // 函数作用域内的局部变量
            console.log(this.name);
        }
    }
})();

console.log(name); // 张三
console.log(person.name); // 访问 person 对象的 name 属性,结果为:王五
person.sayName(); // 调用 person 对象的 sayName 方法,此时 this 指向 person 对象,输出为:王五
person.sayName.call(window); // 显式地调用 person 对象的 sayName 方法,并将 this 绑定到 window 对象,输出为:张三
window.addEventListener("DOMContentLoaded", person.sayName); // 添加 DOMContentLoaded 事件监听,当事件触发时调用 person.sayName 方法,此时 this 指向触发事件的对象(window),输出为:undefined

考核变量提升+事件循环

console.log(a); // 这时 a 是一个函数,它已经被声明了,但是还未被赋值,所以输出 [Function: a]。
console.log(a()); // 函数 a 中并没有返回值,所以输出 undefined
var a = 1; // 将 a 重新赋值为 1
function a() {
  setTimeout(function () {
    console.log(2); // 微任务
  }, 0);
  Promise.resolve().then(function () {
    console.log(3); // 宏任务
  });
  console.log(4);
}
console.log(a); // 1
a = 5;
console.log(a); // 5
// [Function: a] 4 undefined 1 5 3 2

考核深浅拷贝

const obj1 = {
  a: 1,
  b: {
    c: 2,
  },
};
const obj2 = Object.assign({}, obj1); // 浅拷贝
const obj3 = JSON.parse(JSON.stringify(obj1)); // 深拷贝
obj1.a = 10;
obj1.b.c = 20;
console.log(obj1); // {a:10,b:{c:20}}
console.log(obj2); // {a:1,b:{c:20}}
console.log(obj3); // {a:1,b:{c:2}}

颜色排序

按“黑色白色其他颜色”的先后顺序排序,同时遇到同样颜色则根据“size”从小到大排序

let arr = [
  { color: "black", size: 10 },
  { color: "black", size: 5 },
  { color: "white", size: 5 },
  { color: "white", size: 10 },
  { color: "red", size: 12 },
  { color: "red", size: 9 },
  { color: "blue", size: 9 },
  { color: "green", size: 6 },
  { color: "orange", size: 55 },
  { color: "orange", size: 5 },
  { color: "orange", size: 5 },
];

const colorOrder = ["black", "white", "red", "blue", "green", "orange"];

arr.sort((a, b) => {
  const colorIndexA = colorOrder.indexOf(a.color);
  const colorIndexB = colorOrder.indexOf(b.color);

  if (colorIndexA !== colorIndexB) {
    return colorIndexA - colorIndexB;
  } else {
    return a.size - b.size;
  }
});

console.log(arr);

算法题

考核递归:将包含层级信息的平面数组转换为树状结构

function arrayToTree(arr, parentId = null) {
  const tree = [];

  for (const item of arr) {
    if (item.parentId === parentId) {
      const children = arrayToTree(arr, item.id);

      if (children.length > 0) {
        item.children = children;
      }

      tree.push(item);
    }
  }

  return tree;
}

// 示例数据
const nestedArray = [
  { id: 1, name: "Node 1", parentId: null },
  { id: 2, name: "Node 2", parentId: null },
  { id: 3, name: "Node 1.1", parentId: 1 },
  { id: 4, name: "Node 1.2", parentId: 1 },
  { id: 5, name: "Node 2.1", parentId: 2 },
  { id: 6, name: "Node 1.1.1", parentId: 3 },
];

const tree = arrayToTree(nestedArray);
console.log(JSON.stringify(tree, null, 2));

反转字符串

编写一个JavaScript 函数,接受一个字符串作为输入,然后返回该字符串的反转版本。例如,如果输入为"Hello,World!“,则函数应该返回"IdlroW .olleH”
请注意:
1)不要使用JavaScript 内置的 reverse函数
2)考虑处理字符串中的空格和特殊字符

function reverseString(str) {
  var reversed = '';
  for (var i = str.length - 1; i >= 0; i--) {
    reversed += str.charAt(i);
  }
  return reversed;
}

// 测试
var inputString = "Hello, World!";
var reversedString = reverseString(inputString);
console.log(reversedString); // 输出 "dlroW ,olleH"

贪心算法

给出一组区间,将所有重叠的区间合并为一个区间。
输入:intervals =[1.3],[2,6],[8,10],[15,18]
输出:[1,6],[8.10],[15,18]
(解释:在上面的示例中,区间 [1,3] 和 [2,6] 重叠了,因此它们被合并为 [1,6])

function mergeIntervals(intervals) {
  if (intervals.length <= 1) {
    return intervals;
  }

  // 首先按照区间的起点进行排序
  intervals.sort((a, b) => a[0] - b[0]);

  const mergedIntervals = [intervals[0]];

  for (let i = 1; i < intervals.length; i++) {
    const currentInterval = intervals[i];
    const lastMergedInterval = mergedIntervals[mergedIntervals.length - 1];

    // 如果当前区间的起点小于等于上一个合并区间的终点,则它们重叠
    if (currentInterval[0] <= lastMergedInterval[1]) {
      // 合并两个区间,取最大的终点
      lastMergedInterval[1] = Math.max(lastMergedInterval[1], currentInterval[1]);
    } else {
      // 否则,当前区间不重叠,将其加入结果数组
      mergedIntervals.push(currentInterval);
    }
  }

  return mergedIntervals;
}

const intervals = [[1, 3], [2, 6], [8, 10], [15, 18]];
const merged = mergeIntervals(intervals);
console.log(merged);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

刘泽宇Developer

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值