JS面试题(持续更新中)

目录

一、引用类型和基本类型的区别

1.基本类型

 1.1 Number

1.2 Undefined

1.3 String

1.4 Null

1.5 Boolean

1.6 Symbol

2.引用类型

2.1 Object

 2.2 Array

 2.3 Function

 其他引用类型

3.存储区别

3.1 基本类型

 3.2 引用类型

4.总结

二、== 和===区别,在什么情况下使用?

1.等于操作符( == )

总结:

2.全等操作符

3.区别

4.总结

三、typeof与instanceof区别

1.typeof

2.instanceof 

3.区别

四、深拷贝浅拷贝的区别?如何实现一个深拷贝?

1.数据类型存储

2.浅拷贝

2.1 object.assign

2.2 slice() 

 2.3 concat()

 2.4 扩展运算符

 3.深拷贝

3.1 _cloneDeep()

3.2 jQuery.extend()

3.3 JSON.stringify()

3.4 循环递归

4.区别

5.总结

二、对作用域链的理解?

1.作用域

1.1 全局作用域

 1.2 函数作用域

1.3 块级作用域

 2.词法作用域

3.作用域链

三、箭头函数


一、引用类型和基本类型的区别

在javaScript中,可以分为两种类型:基本类型和复杂类型

两种类型的区别:存储位置不同

1.基本类型

基本类型主要为6种:

  • Number
  • String
  • Boolean
  • Undefined
  • null
  • symbol

 1.1 Number

数值最常见的整数类型格式则为十进制,还可以设置八进制(零开头),十六进制(0x开头)。

let intNum = 55 // 10进制的55
let num1 = 070 // 8进制的56
let hexNum = 0xA // 16进制的10

浮点类型则在数值汇总必须包含小数点,还可以通过科学计算法表示

let floatNum1 = 1.1
let floatNum2 = 0.1
let floatNum3 = .1 // 有效但不推荐
let floatNum = 3.125e7 // 等于31250000

在数值类型中,存在一个特殊数值NaN,表示“不是数值”,用于表示本来要返回数值的操作失败了(而不是抛出错误)

console.log(0/0)
// NaN
console.log(-0/+0)
// NaN

1.2 Undefined

Undefined类型只有一个值,就是特殊值undefined。当使用var或者let声明了变量但没有初始化时,就相当于给变量赋予了undefined值。

let message
console.log(message == undefined) // true

包含undefined值的变量跟未定义变量是有区别的

 message这个变量被声明了,只是值为undefined

没有声明age这个变量,就会报错

1.3 String

字符串可以使用双引号(“”)、单引号(‘’)或者反引号(``)标示

 字符串是不可变的,意思是一旦创建了,它们的值就不能变了

 // 可以先销毁再创建

1.4 Null

Null类型只有一个值,即特殊值null。

逻辑上讲,null值表示一个空对象指针,这也是给typeof传一个null会返回’object‘的原因。

 undefined值是由null值派生而来。

 只要变量要保存对象,而当时又没有那个对象可保存,就可以用ull来填充该变量。

1.5 Boolean

Boolean布尔值类型有两个字面值:truefalse

通过Boolean可以将其他类型的数据转化成布尔值。

规则如下:

数据类型 转换为true的值 转化为false的值
String 非空字符串 “ ”
Number 非零数值(包括无穷值) 0、NaN
Object 任意对象 null
Undefined N/A(不存在的) undefined

1.6 Symbol

Symbol(符号)是原始值,且符号实例时唯一、不可变的。符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。

let genericSymbol = Symbol();
let otherGenericSymbol = Symbol();
console.log(genericSymbol == otherGenericSymbol); // false

let fooSymbol = Symbol('foo');
let otherFooSymbol = Symbol('foo');
console.log(fooSymbol == otherFooSymbol); // false

2.引用类型

复杂类型统称为引用类型Object,主要描述下面三种:

  • Object
  • Array
  • Function

2.1 Object

创建object常用方式为对象那个字面量表示法,属性名可以是字符串或数值。

 2.2 Array

javaScript数组是一组有序的数据,但跟其他语言不同的是,数组中每个槽位可以存储任意类型的数据。并且,数组也是有动态大小的,会随着数据添加而自动增长。

 2.3 Function

函数实际上是对象,每个函数都是Function类型的实例,而Function也有属性和方法,跟其他引用类型一样。

函数存在三种常见的表达方式:

  • 函数声明

  •  函数表达式

  •  箭头函数

函数声明和函数表达式两种方式

 其他引用类型

除上述三种方式以外,还包括Date、RegExp、Map、Set等......

3.存储区别

基本数据类型和引用数据类型存储在内存中的不同位置:

  • 基本数据类型存储在栈中
  • 引用数据类型存储在堆中

当把变量赋给一个变量时,解析器首先要确认的就是这个值是基本类型值还是引用类型值。

3.1 基本类型

 a的值为基本类型,是存储在栈中,将a的值赋给b,虽然两个变量的值相等,但是两个变量保存在了不同的内存地址。

基本类型赋值过程:

栈内存

 3.2 引用类型

var obj1 = {}
var obj2 = obj1;
obj2.name = "Xxx";
console.log(obj1.name); // xxx

引用类型数据存放在堆内存中,每个堆内存中有一个引用地址,该引用地址存放在栈中。

obj1是一个引用类型,在赋值操作过程中汇总,实际将堆内存对象在栈内存的引用地址复制了一份给了obj2,实际上他们共同指向同一个堆内存对象,所以更改obj2会对obj1产生影响。

引用类型赋值过程:

4.总结

4.1 声明变量时不同的内存地址分配:

  • 简单类型的值存放在栈中,在栈中存放的是对应的值
  • 引用类型对应的值存储在堆中,在栈中存放的是指向堆内存的地址

4.2 不同的类型数据导致赋值变量时不同:

  • 简单类型赋值,是生成相同的值,两个对象对应不同的地址
  • 引用类型赋值,是将保存在对象的内存地址赋值给另一个变量。也就是两个变量指向堆内存中同一个对象

二、== 和===区别,在什么情况下使用?

1.等于操作符( == )

等于操作符用两个等于号( == )表示,如果操作数相等,则会返回true。

JavaScript中存在隐式转换,等于操作符( == )在比较中会进行类型转换,再确定操作数是否相等。

  • 如果任意操作数是布尔值,则会将其转换为数值再比较是否相等。

  • 如果一个操作数是字符串,另一个操作数是数值,则尝试将字符串转换为数值,再比较是否相等。

  • 如果一个操作数是对象,另一个操作数不是,则调用对象的valueOf( )方法取得其原始值,再根据前面的规则进行比较。
let obj = {valueOf:function(){return 1}}
let result1 = (obj == 1); // true
  • null和undefined相等。

  • 如果任意操作符是NaN,则相等操作符返回false 。

  • 如果两个操作符都是对象,则比较它们是不是同一个对象,如果两个操作符都指向同一个对象,则相等操作符返回true。
let obj1 = {name:"xxx"}
let obj2 = {name:"xxx"}
let result1 = (obj1 == obj2 ); // false

总结:

  • 两个都是简单类型,字符串和布尔值都会转换成数值,再比较
  • 简单类型与引用类型比较,对象转化成其原始类型的值,再比较
  • 两个都为引用类型,则比较它们是否指向同一个对象
  • null和undefined相等
  • 存在NaN则返回false

2.全等操作符

全等操作符是由(===)表示,只有两个操作数在不转换的前提下相等才会返回true。即类型相同,值相同。

不相等是因为数据类型不同 。

 

 相等是因为数据类型相同,值相等。

undefined和null与自身严格相等。

 

3.区别

相等操作符(==)会做类型转换,再进行值的比较,全等运算符(===)不会做类型转换。

 null和undefined比较,相等操作符(==)为true,全等为false。

4.总结

相等操作符隐藏的类型转换,会带来一些违反直觉的结果。

 

 

但在比较null的情况下,一般使用相等操作符(==)。

const obj = {}
if(obj.n == null){
    console.log("1") // 执行
}

 等同于以下写法:

 使用相等操作符(==)的写法更加简洁。

除了在比较对象属性为null或者undefined的情况下,可以使用相等操作符(==)。

其他情况一律使用全等操作符(===)。

三、typeof与instanceof区别

1.typeof

typeof操作符返回一个字符串,表示未经计算的操作数的类型。

使用方法:

operand表示对象或原始值的表达式,其类型将被返回。

 

 

以上例子前6个都是基础数据类型,虽然typeof null为objec,但这只是JavaScript存在的一个悠久的bug,不代表null就是引用数据类型,并且null本身也不是对象。

null在typeof之后返回的是有问题的结果,不能作为判断null的方法,如果需要在if语句中判断是否为null,直接通过 ===null 来判断即可。

可以发现引用类型数据,用typeof来判断,除了function会被识别出来,其余的都输出object。

判断一个变量是否存在,可以使用typeof:(不能使用if(a),若a未声明,会报错)

2.instanceof 

instanceof运算符用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上。

object instanceof constructor

object为实例对象,constructor为构造函数。

构造函数通过new可以实例化对象,instanceof能判断这个对象是否是之前那个构造函数生成的对象。

// 定义构造函数
let car = function()[]
let ben = new car()
ben instanceof car // true
let car = new String('zz')
car instanceof String
let str = 'zz'
str instanceof String

关于instanceof的实现原理:

也就是顺着原型链去找,直到找到相同的原型对象,返回true,否则返回false。

function myInstanceof(left,right) {
    //这里先用typeof来判断基础数据类型,如果是,直接返回false
    if(typeof left !== 'object' || left === null) return false
    // getProtypeof是object对象自带的API,能够拿到参数的原型对象
    let proto = Object.getPrototypeOf(left)
    while(true) {
        if(proto === null) return false
        if(proto === right.prototype) return true
        //找到相同原型对象,返回true
        proto = Object.getPrototypeOf(proto)
    }
}

3.区别

typeofinstanceof都是判断数据类型的方法,区别:

  • typeof会返回一个变量的基本类型,instanceof返回的是一个布尔值
  • instanceof可以准确的判断复杂引用数据类型,但是不能正确判断基础数据类型
  • typeof也存在弊端,它虽然可以判断基础数据类型(null除外),但是引用数据类型中,除了function类型以外,其他无法判断。

两者都有弊端,不能满足所有场景的需求。

需要通过检测数据类型,可以采用Object.prototype.toString,调用该方法,统一返回格式”[object Xxx]“的字符串。

Object.prototype.toString的基本用法:

 

 全局通用的数据类型判断方法:

function getType(obj){
    let type = typeof obj
    // 先进行typeof判断,如果是基础数据类型,直接返回
    if (type !== "object") {
        return type
    }
    // 对于typeof返回结果是object的,在进行如下的判断,正则返回结果
    return Object.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/,'$1')
}

使用如下:

 

四、深拷贝浅拷贝的区别?如何实现一个深拷贝?

浅拷贝:

拷贝基本数据类型为其他的值,拷贝引用数据类型为地址,生成新的数据,修改新的数据会影响原数据,实际开发常用的方法有:

  • object.assgin
  • 扩展运算符
  • 等等

深拷贝:

在内存中开辟一个新的栈空间保存新的数据,修改新数据不会影响到原数据,开发中常用的方法有:

  • loadsh中的_.cloneDeep()方法
  • JSON.stringify() 

1.数据类型存储

JavaScript中存在两大数据类型:

  • 基本类型
  • 引用类型

基本类型数据保存在栈内存中。

引用类型数据保存在堆内存中,引用数据类型的变量是一个指向堆内存中实际对象的引用,存在栈中。

2.浅拷贝

指的是创新数据,这个数据有着原始数据属性值的一份精确拷贝。

如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址。

即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址。

浅拷贝的实现:

在JavaScript中,存在浅拷贝的现象有:

  • object.assign
  • Array.prototype.slice()
  • Array.prototype.concat() 
  • 使用扩展运算符实现的复制

2.1 object.assign

2.2 slice() 

 2.3 concat()

 2.4 扩展运算符

 3.深拷贝

深拷贝开辟一个新的栈,两个对象属性完全相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性。

常见的深拷贝有:

  • _.cloneDeep()
  • jQuery.extend()
  • JSON.stringify()
  • 手写循环递归

3.1 _cloneDeep()

const _ = require('lodash')
const obj1 = {
  a: 1,
  b: { f: { g: 1 } },
  c: [1, 2, 3],
}
const obj2 = _.cloneDeep(obj1)
console.log(obj1.b.f === obj2.b.f) // false

3.2 jQuery.extend()

const $ = require('jquery')
const obj1 = {
  a: 1,
  b: { f: { g: 1 } },
  c: [1, 2, 3],
}
const obj2 = $.extend(true, {}, obj1)
console.log(obj1.b.f === obj2.b.f) // false

3.3 JSON.stringify()

const obj2 = JSON.parse(JSON.stringify(obj1))

这个方式存在弊端,会忽略undefined、symbol和函数。

const obj = {
  name: 'A',
  name1: undefined,
  name3: function () {},
  name4: Symbol('A'),
}
const obj2 = JSON.parse(JSON.stringify(obj))
console.log(obj2) // {name: "A"}

3.4 循环递归

function deepClone(obj, hash = new WeakMap()) {
  if (obj === null) return obj // 如果是null或者undefined我就不进行拷贝操作
  if (obj instanceof Date) return new Date(obj)
  if (obj instanceof RegExp) return new RegExp(obj)
  // 可能是对象或者普通的值  如果是函数的话是不需要深拷贝
  if (typeof obj !== 'object') return obj
  // 是对象的话就要进行深拷贝
  if (hash.get(obj)) return hash.get(obj)
  let cloneObj = new obj.constructor()
  // 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
  hash.set(obj, cloneObj)
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      // 实现一个递归拷贝
      cloneObj[key] = deepClone(obj[key], hash)
    }
  }
  return cloneObj
}

4.区别

深拷贝和浅拷贝都创建出一个新的对象,但在复制对象属性的时候,行为就不一样。

浅拷贝只复制属性指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存,修改对象属性会影响原对象。

 深拷贝会另外创造一个一摸一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象

// 深拷贝
const obj1 = {
  name: 'init',
  arr: [1, [2, 3], 4],
}
const obj4 = deepClone(obj1) // 一个深拷贝方法
obj4.name = 'update'
obj4.arr[1] = [5, 6, 7] // 新对象跟原对象不共享内存

console.log('obj1', obj1) // obj1 { name: 'init', arr: [ 1, [ 2, 3 ], 4 ] }
console.log('obj4', obj4) // obj4 { name: 'update', arr: [ 1, [ 5, 6, 7 ], 4 ] }

5.总结

前提为拷贝类型为引用类型的情况下:

  • 浅拷贝是拷贝一层,属性为对象时,浅拷贝是复制,两个对象指向同一个地址。
  • 深拷贝是递归拷贝深层次,属性为对象时,深拷贝时新开栈,两个对象指向不同的地址。

二、对作用域链的理解?

简单回答即:

作用域一般可以理解为函数或变量的生效范围,我们一般把作用域分成全局作用域,函数(局部)作用域,块级作用域(es6推出),例如:

我们在a函数中定义了一个变量,那么当我们在js中访问这个变量他就会在当前作用域进行查找,如果访问不到,他会一层一层向外进行查找,整个逐级向上查找的过程我们称为作用域链。

1.作用域

作用域即变量(变量作用域又称上下文)和函数生效(能被访问)的区域或集合。

即,作用域决定了代码区块中变量和其他资源的可见性。

例:

// 要先执行obj()这个函数,否则根本不知道里面是啥

函数obj内部创建一个obj1变量,当我们在全局访问这个变量的时候,系统就会报错,说明我们在全局是无法获取到(闭包除外)函数内部的变量。

作用域分为:

  • 全局作用域
  • 函数作用域
  • 块级作用域

1.1 全局作用域

任何不在函数中或是大括号中声明的变量,都是在全局作用域下,全局作用域下声明的变量可以在程序的任意位置访问。

 1.2 函数作用域

函数作用域也叫做局部作用域,如果一个变量是在函数内部声明的它就在一个函数作用域下面。这些变量只能在函数内部访问,不能在函数以外去访问。

 上述代码中在函数内部声明的变量或函数,在函数外部是无法访问的,这说明在函数内部定义的变量或者方法只是函数作用域。

1.3 块级作用域

ES6中引入了let 和const 关键字,和var关键字不同的,在大括号中使用let和const声明的变量存在于块级作用域中,在大括号之外不能访问这些变量。

 2.词法作用域

词法作用域又叫做静态作用域,变量被创建时就确定好了,而非执行阶段确定的,也就是我们写好代码时它的作用域就确定了,JavaScript遵循的就是词法作用域。

 相同层级的goo和oog就没有办法访问到彼此块级作用域中的变量,所以输出1。

3.作用域链

当在JavaScript中使用一个变量的时候,首先JavaScript引擎会尝试在当前作用域下去寻找该变量,如果没有找到,再找它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域。

如果在全局作用域里仍然找不到该变量,它就会在全局范围内隐式声明该变量(非严格模式下)或是直接报错。

在《你不知道的JavaScript(上)》中:把作用域比喻成一个建筑,这份建筑代表程序中的嵌套作用域链,第一层代表当前的执行作用域,顶层代表全局作用域,变量的引用会顺着当前楼层进行查找,如果找不到,则会往上一层找,一旦到达顶层,查找的过程都会停止。

代码展示:

  •  student函数内部属于最内层作用域,找不到name,向上一层作用域person函数内部找,找到了输出’张'。
  • student内部输出sex时找不到,向上一层作用域person函数找,还找不到继续向上一层找,即全局作用域,找到了输出‘女’。
  • 在person函数内部输出age时找不到,向上一层作用域找,即全局作用域,还是找不到就会报错。

三、箭头函数

箭头函数是ES6新增的特性,用来简化普通函数的写法,规避普通函数this指向的痛点。

1.箭头函数没有原型对象prototype,不能作为构造函数使用(不能被new)。

2.箭头函数没有arguments,可以使用`...`拿到所有实参的集合数组。

3.箭头函数中的this在定义时就已经确定,取决于父级的环境。

4.箭头函数不能通过call、apply、bind方法修改它的this指向(会忽略第一个参数,其他功能还是可以正常使用)。

 

 

 5.箭头函数不能用作Generator函数,不能使用yeild关键字(function\*)

 

总结:

箭头函数时定义函数一种新的方式,他比传统函数function定义更加方便和简单,他没有绑定自己的this指向和伪数组arguments,无法调用super方法生成实例化对象,因为他不是构造函数,一般用来取代匿名函数的写法,最主要的是箭头函数的this指向它的上一级作用域中的this也可以理解为它的this是固定的,而普通函数的this是可变的。

猜你喜欢

转载自blog.csdn.net/m0_62181310/article/details/127264028