javascript 面试题

JavaScript面试题

参考:https://juejin.cn/post/7061588533214969892#heading-32

1、数据类型

基本的数据类型 undefined, null, number, string, boolean
复杂的数据类型(引用类型) object
es6新添加的数据类型 symbol, bigInt

其中 symobol是标识符类型,主要作用是给对象赋值唯一的属性名,bigInt类型可以是任意大小的整数。

2、数据类型的判断

主要三种方法:

  1. typeof 能判断所有基本的数据类型和es6新的类型,但是对于复杂的数据类型,也就是引用类型,则是都返回为Object, 因此无法判断引用类型。

  2. instansof 只能判断复杂的数据类型,也就是引用类型。

  3. Object.prototype.toString.call() 万能判断方法,可以判断所有的数据类型,包括es6新的类型。

3、基本数据类型和复杂数据类型赋值的过程详解

基本数据类型的赋值过程。

值的变化都是在栈(stack)中进行的,栈的空间小,大小固定,被频繁操作。

比如:

let a = 100;
let b = a;
a = 200;
console.log(b); // 100

在这里插入图片描述
复杂的数据类型赋值过程,也就是引用类型。

引用类型的数据也就对象之间的拷贝赋值,在栈中存入的是对象的引用地址,而不是对象中实际的属性和方法,对象中的具体内容是存入在堆中的,堆的空间大,大小不固定,很适合用来存放对象中有多个属性或方法的情况。

上面涉及到浅拷贝和深拷贝的问题,这里仅是以浅拷贝举例,探讨的也是浅拷贝引用类型时的过程。

比如:

let a = { age: 20 };
let b = a;
b.age = 30;
console.log(a.age); // 30

在这里插入图片描述
这里简单解释一下浅拷贝,浅拷贝在基本数据类型之间赋值是没有问题的,但是在对象中由于栈只是存放对象的地址引用,这样在浅拷贝赋值时会导致多个对象公用一个引用地址,修改的内容空间也会是一样,在大部分使用对象的场景中,这样是不满足使用要求的。

4、深拷贝详情(手写)

参考:https://juejin.cn/post/6844903929705136141#heading-1

/**
 * 浅拷贝
 * @param {*} target 基本类型 | 引用类型
 * @returns 浅拷贝后的结果
 */
function shallowClone(target) {
  let cloneTarget;
  // 判断传入值是否是引用类型
  if (typeof target === "object") {
    cloneTarget = Array.isArray(cloneTarget) ? [] : {};
    for (const key in target) {
      cloneTarget[key] = target[key];
    }
    return cloneTarget;
  } else {
    // 非引用数据,直接拷贝即可
    cloneTarget = target;
    return cloneTarget;
  }
}

/**
 * 深拷贝 主要是针对引用类型(对象 数组)且解决了循环引用的问题
 * 循环引用 对象的属性间接或直接的引用了自身的情况,这样递归会进入死循环导致栈内存溢出报错
 * @param {*} obj 拷贝的目标
 * @param {*} map 创建并操作拷贝后的对象
 * @returns 深拷贝后的结果
 */
function deepClone(type, target, map = new Map()) {
  if (type === 0) {
    // 可以适用于绝大部分场景的深拷贝
    return JSON.parse(JSON.stringify(target));
  } else {
    // 严谨的深拷贝
    let cloneTarget;
    // 判断传入值是否是引用类型
    if (typeof target === "object") {
      cloneTarget = Array.isArray(cloneTarget) ? [] : {};
      // 检查map中有无克隆过的对象
      if (map.get(target)) {
        // 有就直接返回克隆的对象
        return map.get(target);
      }
      // 没有则继续拷贝,并存一遍拷贝值
      map.set(target, cloneTarget);
      for (const key in target) {
        // 有更深层次的对象可以继续递归直到属性为原始类型, 需要传入存储拷贝值后的map数据结构
        cloneTarget[key] = deepClone(target[key], map);
      }
      return cloneTarget;
    } else {
      cloneTarget = target;
      return cloneTarget;
    }
  }
}

/**
 * 拷贝函数的单元测试
 * @param {*} num 0-浅拷贝测试|1-深拷贝测试
 * @return 拷贝后的结果
 */
function test(num) {
  // 浅拷贝的测试数据
  const target = {
    a: 1,
    b: "测试",
  };
  if (num === 0) {
    return shallowClone(target);
  }
  // 深拷贝的测试数据
  target = {
    field1: 1,
    field2: undefined,
    field3: {
      child: "child",
      child2: {
        child2: "child2",
      },
    },
    field4: [2, 4, 8],
  };
  // 制造循环引用的数据
  target.target = target;
  // 使用严谨模式的深拷贝
  return deepClone(1, target);
}

// 打印拷贝后的结果
console.log(test(1));
5、谈谈 0.1+0.2 ! == 0.3 ,如何解决

因为计算机无法直接对十进制进行运算,所以需要转换为二进制之后进行对阶运算,js中转换为二进制的方法就是IEEE 754, 这种方法只会保留53位二进制数,多余则截掉,且由于有的十进制数转换为二进制数是无限不循环小数,这样截取掉部分数,就会产生精度损失。

又由于转换为二进制后,指数位数的不同,进行对阶运算时也会产生精度损失。

所以,造成精度可能损失的主要原因是 IEEE 754进制转换对阶运算中指数位数不同

解决精度损失的问题:

  1. 换成整数后在进行相加
/**
 * 加法运算(适用整数 小数 不适用超过16位的大整数)
 * @param {*} num1 加数
 * @param {*} num2 被加数
 * @returns 相加后的结果
 */
function add(num1, num2) {
  // 获取小数位数
  const num1Digits = (num1.toString().split(".")[1] || "").length;
  const num2Digits = (num2.toString().split(".")[1] || "").length;
  // 获取放大倍数 如10 100
  const baseNum = Math.pow(10, Math.max(num1Digits, num2Digits));
  // 将小数变为整数相加后在除以放大的倍数
  return (num1 * baseNum + num2 * baseNum) / baseNum;
}
  1. 转换为字符串后相加,在leetcode 415题有相关解法
/**
 * 字符串相加 leetcode 415题-字符串相加 https://leetcode.cn/problems/add-strings/
 * 给定两个字符串形式的非负整数 num1 和num2 ,计算它们的和并同样以字符串形式返回。
 * 你不能使用任何內建的用于处理大整数的库(比如 BigInteger), 也不能直接将输入的字符串转换为整数形式。
 * @param {*} num1 
 * @param {*} num2 
 * @returns 相加后返回字符串形式的结果
 */
function addStrings(num1, num2) {
    // 每位匹配计算的次数
  let n1 = num1.length - 1; 
  let n2 = num2.length - 1;
  const res = []; //存放每位相加后的结果
  let carry = 0; //每位相加大于等于10的进位
  while (n1 >= 0 || n2 >= 0) { //执行每位的相加
    let i = num1[n1] - 0 ? num1[n1] - 0 : 0;
    let j = num2[n2] - 0 ? num2[n2] - 0 : 0;
    const sum = i + j + carry; //如果有进位需要加在后面
    res.unshift(sum % 10); //每次存放余数在数组中头部位置
    carry = Math.floor(sum / 10); //存放进位的数值
    n1 = n1 - 1;
    n2 = n2 - 1;
  }
  if (carry) { //当n1,n2=0时,第0位相加也大于等于10时,就退出循环了,需要在这再填充一次到res
    res.unshift(carry);
  }
  return res.join(""); //最后将数组中的数字拼接回字符串
}
6、原型和原型链

参考:
https://github.com/mqyqingfeng/blog/issues/2
https://juejin.cn/post/7061588533214969892#heading-32

总结:

  • 原型是 js每创建一个对象,就会有与之相关联的另一个对象,也就是指原型。每个对象都会从原型中"继承"属性。
  • 原型链:由相互关联的原型构成的链状结构,就是原型链。

之后结合下面的图,加以理解。
在这里插入图片描述

图中由相互关联的原型组成的链状结构就是原型链,也就是蓝色的这条线。
在这里插入图片描述
需要注意的是:

  1. 所有构造函数的隐式原型都指Function的prototype,也就是Function的实例原型。
  2. 原型沿着原型链找到尽头是null,null的理解就是没有对象,也就是说沿着原型链找某个属性会在找到Object.prototype的实例原型时,停止查找。
7、作用域和作用域链
  • 作用域:规定了如何查找变量,也就是当前执行代码对变量的访问权限。也可以说是作用域决定了当前代码块中变量和其它资源的可见性。(全局作用域, 函数作用域, 块级作用域)

  • 作用域链:从当前的作用域一层层向上找某个变量,直到全局作用域,如果还没有找到就说明变量不存在。这种层级关系就是作用域链。(多个执行上下文的变量对象形成的链表就叫做作用域链)

需要注意的是,js 采用的是静态作用域,所以函数的作用域在函数定义时就确定了。

8、执行上下文

总结:当javascript中执行一段可执行的代码时,会创建一个执行上下文,在执行上下文中有三个重要的属性。

  • 变量对象 (variable object, OV)
  • 作用域链 (scope chain)
  • this
9、闭包

在某个内部函数的执行上下文创建时,会将父级函数的活动对象加到内部函数的 [[scope]] 中,形成作用域链,所以即使父级函数的执行上下文销毁(即执行上下文栈弹出父级函数的执行上下文),但是因为其活动对象还是实际存储在内存中可被内部函数访问到的,从而实现了闭包。

闭包是指那些能够访问自由变量的函数。其中自由变量是指在函数中使用的

// 1. 闭包中自由变量的查找,是在函数定义的地方,向上级作用域查找。不是在执行的地方。

// 函数定义的地方查找变量 因此在creat函数作用域中找到a 就不会去全局作用域中查找a了
function creat() {
  const a = 200;
  return function fn() {
    console.log(a);
  };
}
const a = 100;
const fn = creat();
fn();

// 2. 闭包应用实例 缓存工具,隐藏数据, 提供api
function creatCache() {
  let data = {}; //定义一个空对象,存放数据,且外部不能直接访问该数据
  return { //返回一个对象,提供两个api
    set: function (key, value) {
      data[key] = value;
    },
    get: function (key) {
      return data[key];
    },
  };
}
// 获取api
const api = creatCache()
// 使用api提供的方法去创建,获取数据
api.set('a', 100);
console.log(api.get('a'));
10、call, apply, bind实现

10.1 call

call可以指定this值去指向某一对象并调用函数或方法。

示例:

var obj = {
  value: "obj",
};
function fn() {
  console.log(this.value); //打印obj中的value
}
fn.call(obj); //改变this值,指向obj

也就是说,call可以做两件事:

  • 改变this指向
  • 执行函数或方法

call的具体实现:

/**
 * call方法的具体实现
 * @param {*} ctx 执行上下文 默认为window 如果需要改变this执行可以传入其它对象
 * @param  {...any} arg 函数外来参数,也就是函数的行参
 * @returns 函数执行的结果
 */
Function.prototype.myCall = function (ctx) {
  // 避免上下文不传,不传时默认为 window 全局对象
  ctx = ctx || window;
  // 判断调用对象
  if (typeof this !== "function") {
    throw new Error("Type error");
  }
  // 获取参数
  const args = [...arguments].slice(1);
  // 创建一个唯一的 Symbol 避免对象属性的重复
  const uniqueKey = Symbol("fn");
  // 保存当前this(当前this原来指向调用它的方法)
  ctx[uniqueKey] = this;
  const res = args.length > 0 ? ctx[uniqueKey](...args) : ctx[uniqueKey]();
  // 删除对象的属性,减少内存的占用
  delete ctx[uniqueKey];
  // 返回函数执行的结果
  return res;
};

// -----------------测试区域-------------------

// 全局变量
var target = "world";
// 对象中属性
var context = {
  target: "xinz",
};

function fn() {
  const args = Array.from(arguments).join(" ");
  console.log(`${args} ${this.target}`);
}

fn.myCall(context, "hello", "all", "the"); //hello all the xinz
fn.call(context, "hello", "all", "the"); //hello all the xinz
// 第一个参数为空,则默认使用全局对象window
// fn.myCall("", "hello", "all", "the"); //hello all the xinz

// -------------------------------------------

10.2 apply

apply和call使用上没有什么区别,唯一区别在于传参的方式。

apply的具体实现:

/**
 * apply方法的具体实现 [与call唯一不同在于传参的格式,call是列表,apply是数组]
 * @param {*} ctx 执行上下文 默认为window 如果需要改变this执行可以传入其它对象
 * @param  {...any} arg 函数外来参数,也就是函数的行参
 * @returns 函数执行的结果
 */
Function.prototype.myApply = function (ctx) {
  ctx = ctx || window;
  if (typeof this !== "function") {
    throw new Error("type error");
  }
  const uniqueKey = Symbol("fn");
  ctx[uniqueKey] = this;
  const res = arguments[1] ? ctx[uniqueKey](...arguments[1]) : ctx[uniqueKey]();
  delete ctx[uniqueKey];
  return res;
};

// -----------------测试区域-------------------

// 全局变量
var target = "world";
// 对象中属性
var context = {
  target: "xinz",
};

function fn() {
  const args = Array.from(arguments).join(" ");
  console.log(`${args} ${this.target}`);
}

fn.myApply(context, ["hello", "all", "the"]); //hello all the xinz
fn.apply(context, ["hello", "all", "the"]); //hello all the xinz
// 第一个参数为空,则默认使用全局对象window
// fn.myCall("", ["hello", "all", "the"]); //hello all the world

// -------------------------------------------

10.3 bind

参考:https://github.com/sisterAn/JavaScript-Algorithms/issues/81

bind 的实现与call和apply类似,但是最后返回的是一个函数。

也就是说bind 方法与 call / apply 最大的不同就是前者返回一个绑定上下文的函数,而后两者是直接执行了函数。

bind特点:

  • 指定this
  • 传入参数
  • 返回一个函数
  • 函数柯里化(把将一个接收多个参数的函数,变成接收单一参数,并返回之前函数结果的新函数的技术称为柯里化)

bind的具体实现:

/**
 * bind方法的具体实现
 * @param {*} ctx 执行上下文, 也就是指向myBind的调用者
 * @param  {...any} arg 函数外来参数,也就是函数的行参
 * @returns 函数执行的结果
 */
Function.prototype.myBind = function (ctx) {
  // 判断调用bind的是不是函数,不是则抛出异常
  if (typeof this !== "function") {
    throw new Error("type error");
  }
  // 保存this,且this是指向调用者
  const self = this;
  // 获取第一个参数(this)后的参数
  const args = Array.prototype.slice.call(arguments, 1)
  // const args = [...arguments].splice(1);

  return function Fn() {
    return self.apply(
      // 如果this是Fn的实例,说明是返回的Fn再次new了,需要指定this,否则this失效
      this instanceof Fn ? this : ctx,
      // 这里是将之前bind里面的参数和返回函数Fn里面又接收的参数合到一起
      args.concat(...arguments)
    )
  }
};

// -----------------测试区域-------------------

// 全局变量
var target = "world";
// 对象中属性
var context = {
  target: "xinz",
};

function fn() {
  const args = Array.from(arguments).join(" ");
  console.log(`${args} ${this.target}`);
}

const test = fn.myBind(context, "hello", "all", "the"); //hello all the xinz 
test();

// -------------------------------------------
11、new 的实现

主要做了4个步骤:

  1. 创建空对象

  2. 空对象的原型指向构造函数的原型

  3. 构造函数的this指向空对象,并执行构造函数(可以为这个新对象添加属性,不过构造函数中需要讲这些属性添加到原型中,这样新对象才会有这些属性)

  4. 判断函数的返回值类型,如果是引用类型则说明构造函数里面直接返回的对象,因此可以直接返回该引用类型的对象, 如果不是则正常返回继承了构造函数原型的新对象

function myNew(ctx){
  // 1. 创建空对象
  const obj = new Object();
  // 2. 空对象的原型指向构造函数的原型
  obj.__proto__ = ctx.prototype;
  // 3. 构造函数的this指向空对象,并执行构造函数(可以为这个新对象添加属性,不过构造函数中需要讲这些属性添加到原型中,这样新对象才会有这些属性)
  const res = ctx.apply(obj, [...arguments].slice(1))
  // 4. 判断函数的返回值类型,如果是引用类型则说明构造函数里面直接返回的对象,因此可以直接返回该引用类型的对象, 如果不是则正常返回继承原型了的新对象
  return typeof res === 'object' ? res : obj;
}

// -----------------测试区域-------------------
function Test(){
  this.a = 1;
}

Test.prototype = {
  fn1: function (){
    console.log('测试')
    return '执行完毕'
  } 
}
const test = myNew(Test);
console.log(test.a) //1
console.log(test.fn1()); //测试 执行完毕
// -------------------------------------------
12、游览器垃圾回收机制

参考:https://juejin.cn/post/6981588276356317214

有两种垃圾回收策略:

  • 标记清除:标记阶段即为所有活动对象做上标记,清除阶段则把没有标记(也就是非活动对象)销毁。
  • 引用计数:它把对象是否不再需要简化定义为对象有没有其他对象引用到它。如果没有引用指向该对象(引用计数为 0),对象将被垃圾回收机制回收。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

沐沐茶壶

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

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

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

打赏作者

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

抵扣说明:

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

余额充值