Javascript面试题大杂烩

117 阅读20分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天

第一篇: JS数据类型之问

1.JS原始数据类型有哪些?引用数据类型有哪些?

  1. 基本数据类型:字符串,布尔值,undefined,null,数字类型,es6新增symbol
  2. 引用数据类型:数组,对象,函数
function test(person){
    person.age=26
    person={
        name:'hzj',
        age:18
    }
    return person
}
const p1 = {
    name:'fqy',
    age:19
}
const p2 = test(p1)
console.log(p1)//p1:{name:'hzj',age:26}
console.log(p2)//p2:{name:'fqy',age:19}

原因:函数在传参的时候传递的是对象在堆中的内存地址,,test函数中的实参person是p1对象的内存地址,通过调用person.age = 26确实改变了p1的值,但随后person变成了另一块内存空间的地址,并且在最后将这另外一份内存空间的地址返回,赋给了p2。

2.null是对象吗?

null不是对象,虽然typeof会输出object,但是这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象然而 null 表示为全零,所以将它错误的判断为 object 。

3..tostring()为什么可以调用?

var s = new String('1')
s.toString()
s=null
解析
  1. 创建String实例
  2. 调用实力的方法
  3. 执行完方法立即销毁这个实例
  4. 体现了基本包装类型的性质,基本包装类型属于基本数据类型,包括Boolean, Number和String。

4.0.1+0.2为什么不等于0.3

0.1和0.2在转换成二进制后会无限循环,由于标准位数的限制后面多余的位数会被截掉,现在就已经出现了精度的损失,相加后因为浮点数小数位的限制而截断的二进制数字在转换为十进制就会造成错误

第二篇: JS数据类型之问——检测篇

1.typeof是否能正确判断类型?

对于原始/基本数据类型,除了null都可以调用typeof显示正确结果

typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'

对于引用数据类型,除了函数都会显示object

typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'
typeof function(){}//function

因此采用typeof判断对象数据类型是不合适的,采用instanceof更好,instanceof的原理是基于原型链的查询,只要处于原型链中,判断永远为true

const Person = function() {}
const p1 = new Person()
p1 instanceof Person // true

var str1 = 'hello world'
str1 instanceof String // false

var str2 = new String('hello world')
str2 instanceof String // true

2.手动实现instanceof的功能

核心:原型链的向查找

function myInstanceof(left,right){
    //基本数据类型直接返回false
    if(typeof left !== 'object' || left === null)return false;
    //getProtypeOf是Object对象自带的一个方法,能够拿到参数的原型对象
    let proto = Object.getProtypeOf(left)
    while(true){
        //查找到尽头,还没找到
        if(proto == null) return false;
        //找到相同的原型对象
        if(proto == right.prototype) return true;
        proto = Object.getPrototypeof(proto);
    }
}
console.log(myInstanceof("111", String)); //false
console.log(myInstanceof(new String("111"), String));//true

第三篇: JS数据类型之问——转换篇

1.[] == ![]结果是什么?为什么?

== 中,左右两边都需要转换为数字然后进行比较。

[]转换为数字为0。

![] 首先是转换为布尔值,由于[]作为一个引用类型转换为布尔值为true,因此![]为false,进而在转换成数字,变为0。0 == 0 , 结果为true

2. JS中类型转换有哪几种?

JS中,类型转换只有三种: - 转换成数字 - 转换成布尔值 - 转换成字符串

注意"Boolean 转字符串"这行结果指的是 true 转字符串的例子

数值类型转换.png

第四篇: 谈谈你对闭包的理解

什么是闭包?

闭包是指有权访问另一个函数作用域中变量的函数

一个函数有权访问另一个函数作用域中的变量

闭包用完后要释放资源:将引用变量指向null

  1. 闭包产生的原因

    首先要明白作用域链的概念在ES5中只存在两种作用域————全局作用域和函数作用域,当访问一个变量时,解释器会首先在当前作用域查找如果没有找到,就去父作用域找,直到找到该变量的标示符或者不在父作用域中,这就是作用域链,每一个子函数都会拷贝上级的作用域,形成一个作用域的链条。

    var a = 1;
    function f1() {
      var a = 2
      function f2() {
        var a = 3;
        console.log(a);//3
      }
    }
    

    f1的作用域指向有全局作用域(window)和它本身,而f2的作用域指向全局作用域(window)、f1和它本身。而且作用域是从最底层向上找,直到找到全局作用域window为止,如果全局还没有的话就会报错

闭包的本质

image-20220413201649438.png

  1. 闭包产生的本质就是,当前环境中存在指向父级作用域的引用。
function f1(){
    var a = 2
    function f2(){
        console.log(a)//2
    }
    return f2//产生闭包
}
var x = f1()
x()

这里x会拿到父级作用域中的变量,输出2。因为在当前环境中,含有对f2的引用,f2恰恰引用了window、f1和f2的作用域。因此f2可以访问到f1的作用域的变量。

  1. 闭包的本质,我们只需要让父级作用域的引用存在即可
var f3
function f1(){
    var a = 2;
    f3=function(){
        console.log(a)
    }
}
f1()
f3()

让f1执行,给f3赋值后,等于说现在f3拥有了window、f1和f3本身这几个作用域的访问权限,还是自底向上查找,最近是在f1中找到了a,因此输出2。

在这里是外面的变量f3存在着父级作用域的引用,因此产生了闭包,形式变了,本质没有改变。

闭包有哪些表现形式?

  1. 返回一个函数。
  2. 作为函数参数传递
var a = 1;
function foo(){
  var a = 2;
  function baz(){
    console.log(a);
  }
  bar(baz);
}
function bar(fn){
  // 这就是闭包
  fn();
}
// 输出2,而不是1
foo();

在定时器、事件监听、Ajax请求、跨窗口通信、Web Workers或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。

以下的闭包保存的仅仅是window和当前作用域。

// 定时器
setTimeout(function timeHandler(){
  console.log('111');
},100)

// 事件监听
$('#app').click(function(){
  console.log('DOM Listener');
})

(立即执行函数表达式)创建闭包, 保存了全局作用域window当前函数的作用域,因此可以全局的变量。

var a = 2;
(function IIFE(){
  // 输出2
  console.log(a);
})();

闭包地应用:防抖节流,获取函数中的局部变量

单例模式

单例模式,顾名思义就是只有一个实例,并且她自己负责创建自己的对象,这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。下面我们来看下有哪几种实现方式吧。

image-20220413202209269.png

如何解决下面的循环输出问题?
for(var i = 1; i <= 5; i ++){
  setTimeout(function timer(){
    console.log(i)
  }, 0)
}

因为setTimeout为宏任务,由于JS中单线程eventLoop机制,在主线程同步任务执行完后才去执行宏任务,因此循环结束后setTimeout中的回调才依次执行,但输出i的时候当前作用域没有,往上一级再找,发现了i,此时循环已经结束,i变成了6。因此会全部输出6

  1. 利用IIFE(立即执行函数表达式)当每次for循环时,把此时的i变量传递到定时器中
for(var i = 1;i <= 5;i++){
  (function(j){
    setTimeout(function timer(){
      console.log(j)
    }, 0)
  })(i)
}
  1. 给定时器传入第三个参数, 作为timer函数的第一个函数参数
for(var i=1;i<=5;i++){
  setTimeout(function timer(j){
    console.log(j)
  }, 0, i)
}
  1. 使用ES6中的let
for(let i = 1; i <= 5; i++){
  setTimeout(function timer(){
    console.log(i)
  },0)
}

let使js发生革命性的变化,让js由函数作用域变为了块级作用域。用let后作用域链不复存在,代码的作用域链以块级为单位

第五篇: 谈谈你对原型链的理解

原型对象和构造函数有什么关系

什么是构造函数?
  1. 构造函数模式中拥有了类和实例的概念,并且实例和实例之间是相互独立的

  2. 构造函数就是一个普通的函数,创建方式和普通函数一样

  3. 不同的是构造函数习惯上首字母大写。另外就是调用方式的不同,普通函数是直接调用,而构造函数需要使用new关键字来调用

function Person(name,age,gender){
    this.name=name;
    this.age=age;
    this.gender=gender;
    this.sayName=function(){
        alert(this.name)
    }
}
var per = new Person("孙悟空", 18, "男");
function Dog(name,age,gender){
    this.name=name;
    this.age=age;
    this.gender=gender;
}
 var dog = new Dog("旺财", 4, "雄")
    console.log(per);//当我们直接在页面中打印一个对象时,事件上是输出的对象的toString()方法的返回值
    console.log(dog);

构造函数结果.png

构造函数和实例原型之间的关系

构造函数和原型实例的关系.png

原型对象就相当于一个公共的区域,所有同一个类的实例都可以访问到这个原型对象,我们可以将对象中共有的内容,统一设置到原型对象中。

在javascript中,每当定义一个函数数据类型(普通函数、类)都会自带一个prototype属性,这个属性指向函数的原型对象

当函数使用new调用时,这个函数就成为了构造函数返回一个全新的实例对象,这个实例对象有一个proto属性,指向构造函数的原型对象

原型链图解.png

描述一下原型链

当我们访问对象的一个属性或方法时,它会先在对象自身中寻找,如果有则直接使用,如果没有则会去原型对象中寻找,如果找到则直接使用。如果没有则去原型的原型中寻找,直到找到Object对象的原型,Object对象的原型没有原型,如果在Object原型中依然没有找到,则返回undefined。

原型链原理.png

  • 对象的 hasOwnProperty() 来检查对象自身中是否含有该属性
  • 使用 in 检查对象中是否含有某个属性时,如果对象中没有但是原型链中有,也会返回 true
    function Person() {}
    Person.prototype.a = 123;
    Person.prototype.sayHello = function () {
      alert("hello");
    };
    var person = new Person()
    console.log(person.a)//123
    console.log(person.hasOwnProperty('a'));//false
    console.log('a'in person)//true
1.__proto__constructor

每一个对象数据类型(普通的对象、实例、prototype......)也天生自带一个属性__proto__,属性值是当前实例所属类的原型(prototype)。原型对象中有一个属性constructor, 它指向函数对象

image-20220423204005793.png

原型链.png

    function Person() {}
    var person = new Person()
    console.log(person.__proto__ === Person.prototype)//true
    console.log(Person.prototype.constructor===Person)//true
    //顺便学习一个ES5的方法,可以获得对象的原型
    console.log(Object.getPrototypeOf(person) === Person.prototype) // true
2.何为原型链

当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层Object为止。Object是JS中所有对象数据类型的基类(最顶层的类)在Object.prototype上没有__proto__这个属性。

console.log(Object.prototype.__proto__ === null) // true

原型链完整.png

第六篇 如何正确判this指向

一句话总结this指向的问题,那么就是:谁调用它,this就指向谁

  1. 全局环境中的this

    ​ 无论是否在严格模式下,浏览器环境,在全局执行环境中(在任何函数体外部)this都是指向全局对象,在浏览器中window对象就是全局对象。

    在node环境,在全局执行环境中(在任何函数体外部)this都是空对象

    console.log(this)//window
    
    var a=1;
    console.log(window.a)//1<==>console.log(this.a)
    
    
  2. 在函数环境中

    在函数内容,this指向取决于函数的调用方式,一般的具名函数this都是指向window

    function f(){
      "use strict"; //使用严格模式 ,使用严格模式时,输出undefined
       console.log(this)
    }
    f()//window
    

    可以用一些方法改变this的指向,call,apply,bind

    call,apply,bind的区别

    call,apply,bind理解:

    猫吃鱼,狗吃肉,奥特曼打小怪兽。

    有天狗想吃鱼了

    猫.吃鱼.call(狗,鱼)

    狗就吃到鱼了

    猫成精了,想打怪兽

    奥特曼.打小怪兽.call(猫,小怪兽)

    猫也可以打小怪兽了

    相同点:三个方法都有两个参数,第一个参数都是this对象,call和apply都是立即执行函数不需要调用,bind是等待执行需要调用

    call:call()直接把参数罗列就可以了obj.call(obj,1,2)

    call的使用场景

    1、对象的继承

    function superClass () {
        this.a = 1;
        this.print = function () {
            console.log(this.a);
        }
    }
    
    function subClass () {
        superClass.call(this);
        this.print();
    }
    
    subClass();
    // 1
    
    //subClass 通过 call 方法,继承了 superClass 的 print 方法和 a 变量
    

    2、借用方法

    let domNodes = Array.prototype.slice.call(document.getElementsByTagName("*"));
    

    apply:apply的第二个参数必须是一个包含多个参数的数组(或类数组对象)obj.apply(obj,[])

    apply的使用场景

    1、Math.max

    let max = Math.max.apply(null, array);
    

    2、实现两个数组合并

    let arr1 = [1, 2, 3];
    let arr2 = [4, 5, 6];
    
    Array.prototype.push.apply(arr1, arr2);
    console.log(arr1); // [1, 2, 3, 4, 5, 6]
    

    bind:bind()直接把参数罗列就可以了。obj.bind(obj,1,2)()

    bind的使用场景

    function add (a, b) {
        return a + b;
    }
    
    function sub (a, b) {
        return a - b;
    }
    
    add.bind(sub, 5, 3); // 这时,并不会返回 8
    add.bind(sub, 5, 3)(); // 调用后,返回 8
    
    var obj = {parent:'男'}
    var parent = '20';
    function child(obj){
        console.log(this.parent)
    }
    child();//20->就近原则this指向window
    child.call(obj)//改变this指向->男
    child.apply(obj)
    child.bind(obj)()//ƒ child(obj)console.log(this.parent)}需要调用
    

    bind方法,调用f.bind(obj)会创建一个与f具有相同函数体和作用域的函数,但是在这个新函数中,this将永久地被绑定到了bind的第一个参数,不管函数是怎样调用的。

  3. 箭头函数

    • 它会捕获其所在(即定义的位置)上下文的this值, 作为自己的this值,箭头函数中的this取决于该函数被创建时的环境。(理解的方法箭头函数中的this指向就近的父级元素)但是正常来说箭头函数中没有this,使用this不容易找出this的指向,在es6中尽量减少this的使用

    • call() / apply() / bind() 方法对于箭头函数来说只是传入参数,对它的 this 毫无影响

    var objProject = this;
    var foo = (() => this);
    console.log(foo());  // window
    console.log(objProject);  // window
    console.log(foo() === objProject ); // true
    
  4. 作为对象的方法调用

    当函数作为对象的方法被调用,this指向调用的该函数的对象

    var obj={
    	a:37,
    	fn:function(){
    		return this.a
    	}
    }
    console.log(obj.fn()); //this指向obj--->37
    
    
    //也可以先定义函数,然后将其附属到obj.fn
    var obj = {a:47}
    function foo(){
        return this.a
    }
    obj.fn = foo;
    console.log(obj); //{a:47,fn:f}
    console.log(obj.fn()); //  47
    
    
  5. 在对象原型链上某处定义的方法,this指向的是调用这个方法的对象

    var o ={
    	f:function(){
            return this.a+this.b
        }
    }
    var p = Object.create(o)//方法创建一个新对象,使用现有的对象来提供新创建的对象的proto。->返回值:在指定原型对象上添加新属性后的对象。
    p.a=1;
    p.b=4
    console.log(p.f())//对象p没有属于它自己的f属性,它的f属性继承自它的原型。虽然在对 f 的查找过程中,最终是在 o 中找到 f 属性的
    

    6.构造函数中的this

    构造函数中的this与被创建的新对象绑定

    当构造器返回的默认值是一个this引用的对象时,可以手动设置返回其他的对象,如果返回值不是对象,则返回this

    function c(){
    	this.a=37
    }
    var o = new c();
    console.log(this)//window
    console.log(o.a)//37
    
    function c2(){
        this.a=37
        return {a:38}
    }
    var b = new c2();
    console.log(b.a)//38
    
    function Super(age) {
        this.age = age;
        console.log(this);//Super { age: 'hello' }
    }
    
    let instance = new Super('26');
    

    7.定时器中的this指向全局对象window

    8.DOM 事件处理函数中的 this & 内联事件中的 this指向触发该事件的元素

第七篇:new的创建过程

  1. new构造函数在内存中创建一个空的对象

  2. 执行构造函数里面的代码给这个空的对象添加属性和方法

  3. this指向刚才创建的对象

  4. 返回这个对象(构造函数里面不需要return)

    function Person (name,age){
        this.name=name;
        this.age=age;
        console.log(this)//Person {name: "neo", age: "23"}
        //return this; 默认隐藏
    }
    var p1=new Person('neo',10)
    console.log(p1.name)
    //等同于
    function Person(name,age){
        this.name=name;
        this.age=age;
        console.log(this)//window
        return this
    }
    var p1 = new Object()
    p1 = Person('neo',10)
    console.log(p1.name);
    

第九篇 js防抖和节流

防抖

对于短时间内连续触发的事件,防抖的含义就是让某个时间期限内,事件函数只执行一次,如果在期限内再次点击,重新开始计时

当持续触发事件,一定时间内没有再触发事件函数才会执行一次,在这个时间内再次执行,重新开始计时

//实现
function debounce(fn,delay){
    let timer = null;
    return function(){
        if(timer){
            clearTimeout(timer)
        }
        timer=setTimeout(fn,delay)
    }
}
//用防抖实现滚动条
function showTop(){
    var scrollTop = document.body.scrollTop || document.documentElement.scrollTop
    console.log('滚动条位置:' + scrollTop);
}
window.onscroll = debounce(showTop,1000)
节流

在函数执行一次之后,该函数在指定的时间内不会再次执行,直到过了这个时间才会再次执行

//实现
function throttle(fn,delay){
    let valid = true
    return function(){
        if(!valid){
            return false
        }
        valid = false
        setTimeout(()=>{
            fn()
            valid = true
        },delay)
    }
}
// 以下照旧
function showTop  () {
    var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
  console.log('滚动条位置:' + scrollTop);
}
window.onscroll = throttle(showTop,1000) 

预解析

// 预解析
function fn(a, c) {
  console.log(a)
  var a = 123
  console.log(a)
  console.log(c)
  function a() {}
  if(false){
      var d = 678
  }
  console.log(d)
  console.log(b)
  var b = function () {}
  console.log(b)
  function c() {}
  console.log(c)
}
fn(1, 2)

image-20220405155637508.png 作用域的创建阶段就是预编译的阶段

预编译的时候做了那些事情

作用域分为全局作用域和函数作用域

ao对象做的事情

1.创建ao对象

2.形参和变量的声明

3.实参和形参相统一

4.找函数声明,会覆盖变量的声明

image-20220405160737830

this相关

var name = 222
var a={
    name:111,
	say:function(){
        console.log(this.name)
    }    
}
var fun = a.say
fun() //fun.call(window)
a.say()//a.say.call(a)
var b = {
    name:333,
    say:function(fun){
        fun()//fun.call(window)
    }
}
b.say(a.say)
b.say = a.say
b.say()//b.say.call(b)

在函数中直接使用

function get(count){
    console.log(content)
}
get('你好')
get.call(window,'您好') 
var person = {
    name:'张三'run:function(time){
        console.log(`${this.name}`在跑步 最多${time}min就不行了)
    }
}
person.run(30)
person.run.call(person,30)

箭头函数中的this

  1. 箭头函数的this是在定义函数的时候绑定,而不是在执行的时候绑定

  2. 箭头函数中,this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际上箭头函数根本没有自己的this,导致内部的this就是外层代码块的this,正因为他没有this,所以不能用作构造函数

  3. 箭头函数中this是定义在函数的时候绑定

    var x = 111
    var obj = {
        x:22,
        say:()=>{
            console.log(this.x)
        }
    }
    obj.say()//111 -->外层代码块的this箭头函数的this指向上一级
    
    
    this练习题
    var name = 222
    var a = {
        name:111
        say:function(){
            console.log(this.name)
        }
    }
    var fun = a.say
    fun() //222=>fun.call(window)
    a.say()//111=>a.say.call(a)
    var b= {
        name:333
        say:function(fun){
            fun()//222=>fun.call(window)
        }
    }
    b.say(a.say)
    b.say = a.say
    b.say()//333 =>b.say().call(b)
    

js的作用域

全局作用域
  1. 全局作用域在页面打开时被创建,页面关闭时被销毁
  2. 编写在script标签中的变量和函数,作用域为全局,在页面的任意位置都能访问到
  3. 在全局作用域中有全局对象window,代表一个浏览器窗口
  4. 全局作用域中声明的变量和函数会作为window对象的属性和方法保存
函数作用域

(1)调用函数时,函数作用域被创建,函数执行完毕,函数作用域被销毁

(2)每调用-次函数就会创建一 个新的函数作用域 ,他们之 间是相互独立的

(3)在函数作用域中 可以访问到全局作用域的变量,在函数外无法访问到函数作用域内的变量

(4)在函数作用域中访问变量、函数时,会先在自身作用域中寻找,若没有找到,则会到函数的上-级作用域中找,一直到全局作用域

作用域的深层次理解
执行期的上下文

1.当函数代码执行的前期会创建个执行期 上下文的内部对象 A0 (作用域)

2.这个内部的对象是预编译的时候创建出来的因为当函数被调用的时候会先进行预编译 在全局代码执行的前期会创建一个执行期的 上下文的对象GO

+这里有关j s的预编译也简单的提一 下

函数作用域预编译

1.创建ao对象AO{}

2.找形参和变量声明将变量和形参名当做A0对象的属性名值为undefined

3.实参形参相统一

4.在函数体里面找函数声明值赋予函数体

全局作用域的预编译
  1. 创建G0对象

  2. 找变量声明将变量名作为G0对象的属性名值是undef ined

  3. 找函数声明值赋予函数体

深拷贝和浅拷贝

浅拷贝

浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果是引用类型,拷贝的是内存地址,所以如果其中一个对象改变了这个地址,就会影响到另一个对象

深拷贝

深拷贝是将一个对象从内存中完整拷贝出一份,从堆内存中开辟出一个新的区域存放新对象,修改新对象不会影响原对象

赋值和深/浅拷贝的区别

赋值:当我们把一个对象赋值给一个新的变量的时候,赋的值其实是该对象在栈中的地址,而不是堆中的数据也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的

浅拷贝:重新在堆中创建内存,拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因共享同一块内存,会相互影响。

深拷贝:从堆内存中开辟一个新的区域存放新对象,对对象中的子对象进行递归拷贝,拷贝前后的两个对象互不影响。

//对象赋值
let obj1 = {
    name:'赋值',
    arr:[1,[2,3],4]
}
let obj2 = obj1
obj2.name='张三'
obj2.arr[1]=[5,6,7]
console.log('obj1',obj1) //obj1{name: '张三',arr:  [1,[5, 6, 7], 4]
console.log('obj2',obj2)//obj2{name: '张三',arr:  [1,[5, 6, 7], 4]
//浅拷贝
let obj1={
     name:'浅拷贝',
    arr:[1,[2,3],4]
}
let obj3 = shallowClone(obj1)
obj3.name='李四'
obj3.arr[1] = [5,6,7] ; // 新旧对象还是共享同一块内存
//浅拷贝的方法
function shallowClone(source){
    var target = {}
    for(var i in source){
        //hasOwnProperty这个方法会查找一个对象是否有某个属性,但是不会去查找它的原型链。
        if(source.hasOwnProperty(i)){
            target[i] = source[i]
        }
    }
    return target
}
console.log('obj1',obj1) //obj{name:'浅拷贝',arr:  [1,[5, 6, 7], 4]}
console.log('obj3',obj3) //obj{name: '李四',arr:  [1,[5, 6, 7], 4]}
//深拷贝
let obj1={
     name:'深拷贝',
    arr:[1,[2,3],4]
}
let obj4  = deepClone(obj1)
obj4.name='王五'
obj4.arr[1] = [5,6,7] //新对象跟原对象不共享内存
//深拷贝方法
function deepClone(obj){
    if(obj === null) return obj
    if(obj instanceof Date) return new Date(obj)
    if (obj instanceof RegExp) return new RegExp(obj);
    if (typeof obj !== "object") return obj;
    let cloneObj = new obj.constructor()
    for(let key in obj){
        if(obj.hasOwnProperty(key)){
            //实现一个递归拷贝
            cloneObj[key] = deepClone(obj[key])
        }
    }
    return cloneObj
}
console.log('obj1',obj1)//obj{name:'深拷贝',arr:  [1,[2,3], 4]}
console.log('obj4',obj4)// obj4 { name: '王五', arr: [ 1, [ 5, 6, 7 ], 4 ] }
浅拷贝的实现方式
1.Object.assign()

Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。

let obj1 = { person: {name: "kobe", age: 41},sports:'basketball' };
let obj2 = Object.assign({}, obj1);
obj2.person.name = "wade";
obj2.sports = 'football'
console.log(obj1); // { person: { name: 'wade', age: 41 }, sports: 'basketball' }

2.函数库lodash的_.clone方法

该函数库也有提供_.clone用来做 Shallow Copy,后面我们会再介绍利用这个库实现深拷贝。

var _ = require('lodash');
var obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
var obj2 = _.clone(obj1);
console.log(obj1.b.f === obj2.b.f);// true

3.展开运算符...

展开运算符是一个 es6 / es2015特性,它提供了一种非常方便的方式来执行浅拷贝,这与 Object.assign ()的功能相同。

let obj1 = { name: 'Kobe', address:{x:100,y:100}}
let obj2= {... obj1}
obj1.address.x = 200;
obj1.name = 'wade'
console.log('obj2',obj2) // obj2 { name: 'Kobe', address: { x: 200, y: 100 } }

4.Array.prototype.concat()
let arr = [1, 3, {
    username: 'kobe'
    }];
let arr2 = arr.concat();    
arr2[2].username = 'wade';
console.log(arr); //[ 1, 3, { username: 'wade' } ]

5.Array.prototype.slice()
let arr = [1, 3, {
    username: ' kobe'
    }];
let arr3 = arr.slice();
arr3[2].username = 'wade'
console.log(arr); // [ 1, 3, { username: 'wade' } ]
深拷贝的实现方式
1.JSON.parse(JSON.stringify())
let arr = [1, 3, {
    username: ' kobe'
}];
let arr4 = JSON.parse(JSON.stringify(arr));
arr4[2].username = 'duncan'; 
console.log(arr, arr4)

img

这也是利用JSON.stringify将对象转成JSON字符串,再用JSON.parse把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。

这种方法虽然可以实现数组或对象深拷贝,但不能处理函数和正则,因为这两者基于JSON.stringify和JSON.parse处理后,得到的正则就不再是正则(变为空对象),得到的函数就不再是函数(变为null)了。

比如下面的例子:

let arr = [1, 3, {
    username: ' kobe'
},function(){}];
let arr4 = JSON.parse(JSON.stringify(arr));
arr4[2].username = 'duncan'; 
console.log(arr, arr4)

img

2.函数库lodash的_.cloneDeep方法

该函数库也有提供_.cloneDeep用来做 Deep Copy

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

3.jQuery.extend()方法

jquery 有提供一個$.extend可以用来做 Deep Copy

$.extend(deepCopy, target, object1, [objectN])//第一个参数为true,就是深拷贝

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

es6新特性

1.变量声明:let和const

ES6推荐使用let声明局部变量,相比之前的var(无论声明在何处,都会被视为声明在函数的最顶部 )

//let 和 var声明的区别
var x = '全局变量',
    {
        let x = '局部变量',
        console.log(x); // 局部变量
    }
console.log(x); // 全局变量
var,let,const区别

1.var声明的变量会挂载在window上,而let和const声明的变量不会

2.var声明的变量存在变量提升,而let和const不存在变量提升

3.let和const声明形成块级作用域 if语句和 for语句里面的{ }也属于块作用域

4.同一作用域下let和const不能声明同名变量,var可以

5.暂存死区

var a = 100;

if(1){
    a = 10;
    //在当前块作用域中存在a使用let/const声明的情况下,给a赋值10时,只会在当前作用域找变量a,
    // 而这时,还未到声明时候,所以控制台Error:a is not defined
    let a = 1;
}

6.const

  • 一旦声明必须赋值,不能使用null占位
  • 声明后不能修改
  • 如果声明的是复合类型数据,可以修改其属性
2.模板字符串

在ES6之前,我们往往这么处理模板字符串: 通过“\”和“+”来构建模板

$("body").html("This demonstrates the output of HTML \
content to the page, including student's\
" + name + ", " + seatNumber + ", " + sex + " and so on.");
123

而对ES6来说

基本的字符串格式化。将表达式嵌入字符串中进行拼接。用${}来界定; ES6反引号(``)直接搞定;

$("body").html(`This demonstrates the output of HTML content to the page, 
including student's ${name}, ${seatNumber}, ${sex} and so on.`);
3.箭头函数
  • 不需要 function 关键字来创建函数
  • 省略 return 关键字
  • 继承当前上下文的 this 关键字
4.函数的参数默认值

在ES6之前,我们往往这样定义参数的默认值:

// ES6之前,当未传入参数时,text = 'default';
function printText(text) {
    text = text || 'default';
    console.log(text);
}

// ES6;
function printText(text = 'default') {
    console.log(text);
}
5.对象和数组解构/(展开运算符)
const user = { id: 10, name: 'Tom'}
const userWithPass = { ...user, password: 'Password!' }
6.symbol

表示独一无二的值,即每个symbol类型的值都不相同,特殊的是:不能使用new关键字调用它。

状态码有哪些

2xx成功状态码

2xx响应结果表示请求被正常处理了

204 No Content

200 (成功) 服务器已成功处理了请求。 通常,这表示服务器提供了请求的网页。 201 (已创建) 请求成功并且服务器创建了新的资源。 202 (已接受) 服务器已接受请求,但尚未处理。 203 (非授权信息) 服务器已成功处理了请求,但返回的信息可能来自另一来源。 204 (无内容) 服务器成功处理了请求,但没有返回任何内容。 205 (重置内容) 服务器成功处理了请求,但没有返回任何内容。 206 (部分内容) 服务器成功处理了部分 GET 请求。

3xx(Redirection重定向状态码)

3XX 响应结果表明浏览器需要执行某些特殊的处理以正确处理请求

301

永久性重定向

该状态码表示请求的资源已被分配了新的 URI,以后应使用资源现在所指的 URI。也就是说,如果已经把资源对应的 URI 保存为书签了,这时应该按 Location 首部字段提示的 URI 重新保存

302

​ 临时重定向

302 状态码代表的资源不是被永久移动,只是临时性质的。

303 (查看其他位置) 请求者应当对不同的位置使用单独的 GET 请求来检索响应时,服务器返回此代码。 304 (未修改) 自从上次请求后,请求的网页未修改过。 服务器返回此响应时,不会返回网页内容。 305 (使用代理) 请求者只能使用代理访问请求的网页。 如果服务器返回此响应,还表示请求者应使用代理。 307 (临时重定向) 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。

4xx客户端错误状态码

4XX 的响应结果表明客户端是发生错误的原因所在

400

该状态码表示请求报文中存在语法错误。当错误发生时,需修改请求的内容后再次发送请求。另外,浏览器会像 200 OK 一样对待该状态码

401

400 (错误请求) 服务器不理解请求的语法。(解决办法传参格式不正确) 401 (未授权) 请求要求身份验证。 对于需要登录的网页,服务器可能返回此响应。 403 (禁止) 服务器拒绝请求。 404 (未找到) 服务器找不到请求的网页。 405 (方法禁用) 禁用请求中指定的方法。 406 (不接受) 无法使用请求的内容特性响应请求的网页。 407 (需要代理授权) 此状态代码与 401(未授权)类似,但指定请求者应当授权使用代理。 408 (请求超时) 服务器等候请求时发生超时。 409 (冲突) 服务器在完成请求时发生冲突。 服务器必须在响应中包含有关冲突的信息。 410 (已删除) 如果请求的资源已永久删除,服务器就会返回此响应。 411 (需要有效长度) 服务器不接受不含有效内容长度标头字段的请求。 412 (未满足前提条件) 服务器未满足请求者在请求中设置的其中一个前提条件。 413 (请求实体过大) 服务器无法处理请求,因为请求实体过大,超出服务器的处理能力。 414 (请求的 URI 过长) 请求的 URI(通常为网址)过长,服务器无法处理。 415 (不支持的媒体类型) 请求的格式不受请求页面的支持。 416 (请求范围不符合要求) 如果页面无法提供请求的范围,则服务器会返回此状态代码。 417 (未满足期望值) 服务器未满足"期望"请求标头字段的要求

5.状态码 5xx(服务器错误状态码)

500 (服务器内部错误) 服务器遇到错误,无法完成请求。(解决办法传参数不正确) 501 (尚未实施) 服务器不具备完成请求的功能。 例如,服务器无法识别请求方法时可能会返回此代码。 502 (错误网关) 服务器作为网关或代理,从上游服务器收到无效响应。 503 (服务不可用) 服务器目前无法使用(由于超载或停机维护)。 通常,这只是暂时状态。 504 (网关超时) 服务器作为网关或代理,但是没有及时从上游服务器收到请求。 505 (HTTP 版本不受支持) 服务器不支持请求中所用的 HTTP 协议版本。

HTTP请求中get和post的区别是什么

差异

  1. GET请求资源数据,POST向服务器传递需要处理的数据
  2. GET是查询参数会在URL上显示,POST是请求体参数在RequestBody里面
  3. GET请求的资源会被浏览器缓存,POST不会被缓存

箭头函数与普通函数的区别

  1. 箭头函数是匿名函数,不能作为构造函数,不能使用new

  2. 箭头函数内没有arguments可以用展开运算符解决

  3. 箭头函数的this,始终指向父级上下文(箭头函数的this取决于定义位置父级的上下文,跟使用位置没关系,普通函数this指向调用的那个对象)

  4. 箭头函数不能通过call() 、 apply() 、bind()方法直接修改它的this指向。(call、aaply、bind会默认忽略第一个参数,但是可以正常传参)

  5. 箭头函数没有原型属性

页面全屏

设置全屏

document.documentElement.requestFullscreen

退出全屏

document.webkitExitFullscreen()

document.exitFullScreen

快速排序算法

/数据,左子序起始位置,结束位置
var quickSort = function(arr,left,right){
    //基准点位置(中间位置)
    var par = 0;
    left = typeof left !== 'number' ? 0 : left
    right = typeof right !== 'number' ? arr.length - 1 : right
    if(left < right){
        //创建基准点初始为0
        var pivot = left
        //索引记录
        var index = pivot + 1
        for(var i =  index; i <= right ; i++){
            if(arr[i] < arr[pivot]){
                swap(arr,i,index)
                index++
            }
        }
       swap(arr,pivot,index-1)
        par = index - 1
        //左
        quickSort(arr,left,par-1)
        //有
        quickSort(arr,par+1,right)
    }
}
//替换
function swap(arr,i,j){
    var temp = arr[i]
    arr[i] = arr[j]
    arr[j] = temp
}

改变this指向

call ,apply ,bind

apply参数接收的是一个数组,apply 的所有参数都必须放在一个数组里面传进去

call接收的是单向的数据,call 的参数是直接放进去的,第二第三第 n 个参数全都用逗号分隔,直接放到后面 obj.myFun.call(db,'成都', ... ,'string' )

obj.myFun.call(db,'成都','上海');     // 德玛 年龄 99  来自 成都去往上海
obj.myFun.apply(db,['成都','上海']);      // 德玛 年龄 99  来自 成都去往上海  
obj.myFun.bind(db,'成都','上海')();     
使用场景

js继承:原型链继承,构造函数继承 call实现

es5把伪数组转为数组 :

es6: Array(Array.from()) /[...arguments]

es5:Array.prototype.slice.call(arguments)

js数据类型检测类型:

array.isArray instanceof Object.toString.call({}/[])

js事件流

  1. 事件冒泡
  2. 事件捕获
  3. 事件委托

undefined和null的区别

undefined表示变量声明未被初始化

Null类型也只有一个值,即null。null用来表示尚未存在的对象,常用来表示函数企图返回一个不存在的对象。

判断数据类型的方法有哪些

  1. 利用typeof判断数据类型
  2. instanceof可以判断A是否为B的原型
  3. Object.prototype.toString.call()

多维数组扁平化

let arr = [1, [2, [3, 4]]]
const fn=(arr)=>{
   return arr.reduce((index,item)=>{
        return index.concat(item.length?fn(item):item)
    },[])
}
const res = fn(arr)
console.log(res);

转成树形结构

export function translateListToTreeData(list, pid) {
  const arr = []
  list.forEach(item => {
    if (item.pid === pid) {
      const children = translateListToTreeData(list, item.id)
      // console.log(children);
      if (children.length) {
        item.children = children
      }
      arr.push(item)
    }
  })
  // console.log(arr);
  return arr
}

js的运行机制EventLoop

js是单线程的运行机制

它分为两种任务同步任务和异步任务

同步任务:在主线程上排队进行的,按顺序执行

异步任务:不进入主线程,而进入event table执行,异步事件完成有结果后把结果回调函数放入“任务队列”(taskqueue),只有“任务队列”通知主线程某个异步任务可以执行了,该任务才会进入主线程执行

  1. js中的异步操作 – setTimeOut 和 setInterval(定时器) – ajax – promise – DOM事件
微任务和宏任务(异步任务)
  • 宏任务(macro-task):

    script(整体代码) setTimeout setInterval I/O UI交互事件 postMessage MessageChannel setImmediate UI rendering

  • 微任务(mincro-task):promise.then、promise.nextTick(node)

这段代码的执行顺序: 1、进入整体代码 2、setTimeout回调函数会被注册发放到宏任务列表中 3、new Promise立即执行,then发放到微任务中 4、主线程执行完毕,执行微任务列表 5、第一轮循环结束,从宏任务开始,执行setTimeout ![

](F:\code\开课吧\面试\image\image-20220423203335236.png)

总结:先执行微任务在执行宏任务

单线程的优缺点
  • 优点:系统稳定,不会产生严重的同步问题,比如一个线程操作DOM的增加另一个线程操作DOM的删除
  • 缺点:容易出现代码的阻塞

手写promise

const promise = new Promise((resolve,reject)=>{
    if(true){
        resolve()
    }else{
        reject()
    }
})
promise.then(data=>{
    console.log(data)
}).catch(err=>{
    console.log(err)
})

手写promise.all

promise.all = function(arr){
    //定义一个空数组,这个空数组里面是一些API
    let list = []
    //长度初始值为0
    let len = 0
    //失败的状态为false
    let flag = false
    return new promise((resolve,reject)=>{
        for(let i=0; i<arr.length;i++){
            //返回出来成功的数据
            arr[i] = data
            //返回出来的长度
            len++
            //进行一个判断查看是否请求成功
            len === arr.length&&resolve(list)
        },error=>{
            //失败的结果
            !flag&&reject(error)
            flag = true
        }
    })
}

对webpack的理解

打包模块化js的工具,在webpack里一切文件皆模块通过loader转换文件通过plugin注入钩子,输出由多个模块组合成的文件,可以把webpack看作是模块的打包机器,找到js模块,分析项目结构,以及其他浏览器不能直接运行拓展的语言,将其打包成合适的模块供浏览器使用

http

http是超文本传输协议,http是一个双向协议,在计算机中专门用来在两点之间传输数据的约定

http常见的字段

Host:客户端发起请求时,用来指定服务器的域名,将请求发往同一台服务器上的不同网站

Content-Length:服务器返回数据时会有,表明本次会员的数据长度

Connection 字段最常用于客户端要求服务器使用 TCP 持久连接,以便其他请求复用。

GET和POST的区别

Get 方法的含义是请求从服务器获取资源,这个资源可以是静态的文本、页面、图片视频等。

POST 方法则是相反操作,它向 URI 指定的资源提交数据,数据就放在报文的 body 里。

HTTP特性

HTTP最突出的优点简单灵活易于扩展应用广泛和跨平台

HTTP协议里的各类请求方法、URI/URL、状态码、头字段等每个组成要求都没有被固定死,都允许开发人员自定义和扩充

http和https的区别
  1. HTTP 是超文本传输协议,信息是明文传输,存在安全风险的问题。HTTPS 则解决 HTTP 不安全的缺陷,在 TCP 和 HTTP 网络层之间加入了 SSL/TLS 安全协议,使得报文能够加密传输。
  2. HTTP 连接建立相对简单, TCP 三次握手之后便可进行 HTTP 的报文传输。而 HTTPS 在 TCP 三次握手之后,还需进行 SSL/TLS 的握手过程,才可进入加密报文传输。
  3. HTTP 的端口号是 80,HTTPS 的端口号是 443。
  4. HTTPS 协议需要向 CA(证书权威机构)申请数字证书,来保证服务器的身份是可信的。
axios的请求类型
  1. get请求一般用于获取数据
  2. post请求主要用于提交表单和上传文件
  3. put请求是对数据的全部更新
  4. patch是局部更新
  5. delete是删除请求
npm是怎么打包的

1.打开.env.development将接口地址设置成线上访问 注释本地VUE_APP_BASE_API = ‘/’

2:打开.env.local文件和.env.production文件 同上

3:打开vue.config.js 注释掉proxy proxy代理在打包的时候需要注释,平时开发需要开着

undefined和null转换结果
  1. typeof(undefined) 'undefined'
  2. typeof(null) 'object'

数据是一次性加载到前端的还是异步加载

同步模式,又称阻塞模式,会阻止浏览器的后续处理,停止了后续的解析,因此停止了后续的文件加载(如图像)、渲染、代码执行。

异步加载又叫非阻塞,浏览器在下载执行js同时,还会继续进行后续页面的处理。

es6新特性

1、 变量声明关键字let、const

let 是在代码块内有效,var 是在全局范围内有效;

let 只能声明一次,无法进行赋值

var 可以声明多次;

let 不存在变量提升,var 会变量提升:

const 用来声明常量,块级作用域,一旦执行无法取消

2、 许函数参数设置默认值

function testAdd(x, y = 10) {
  // 当y不给值或者为undefined时值取10
  return x + y;
}

3、 箭头函数(Arrow Function)

类似于匿名函数,但没有自己的 this,不适合定义对象方法;

如果只是单个语句,则可省略掉return和花括号(但保留一般更好一些)

例:z = (x, y) => { return x * y };

4、 class 关键字定义类

class 的本质是 function,它可以看作一个语法糖,让对象原型的写法更加清晰、更像面向对象编程的语法;

(1) 声明

class Example {
    constructor(a) {
        this.a = a;
    }
}

5、 解构赋值

是一种针对数组或者对象进行模式匹配,然后对其中的变量进行赋值

你知道哪些JS数组的API?

方法作用是否影响原数
push在数组后添加元素,返回长度
pop删除数组最后一项
shift删除数组第一项返回被删项
unshift数组开头添加元素,返回长度
reserve反转数组,返回数组
sort排序数组,返回数组
splice截取数组,返回被截取部分
join将数组变字符串,返回字符串
concat连接数组
map相同规则处理数组项,返回新数组
forEach遍历数组
filter过滤数组项,返回符合条件的数组
every每一项符合规则才返回true
some只要有一项符合规则就返回true
reduce接受上一个return和数组下一项
flat数组扁平化
slice截取数组,返回被截取区间

数组里面有10万个数据,取第一个元素和第10万个元素的时间相差多少

JavaScript 没有真正意义上的数组,所有的数组其实是对象,其“索引”看起来是数字,其实会被转换成字符串,作为属性名(对象的 key)来使用。所以无论是取第 1 个还是取第 10 万个元素,都是用 key 精确查找哈希表的过程,其消耗时间大致相同。得出结论:消耗时间几乎一致,差异可以忽略不计

项目性能优化

  • 减少 HTTP 请求数
  • 减少 DNS 查询
  • 使用 CDN
  • 避免重定向
  • 图片懒加载
  • 减少 DOM 元素数量
  • 减少 DOM 操作
  • 使用外部 JavaScript 和 CSS
  • 压缩 JavaScript、CSS、字体、图片等
  • 优化 CSS Sprite
  • 使用 iconfont
  • 多域名分发划分内容到不同域名
  • 尽量减少 iframe 使用
  • 避免图片 src 为空
  • 把样式表放在 link 中
  • 把 JavaScript 放在页面底部

null和undefined的异同点

相同点:

  1. 都是空类型
  2. 转布尔值都是false,都是假值
  3. null == undefined为true

不同点

  1. typeof前者为Object后者为undefined
  2. null转数字为0,undefined转数字为NaN
  3. null === undefined为false

创建一个对象的方式有哪几种?

  • new Object
const obj = new Object()
obj.name = 'Sunshine_Lin'
  • 字面量
const obj = { name: 'Sunshin_Lin' }
  • 工厂模式
function createObj(name) {
  const obj = new Object()
  obj.name = name
  return obj
}
const obj = createObj('Sunshine_Lin')
  • 构造函数
function Person(name) {
  this.name = name
}
const person = new Person('Sunshine_Lin')

数组的常用方法有哪些?

方法作用是否影响原数组
push在数组后添加元素,返回长度
pop删除数组最后一项,返回被删项
shift删除数组第一项,返回被删项
unshift数组开头添加元素,返回长度
reserve反转数组,返回数组
sort排序数组,返回数组
splice截取数组,返回被截取部分
join将数组变字符串,返回字符串
concat连接数组
map相同规则处理数组项,返回新数组
forEach遍历数组
filter过滤数组项,返回符合条件的数组
every每一项符合规则才返回true
some只要有一项符合规则就返回true
reduce接受上一个return和数组下一项
flat数组扁平化
slice截取数组,返回被截取区间

Math的常用方法有哪些?

方法作用
Math.max(...arr)取arr中的最大值
Math.min(...arr)取arr中的最小值
Math.ceil(小数)小数向上取整
Math.floor(小数)小数向下取整
Math.round(小数)小数四舍五入
Math.sqrt(num)对num进行开方
Math.pow(num, m)对num取m次幂
Math.random() * num取0-num的随机数
 const list = [
      {
        name: '张三',
        age: 18,
        height: 180
      },
      {
        name: '李四',
        age: 20,
        height: 170
      },
      {
        name: '王五',
        age: 28,
        height: 180
      }
    ]

    // const targetList = [
    //   ['张三', 18, 180],
    //   ['李四', 20, 170],
    //   ['王五', 28, 180]
    // ]

    function transList(list, delay) {
      let newArr = []
      list.forEach(item => {
        let arr = []
        if (delay) {
          delay.forEach(k => {
            arr.push(item[k])
          })
        } else {
          for (var i in item) {
            arr.push(item[i])
          }

        }
        newArr.push(arr)
      })
      return newArr
    }

    console.log(transList(list, ['name', 'age']))
/* 
  编写一个函数transList 输入为list,输出为targetList
/*
  优化这个函数可以自由的决定当前想要哪些字段
  transList(list,['name']) 返回值为: 
  const targetList = [
    ['张三'],
    ['李四'],
    ['王五'],
  ]
  transList(list,['name','age']) 返回值为: 
  const targetList = [
    ['张三', 18],
    ['李四', 20],
    ['王五', 28],
  ]
*/