前端面试题系列之-原生js篇

隐式类型转换相关

字符串连接符与算数运算符

  • 字符串连接符+:会把其他数据类型调用String()方法转换成字符串然后拼接
  • 算数运算符+:会把其他数据类型调用Number()方法转换成数字然后做加法计算

关系运算符

关系运算符会把其他数据转换成number之后再比较

  • 一个为字符串另一个为数字,则将字符串转换成数字比较
  • 两个都为字符串则依次比较两个的charCodeAt()值
  • undefined与null互相比较为相等,与其他类型比较为不等
  • NaN与任何数据都不等
  • 复杂数据类型会先调用valueOf()再调用toString()转换成字符串,然后再转换成number

逻辑非运算符

  • 有逻辑非运算符的会将数据转换成布尔类型
  • 除了0、-0、NaN、undefined、null、""、false以外都为true

各类型隐式转换规则

  • String:变量 + 字符串
  • Number:变量 + 自增自减运算符,算术运算符,关系运算符
  • Boolean:变量 + 逻辑非运算符

字符串类型转为数值类型:

  • 字符串任意位置出现任意非数字、非空格的字符,均转为NaN
  • 数字中间存在空格,转为NaN

闭包相关

什么是闭包

闭包就是一个函数,这个函数能够访问其他函数的作用域中的变量。

优点

  • 封装变量,模仿块级作用域

缺点

  • this指向问题
  • 内存泄漏问题
  • 变量发生变化问题

作用域和变量提升相关

作用域

作用域分为全局作用域、函数作用域及块级作用域

变量提升

js在执行一个作用域的代码之前,会先将变量声明及函数声明提到整个作用域顶部,随后依次执行代码,其中若变量与函数同名,函数优先级要高于变量。
即:

  • 将变量声明与函数声明提到作用域顶部
  • 函数声明优先级高于变量声明
  • 依次执行代码

作用域链

如果当前作用域中找不到变量,则会想上层作用域查找,依次往上查找,形成作用域链

NaN是什么?有什么特别之处?

NaN 是 not a number 的缩写,代表非数字值的特殊值,该属性用于指示某个值不是数字,是一个全局对象的属性
特别之处:

  • NaN 属性的初始值就是 NaN,和 Number.NaN 的值一样
  • 在现代浏览器中(ES5),NaN是一个不可配置(non-configurable),不可写(non-writable)的属性.但是在ES3中,这个属性的值是可以更改的,但是也应该避免覆盖
  • 编码中很少直接用到 NaN ,通常都是计算失败的时候,作为Math的某个方法的返回值出现(例如:Math.sqrt(-1)),或者尝试将一个字符串解析成为数字,但是失败了的时候(parseInt("blabla"))
  • NaN和任何值比较,都不相等,包括它自己NaN是number类型 typeof NaN //'number'

== 和 === 的区别

两等是值判断,会调用隐式类型转换,不判断类型
三等是值判断加类型判断,判断两个值是否严格相等

typeof和instanceof的区别

typeof

用于判断数据类型,返回值为6个字符串,分别为string、Boolean、number、function、object、undefined。其中null会返回object结果

instanceof

instanceof用来判断对象,判断方法是根据对象的原型链依次向下查询

js的数据类型有哪些

JS原生有5种简单数据类型:UndefinedNullBooleanNumberString。还有一个复杂数据类型Object函数在JS中是对象,而不是一种数据类型

undefined和null的区别

Undefined类型为变量声明了但是没有初始化(var box),而Null表示一个空对象引用(var box = null)。注意:未初始化的变量和赋值为 null 的 变量会相等,可通过typeof或者使用三等进行判断

call、apply、bind的区别

call 和 apply 是为了改变函数体内部 this 的指向。对于 apply、call 二者而言,作用完全一样,只是接受参数的方式不太一样。bind()最简单的用法是创建一个函数,使这个函数不论怎么调用都有同样的this值。

示例:

function func(arg1, arg2) {};

func.call(this, arg1, arg2);
func.apply(this, [arg1, arg2])
var obj = {
    a: 123,
};
 
var foo = {
    getV: function(k) {
        return this.a + k;
    }
}
 
console.log(foo.getV.bind(obj, 1)());  //124
console.log(foo.getV.call(obj, 1));    //124
console.log(foo.getV.apply(obj, [1]));   //124

区别:

  • 传参不同,调用不同。bind 是返回对应函数,便于稍后调用;apply 、call 则是立即调用 。

js的假值有哪些

0-0NaNundefinednull""false

js面向对象与原型、原型链相关

参考链接

原型(对象属性)

Javascript规定,每个函数都有一个prototype对象属性。只有函数才有prototype属性。指向另一个对象(原型链上面的)。prototype(对象属性)的所有属性和方法,都会被构造的实例继承。这意味着,我们可以把那些不变的属性和方法,直接定义在prototype对象属性上。

prototype就是调用构造函数所创建的那个实例对象的原型(proto)。

prototype可以让所有对象实例共享它所包含的属性和方法。也就是说,不必在构造函数中定义对象信息,而是可以直接将这些信息添加到原型中。

原型链 (JS原型与原型链继承)

实例对象与原型之间的连接,叫做原型链。proto( 隐式连接 )
JS在创建对象的时候,都有一个叫做proto的内置属性,用于指向创建它的函数对象的原型对象prototype。
内部原型(proto)和构造器的原型(prototype)
1、每个对象都有一个proto属性,原型链上的对象正是依靠这个属性连结在一起
2、作为一个对象,当你访问其中的一个属性或方法的时候,如果这个对象中没有这个 方法或属性,那么Javascript引擎将会访问这个对象的proto属性所指向上一个对 象,并在那个对象中查找指定的方法或属性,如果不能找到,那就会继续通过那个对象 的proto属性指向的对象进行向上查找,直到这个链表结束。

浅谈constructor

在 Javascript 语言中,constructor 属性是专门为 function 而设计的,它存在于每一个 function 的prototype 属性中。这个 constructor 保存了指向 function 的一个引用。

__proto__

JS 在创建对象(不论是普通对象还是函数对象)的时候,都有一个叫做 __proto__ 的内置属性,用于指向创建它的构造函数的原型对象。

    var obj = []
    console.log(obj.__proto__ === Array.prototype)  //true

示例题

    function Foo() {}
    var f1 = new Foo()

请回答以下问题:

  1. f1.__proto__ ===
  2. f1.constructor ===
  3. f1.prototype ===
  4. Foo.__proto__ ===
  5. Foo.prototype ===
  6. Foo.constructor ===
  7. Foo.prototype.constructor ===
  8. Foo.prototype.__proto__ ===

继承的几种写法及优缺点

JS继承的实现方式

this指向相关

  • this 永远指向函数运行时所在的对象,而不是函数被创建时所在的对象。
  • this的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定this到底指向谁,实际上this的最终指向的是那个调用它的对象

js new一个对象都经过了什么

new对象:

function Person(name, age) {
  this.name = name;
  this.age = age;
}
var person = new Person("Alice", 23);

new一个对象的四个过程:1、创建一个空对象?

var obj = new Object();

2、让Person中的this指向obj,并执行Person的函数体?

var result = Person.call(obj);

3、设置原型链,将obj的proto成员指向了Person函数对象的prototype成员对象?

obj.__proto__ = Person.prototype;

4、判断Person的返回值类型,如果是值类型,返回obj。如果是引用类型,就返回这个引用类型的对象。

if (typeof(result) == "object")
  person = result;
else
  person = obj;

宏任务和微任务

  • 宿主环境提供的叫宏任务,宿主环境包括浏览器和node
  • 宏任务有:
    • script
    • setTimeout
    • setInterval
    • setImmediate
    • requestAnimationFrame
    • I/O
    • UI渲染
  • 由语言标准提供的叫微任务,即由语言标准(ECMA)提供的就是微任务,比如ES6提供的promise。
  • 微任务有
    • process.nextTick
    • MutationObserver
    • Promise

执行过程

  • js引擎将代码按照类别分到宏任务和微任务队列中
  • 执行宏任务,执行完成,清空微任务队列
  • 执行下一个宏任务,执行完成后,清空微任务队列
  • 依次执行宏任务队列的下一个宏任务,执行完成后,清空微任务队列

怎样理解setTimeout 执行误差

如果当前 执行栈 所花费的时间大于 定时器 时间,那么定时器的回调在 宏任务(macrotask) 里,来不及去调用,所以这个时间会有误差。

如何实现深浅拷贝

浅拷贝

// 1. ...实现
let copy1 = {...{x:1}}
// 2. Object.assign实现
let copy2 = Object.assign({}, {x:1})
// 数组采用slice、concat方法

深拷贝

// 1. JOSN.stringify()/JSON.parse()
let obj = {a: 1, b: {x: 3}}
JSON.parse(JSON.stringify(obj))

// 2. 递归拷贝
function deepClone(obj) {
  let copy = obj instanceof Array ? [] : {}
  for (let i in obj) {
    if (obj.hasOwnProperty(i)) {
      copy[i] = typeof obj[i] === 'object' ? deepClone(obj[i]) : obj[i]
    }
  }
  return copy
}

JSON.stringify()的缺点

  • 会忽略 undefined
  • 会忽略 symbol
  • 不能序列化函数
  • 不能解决循环引用的对象
  • 不能正确处理new Date()
  • 不能处理正则

数组去重的几种写法

let list = [1, 2, 3, 4, 5, 2, 2, 2, 7, 6, 4, 8, 1];
// 创建新数组,判断数据是否在新数组中
function arrToRepeat1(arr) {
    let repeatList = [];
    list.forEach(value => {
        if (!repeatList.includes(value)) repeatList.push(value);
    })
    return repeatList;
}
console.log(arrToRepeat1(list));
// 创建新数组,通过下标判断是否是重复的值
function arrToRepeat2(arr) {
    let repeatList = [];
    list.forEach((value, index) => {
        // 左侧的为第一个值的索引,右侧为重复的当前的值的索引
        if (arr.indexOf(value) === index) repeatList.push(value);
    })
    return repeatList;
}
console.log(arrToRepeat2(list));
// 使用set集合
console.log([...new Set(list)]);

数组排序的问题

默认数组排数函数sort有问题,不指定排序方法时,元素会按照转换为的字符串的诸个字符的Unicode位点进行排序

常用解决方法


list.sort((a, b) => a - b) // 从小到大
list.sort((a, b) => b - a) // 从大到小

数组降维

array = array.reduce((acc, val) => acc.concat(val), []);  // 一层扁平化
array = [].concat(...array);  // 一层扁平化
array = array.flat();  // 一层扁平化

手写防抖和节流

js垃圾回收机制,怎么理解js中的内存泄露

垃圾回收机制

垃圾回收是释放已经不再需要的变量等,来减少系统所占用的内存。

标记清除法

工作原理:
当变量进入环境时(例如在函数中声明一个变量),将这个变量标记为“进入环境”,当变量离开环境时,则将其标记为“离开环境”。标记“离开环境”的就回收内存。
工作流程:

  • 垃圾收集器会在运行的时候会给存储在内存中的所有变量都加上标记。
  • 去掉环境中的变量以及被环境中的变量引用的变量的标记。
  • 那些还存在标记的变量被视为准备删除的变量。
  • 最后垃圾收集器会执行最后一步内存清除的工作,销毁那些带标记的值并回收它们所占用的内存空间。
引用计数法

跟踪记录每个值的引用次数(值)
流程:

  • 声明一个变量,并给这个变量赋值,这个值的引用次数就是1
  • 同一个值被赋给另一个变量,这个值的引用次数+1
  • 变量的值被更改了 那原本那个值的引用次数-1
  • 引用次数为0时,说明没办法访问这个值了
  • 垃圾收集器下一次运行时,会释放引用次数为0的值所占的内存

缺点:循环引用将不能被自动回收。

内存泄漏

内存泄漏是由于疏忽或错误造成程序未能释放那些已经不再使用的内存,造成内存的浪费。

引起内存泄漏的情况
  • 意外的全局变量
  • 被遗忘的定时器和定时器的回调函数
  • 闭包
  • 循环引用问题
  • 没有清理的DOM元素引用

DOM事件模型是什么

事件处理的几种写法

移动端的touch事件

事件委托/事件代理

target、currentTarget的区别?

你可能感兴趣的:(前端面试题系列之-原生js篇)