web前端面试题-基础篇总结超全超详细

文章介绍了JavaScript中的字符串、数组和对象的常用方法,如数组的push、pop、join、slice等,以及对象的原型链、数据类型检测方法如typeof、instanceof、Object.prototype.toString.call。接着讨论了ES6的新数据类型BigInt和Symbol,以及Set和Map的使用。此外,文章还探讨了深拷贝和浅拷贝的概念、应用场景,以及宏任务和微任务在JavaScript事件循环中的作用。最后,提到了递归的概念和应用场景,以及防抖和节流在性能优化中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

JavaScript常见字符串方法、数组方法、对象方法

一、数组常用方法汇总

二、关于对象的问题

三丶字符串的方法

js检测数据类型的方法 

一.JS中的数据类型:

二、检测数据类型的方法:

js原型链

ES6新的数据类型和应用场景

深拷贝、浅拷贝和应用场景

2.浅拷贝和深拷贝的适用场景2.1 浅拷贝和深拷贝的前端应用

宏任务和微任务

什么是递归?应用场景

什么是递归?

递归的概念

递归的步骤

经典案例 1: 求和

经典案例 2: 斐波拉契数列

经典案例 3: 爬楼梯

经典案例 4: 深拷贝

经典案例 5:递归组件

防抖和节流?应用场景

JavaScript常见字符串方法、数组方法、对象方法

一、数组常用方法汇总


1.转成字符串
把数组转换为字符串:JavaScript 方法 toString() 把数组转换为数组值(逗号分隔)的字符串
join() 方法也可将所有数组元素结合为一个字符串。
2.增删
pop() 方法从数组中删除最后一个元素:
push() 方法(在数组结尾处)向数组添加一个新的元素:
shift() 方法会删除首个数组元素,并把所有其他元素“位移”到更低的索引。shift() 方法返回被“位移出”的字符串:
unshift() 方法(在开头)向数组添加新元素,并“反向位移”旧元素unshift() 方法返回新数组的长度。
length 属性提供了向数组追加新元素的简易方法:var fruits = ["Banana", "Orange", "Apple", "Mango"]; fruits[fruits.length] = "Kiwi"; // 向 fruits 追加 "Kiwi"
既然 JavaScript 数组属于对象,其中的元素就可以使用 JavaScript delete 运算符来删除:var fruits = ["Banana", "Orange", "Apple", "Mango"]; delete fruits[0]; // 把 fruits 中的首个元素改为 undefined
使用 delete 会在数组留下未定义的空洞。请使用 pop() 或 shift() 取而代之。

3.拼接数组
splice() 方法可用于向数组添加新项:var fruits = ["Banana", "Orange", "Apple", "Mango"]; fruits.splice(2, 0, "Lemon", "Kiwi");
第一个参数(2)定义了应添加新元素的位置(拼接)。
第二个参数(0)定义应删除多少元素。
其余参数(“Lemon”,“Kiwi”)定义要添加的新元素。

使用 splice() 来删除元素
通过聪明的参数设定,您能够使用 splice() 在数组中不留“空洞”的情况下移除元素:

var fruits = ["Banana", "Orange", "Apple", "Mango"];
fruits.splice(0, 1);        // 删除 fruits 中的第一个元素
//第一个参数(0)定义新元素应该被添加(接入)的位置。
//第二个参数(1)定义应该删除多个元素。
//其余参数被省略。没有新元素将被添加。



4.合并(连接)数组
concat() 方法通过合并(连接)现有数组来创建一个新数组:
concat() 方法不会更改现有数组。它总是返回一个新数组。
concat() 方法可以使用任意数量的数组参数:

var arr1 = ["Cecilie", "Lone"];
var arr2 = ["Emil", "Tobias", "Linus"];
var arr3 = ["Robin", "Morgan"];
var myChildren = arr1.concat(arr2, arr3);   // 将arr1、arr2 与 arr3 连接在一起
//concat() 方法也可以将值作为参数:
var arr1 = ["Cecilie", "Lone"];
var myChildren = arr1.concat(["Emil", "Tobias", "Linus"]); 



5.裁剪数组
slice() 方法用数组的某个片段切出新数组。
slice() 方法创建新数组。它不会从源数组中删除任何元素。
slice() 可接受两个参数,比如 (1, 3),该方法会从开始参数选取元素,直到结束参数(不包括)为止。
如果结束参数被省略,比如第一个例子,则 slice() 会切出数组的剩余部分。
 

var fruits = ["Banana", "Orange", "Lemon", "Apple", "Mango"];
var citrus = fruits.slice(3); 

var fruits = ["Banana", "Orange", "Lemon", "Apple", "Mango"];
var citrus = fruits.slice(1, 3); 

var fruits = ["Banana", "Orange", "Lemon", "Apple", "Mango"];
var citrus = fruits.slice(2); 

二、关于对象的问题


(一) 项目遇到的方法:
将对象直接拷贝到数组可以使用扩展运算符
form.applyForm.push({ …serviceInfo })

将数组拷贝到对象中,并且将数组转化成对象
this.serviceInfo = { …applyForm[index] } 这里的applyForm就是一个数组,index表示索引值

将对象值清空
Object.keys(serviceInfo).map(item => { serviceInfo[item] = ’ '; })

添加和删除属性
1.为对象添加动态属性
obj.userName = “admin”;
obj.passWord = “123456”;
2.移除属性
delete obj.passWord;
console.log(obj);

(一) 最原始的方法:
1.原型链上方法:

//1. hasOwnProperty():返回一个布尔值,指示对象自身属性中是否具有指定的属性(也就是,是否有指定的键)
let object1 = new Object();
object1.property1 = 42;
object1.hasOwnProperty('property1'); // true
object1.hasOwnProperty('toString'); // false
//2. isPrototypeOf():用于测试一个对象是否存在于另一个对象的原型链上
function Foo() {}
function Bar() {}
function Baz() {}
Bar.prototype = Object.create(Foo.prototype);
Baz.prototype = Object.create(Bar.prototype);
let baz = new Baz();
Baz.prototype.isPrototypeOf(baz); // true
Bar.prototype.isPrototypeOf(baz); // true
Foo.prototype.isPrototypeOf(baz); // true
Object.prototype.isPrototypeOf(baz); // true

2.常用的对象方法

1.toString(): 返回一个表示对象的字符串

let o = new Object();
o.toString(); // '[objdec object]' 当对象需要调用toString()方法时会被自动调用.

2.valueOf(): 返回指定对象的原始值

let b = {name:'小明', age:18}
b.valueOf() //{name:'小明', age:18}

3.Object.assign(): 用于将所有可枚举性的值从一个或多个源对象复制到目标对象。它将返回目标对象

let target = { a: 1, b: 2 };
let source = { b: 4, c: 5 };

let returnedTarget = Object.assign(target, source);

target; // { a: 1, b: 4, c: 5 }
returnedTarget; // { a: 1, b: 4, c: 5 }

4.Object.create():创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。即创建一个以指定的对象为原型的子对象。
5.Object.setPrototypeOf():设置一个指定的对象的原型 ( 即, 内部[[Prototype]]属性)到另一个对象或null。
6.Object.getPrototypeOf():返回指定对象的原型(内部[[Prototype]]属性的值)。
7.Object.defineProperties():直接在一个对象上定义新的属性或修改现有属性,并返回该对象。

let obj = {};
Object.defineProperties(obj, {
  'property1': {
    value: true,
    writable: true
  },
  'property2': {
    value: 'Hello',
    writable: false
  }
});

obj.property1; // true
obj.property2; // 'Hello'


三丶字符串的方法


charAt( ) 方法从一个字符串中返回某个下标上的字符
concat( ) 方法将一个或多个字符串与原字符串连接合并,形成一个新的字符串并返回。

search( ) 获取某个字符或者字符串片段首次出现的位置
match( ) 检索字符串或者正则表达式
replace( ) 替换
replace(参数 1,参数 2); 参数 1:替换谁 参数 2:替换值
split( 参数 1,参数 2 ) 字符串切割,切割后返回数组

slice( ) 和 substring( ) ( 开始位置,结束位置 ) 获取两个索引值之间的字符串片段 从下标 2 开始截取,到下标 4 结束,但不包含 4
substr ( 开始位置,截取的长度 ) 获取两个索引值之间的字符串片段

indexOf( ) 获取某个字符或者字符串首次出现的位置,找到则返回索引值,找不到则返回-1
lastIndexOf( ) 获取某个字符或者字符串最后出现的位置,找到则返回索引值,找不到则返回-1

js检测数据类型的方法 

一.JS中的数据类型:


1) 简单类型:String、Number、Boolean、Undefined、Null、Symbol
2)引用(复杂)类型:Object

二、检测数据类型的方法:


1、typeof 检测一些基本的数据类型
语法:typeof 后面加不加括号都是可以用的
注意:正则、{}、[]、null输出结果为object


    console.log(typeof /\d/);//object
    console.log(typeof {});//object
    console.log(typeof []);//object
    console.log(typeof (null));//object
    console.log(typeof 123);//number
    console.log(typeof true);//boolean
    console.log(typeof function () {});//function
    console.log(typeof (undefined));//undefined


2、A  instanceof   B检测当前实例是否隶属于某各类
双目运算符 a instanceof b ,判断a的构造器是否为b,返回值为布尔值

    function b(){}
    let a = new b;
    console.log(a instanceof b);//true
    console.log(b instanceof Object);//true
    let arr = [1,2,3,4];
    console.log(arr instanceof Array);//true


3、constructor构造函数
语法:实例.constructor
        对象的原型链下(构造函数的原型下)有一个属性,叫constructor
缺点:**constructor并不可靠,容易被修改(只有参考价值)。即使被修改了,也不会影响代码的正常运行。正常开发的时候,constructor不小心被修改了,为了方便维护,和开发,可以手动更正constructor的指向。

 
    

function a() {}
    let a = new b;
    console.log(a.constructor.name);//a
    console.log(b.constructor);//Function(){}
    console.log(Function.constructor);//Function(){}


4、hasOwnporperty 检测当前属性是否为对象的私有属性
语法: obj.hasOwnporperty(“属性名(K值)”)

例子:
  

  let obj = {
    name:"lxw"
    };
    console.log(obj.hasOwnProperty('name'));//true
    console.log(obj.hasOwnProperty('lxw'));//false


 5、is Array 判断是否为数组
 
 

   console.log(Array.isArray([]));//true
    console.log(Array.isArray(new Array()));//true


6、valueOf
可以看到数据最本质内容(原始值)

例子:
  

  let a = "12345";
    let b= new String('12345');
    console.log(typeof a);//string
    console.log(typeof b);//object
    console.log(a == b);//true  (typeof检测出来是对象居然和一个数组相等,显然b并不是一个真的对象。)
    //此时看看 valueOf()的表现
    console.log(b.valueOf());//12345  拿到b的原始值是 字符串的12345
    console.log(typeof b.valueOf())//string  这才是 b的真正的数据类型


1.Math是个对象,不是类。类才有prototype(原型)。也就是说类都有原型:prototype,对象都有原型链:proto,函数既是类,也是对象,也是函数。
2.如何把对象转换成字符串?
 对象下的toString方法是检测数据类型的,而不是用来转化成字符串的,这时可以用JSON的方法来转化
 

例子:
 

   let obj={name:'王前'}
    obj.toString()//=>"[object Object]"
    
    
    JSON.stringify({name:'王前'})//"{"name":"王前"}"


7、Object.portotype.toString (最好的)
语法:Object.prototype.toString.call([value])
        获取Object.portotype上的toString方法,让方法的this变为需要检测的数据类型值,并且让这个方法执行

在Number、String、Boolean、Array、Function、RegExp…这些类的原型上都有一个toString方法:这个方法就是把本身的值转化为字符串

例子:

    (123).toString()//'123'
    (true).toString()//'true'
    [12,23].toString()//'12.23'
 
...


在Object这个类的原型上也有一个方法toString,但是这个方法并不是把值转换成字符串,而是返回当前值得所属类详细信息,固定结构:’[object 所属的类]'

调取的正是Object.prototype.toString方法obj.toString()
 首先执行Object.prototype.tostring方法,这个方法中的this就是我们操作的数据值obj


总结:Object.prototype.toString执行的时候返回当前方法中的this的所属类信息,也就是,我想知道谁的所属类信息,我们就把这个toString方法执行,并且让this变为我们检测的这个数据值,那么方法返回的结果就是当前检测这个值得所属类信息 

    

Object.prototype.toString.call(12)//[object Number]
 
    Object.prototype.toString.call(true)//[object Boolean]
      ...                                  //"[object Number]"
                                        //"[object String]"
                                        //"[object Object]"
                                        //"[object Function]"
                                        //"[object Undefined]"
                                        //"[object Null]"
                                        //"[object RegExp]"
                                        //"[object Boolean]"

js原型链

什么是原型链?
原型链通俗易懂的理解就是可以把它想象成一个链条,互相连接构成一整串链子!
而原型链中就是实例对象和原型对象之间的链接。每个函数都有一个prototype属性,这个prototype属性就是我们的原型对象,我们拿这个函数通过new构造函数创建出来的实例对象,这个实例对象自己会有一个指针(_proto_)指向他的构造函数的原型对象!这样构造函数和实例对象之间就通过( _proto_ )连接在一起形成了一条链子。

废话不多说,直接上图:

为什么要使用原型链呢?
1.为了实现继承,简化代码,实现代码重用!
2.只要是这个链条上的内容,都可以被访问和使用到!

使用原型链有什么作用?
继承
prototype用来实现基于原型的继承与属性的共享
避免了代码冗余,公用的属性和方法,可以放到原型对象中,这样,通过该构造函数实例化的所有对象都可以使用该对象的构造函数中的属性和方法!
减少了内存占用
原型链的特点
就近原则,当我们要使用一个值时,程序会优先查找离自己最近的,也就是本身有没有,如果自己没有,他就会沿着原型链向上查找,如果还没有找到,它还会沿着原型链继续向上查找,找到到达Object
引用类型,当我们使用或者修改原型链上的值时,其实使用的是同一个值!
JS中每个函数都存在原型对象属性prototype。并且所有函数的默认原型都是Object的实例。
每个继承父函数的实例对象都包含一个内部属性_proto_。该属性包含一个指针,指向父函数的prototype。若父函数的原型对象的_proto_属性为再上一层函数。在此过程中就形成了原型链。
__proto__和prototype的区别
_proto_ :是实例对象指向原型对象的指针,隐式原型,是每个对象都会有的一个属性。
prototype:是构造函数的原型对象,显式原型,只有函数才会有。
 

ES6新的数据类型和应用场景

一、BigInt
BigInt是ES10新加的一种JavaScript数据类型,用来表示表示大于 2^53 - 1 的整数,2^53 - 1是ES10之前,JavaScript所能表示最大的数字

const bigNum = BigInt(1728371927189372189739217)
console.log(typeof bigNum) // bigint


二、Symbol
Symbol是ES6中新增的一种基本数据类型,它是一个函数,会返回一个Symbol类型的值,每一个Symbol函数返回的值都是唯一的,它们可以被作为对象属性的标识符。

typeof Symbol(); // Symbol
基本用法:使用Symbol值作为对象的key

   let name=Symbol() //声明一个变量,数据类型为symbol
   let obj={
    [name]:"hsq"
   }
console.log(obj.name);// undefined
console.log(obj[name]);// hsq


注意:当我们使用变量去定义一个对象key时需要使用[ ]包裹着,否则就会被自动转化成string类型,因为属性使用变量进行定义,所以获取属性值需要使用obj[key]这种方式获取

但是Symbol作为属性的属性不会被枚举出来

 let name=Symbol() //声明一个变量,数据类型为symbol
   let obj={
    [name]:"hsq",
    age:22
   }
   console.log(Object.keys(obj)) // [ 'age' ]
   for(const key in obj) {
  console.log(key) // age
  }
   console.log(JSON.stringify(obj)) // {"age":22}


获取Symbol属性

  let name=Symbol('name') //声明一个变量,数据类型为symbol
   let obj={
    [name]:"hsq",
    age:22
   }
// 方法一
console.log(Object.getOwnPropertySymbols(obj)) // [ Symbol(name) ]
// 方法二
console.log(Reflect.ownKeys(obj)) // [ 'name', 'age', Symbol(name) ]
应用场景:使用Symbol定义类的私有属性

   class Login {
  constructor(username, password) {
    const PASSWORD = Symbol()
    this.username = username
    this[PASSWORD] = password
  }
  checkPassword(pwd) { return this[PASSWORD] === pwd }
}
 
const login = new Login('123456', 'hahah')
 
console.log(login.PASSWORD) // undefined
console.log(login[PASSWORD]) // 报错


 三、set(集合)
Set 对象(集合)类似于数组,且成员的值都是唯一的(通俗的理解:不能出现相同的元素)

常用语法

// 可不传数组
const set1 = new Set()
set1.add(1)
set1.add(2)
console.log(set1) // Set(2) { 1, 2 }
 
// 也可传数组
const set2 = new Set([1, 2, 3])
// 增加元素 使用 add
set2.add(4)
set2.add('林三心')
console.log(set2) // Set(5) { 1, 2, 3, 4, '林三心' }
// 是否含有某个元素 使用 has
console.log(set2.has(2)) // true
// 查看长度 使用 size
console.log(set2.size) // 5
// 删除元素 使用 delete
set2.delete(2)
console.log(set2) // Set(4) { 1, 3, 4, '林三心' }

不重复性 


 

// 两个对象都是不同的指针,所以没法去重
const set1 = new Set([1, {name: '林三心'}, 2, {name: '林三心'}])
console.log(set1) // Set(4) { 1, { name: '林三心' }, 2, { name: '林三心' } }
 
 
// 如果是两个对象是同一指针,则能去重
const obj = {name: '林三心'}
const set2 = new Set([1, obj, 2, obj])
console.log(set2) // Set(3) { 1, { name: '林三心' }, 2 }
 
咱们都知道 NaN !== NaN,NaN是自身不等于自身的,但是在Set中他还是会被去重
const set = new Set([1, NaN, 1, NaN])
console.log(set) // Set(2) { 1, NaN }

最常用来去重使用,去重方法有很多但是都没有它运行的快。

const arr = [1, 2, 3, 4, 4, 5, 5, 66, 9, 1]
 
// Set可利用扩展运算符转为数组哦
const newArr = [...new Set(arr)]
console.log(newArr) // [1,  2, 3, 4, 5, 66, 9]


四、map
Map 是一组键值对的结构,和 js对象类似。

基本用法
 

//初始化`Map`需要一个二维数组(请看 Map 数据结构),或者直接初始化一个空`Map` 
let map = new Map();
 
//添加key和value值
map.set('Amy','女')
map.set('liuQi','男')
 
//是否存在key,存在返回true,反之为false
map.has('Amy') //true
map.has('amy') //false
 
//根据key获取value
map.get('Amy') //女
 
//删除 key为Amy的value
map.delete('Amy')
map.get('Amy') //undefined  删除成功

Map对比object最大的好处就是,key不受类型限制

Map对比object最大的好处就是,key不受类型限制

// 定义map
const map1 = new Map()
// 新增键值对 使用 set(key, value)
map1.set(true, 1)
map1.set(1, 2)
map1.set('哈哈', '嘻嘻嘻')
console.log(map1) // Map(3) { true => 1, 1 => 2, '哈哈' => '嘻嘻嘻' }
// 判断map是否含有某个key 使用 has(key)
console.log(map1.has('哈哈')) // true
// 获取map中某个key对应的value 使用 get(key)
console.log(map1.get(true)) // 1
// 删除map中某个键值对 使用 delete(key)
map1.delete('哈哈')
console.log(map1) // Map(2) { true => 1, 1 => 2 }
 
// 定义map,也可传入键值对数组集合
const map2 = new Map([[true, 1], [1, 2], ['哈哈', '嘻嘻嘻']])
console.log(map2) // Map(3) { true => 1, 1 => 2, '哈哈' => '嘻嘻嘻' }

深拷贝、浅拷贝和应用场景

前言:在日常的开发中,我们常常遇到一些我们不懂的知识,比如我最近看到一个Object.assign() ,就不太清楚其究竟代表着啥意思,因而在查阅资料后,得知其是前端浅拷贝的一种方式。以前也有听说过深拷贝,但不太清楚其代表的意思。因此,今天借此篇博文,来看看什么是浅拷贝,什么又是深拷贝,它们又是如何在开发中运用的。

1,浅拷贝和深拷贝的概念
1.1 浅拷贝和深拷贝
浅拷贝是指拷贝对象时仅仅拷贝对象本身(包括对象中的基本变量),而不拷贝对象包含的引用指向的对象。深拷贝不仅拷贝对象本身,而且拷贝对象包含的引用指向的所有对象。举例来说更加清楚:对象A1中包含对B1的引用,B1中包含对C1的引用。浅拷贝A1得到A2,A2 中依然包含对B1的引用,B1中依然包含对C1的引用。深拷贝则是对浅拷贝的递归,深拷贝A1得到A2,A2中包含对B2(B1的copy)的引用,B2 中包含对C2(C1的copy)的引用。

为了便于对于浅拷贝和深拷贝的记忆,我们引入引用拷贝和对象拷贝来加深其理解:

1.2 引用拷贝和对象拷贝
引用拷贝:创建一个指向对象的引用变量的拷贝,具体代码如下:

public class QuoteCopy {
    public static void main(String[] args) {
        Teacher teacher = new Teacher("riemann", 28);
        Teacher otherTeacher = teacher;
        System.out.println(teacher);
        System.out.println(otherTeacher);
    }
}
 
class Teacher {
    private String name;
    private int age;
    public Teacher(String name, int age) {
        this.name = name;
        this.age = age;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public int getAge() {
        return age;
    }
 
    public void setAge(int age) {
        this.age = age;
    }
}


输出结果:

com.test.Teacher@28a418fc
com.test.Teacher@28a418fc
结果分析:由输出结果可以看出,它们的地址值是相同的,那么它们肯定是同一个对象。teacher和otherTeacher的只是引用而已,他们都指向了一个相同的对象Teacher(“riemann”,28)。 这就叫做引用拷贝。

对象拷贝:创建对象本身的一个副本,代码如下:

public class ObjectCopy {
    public static void main(String[] args) throws CloneNotSupportedException {
        Teacher teacher = new Teacher("riemann", 28);
        Teacher otherTeacher = (Teacher) teacher.clone();
        System.out.println(teacher);
        System.out.println(otherTeacher);
    }
}
 
class Teacher implements Cloneable {
    private String name;
    private int age;public Teacher(String name, int age) {
    this.name = name;
    this.age = age;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public int getAge() {
        return age;
    }
 
    public void setAge(int age) {
        this.age = age;
    }
 
    public Object clone() throws CloneNotSupportedException {
        Object object = super.clone();
        return object;
    }
}


输出结果:

com.test.Teacher@28a418fc
com.test.Teacher@5305068a
结果分析:由输出结果可以看出,它们的地址是不同的,也就是说创建了新的对象, 而不是把原对象的地址赋给了一个新的引用变量,这就叫做对象拷贝。

1.3 浅拷贝和深拷贝的实例
深拷贝和浅拷贝都是对象拷贝,其各有特点,浅拷贝仅仅复制所考虑的对象,而不复制它所引用的对象,因而相对开销较小;深拷贝把要复制的对象所引用的对象都复制了一遍,速度较慢并且花销较大。在实际的应用中,我们对于浅拷贝的方法用的较多,比如常见的clone()方法,其拷贝出来的对象是通过浅拷贝,若不对clone()方法进行改写,则调用此方法得到的对象即为浅拷贝,具体代码示例如下:

我们可以通过创建一个学生对象,里边引入教授来区别浅拷贝和深拷贝:

//浅拷贝内部对象——教授
class Professor0 implements Cloneable {
        String name;
        int age;
 
    Professor0(String name, int age) {
        this.name = name;
        this.age = age;
    }
 
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
 
//浅拷贝克隆和修改对象——学生
class Student0 implements Cloneable {
        String name;// 常量对象。
        int age;
        Professor0 p;// 学生1和学生2的引用值都是一样的。
        Student0(String name, int age, Professor0 p) {
        this.name = name;
        this.age = age;
        this.p = p;
    }
 
    public Object clone() {
        Student0 o = null;
        try {
            o = (Student0) super.clone();
        } catch (CloneNotSupportedException e) {
            System.out.println(e.toString());
        }
 
        return o;
    }
}


通过固定方法调用查看区别:

public class ShallowCopy {
    public static void main(String[] args) {
        Professor0 p = new Professor0("wangwu", 50);
        Student0 s1 = new Student0("zhangsan", 18, p);
        Student0 s2 = (Student0) s1.clone();
        s2.p.name = "lisi";
        s2.p.age = 30;
        s2.name = "z";
        s2.age = 45;
        System.out.println("学生s1的姓名:" + s1.name +
        "\n学生s1教授的姓名:" + s1.p.name + "," + 
        "\n学生s1教授的年纪" + s1.p.age);// 学生1的教授
    }
}


 
结果:s2变了,但s1也变了
证明:s1的p和s2的p指向的是同一个对象。
这在我们有的实际需求中,却不是这样,因而我们需要深拷贝:

//深拷贝内部对象——教授
class Professor implements Cloneable {
    String name;
    int age;
 
    Professor(String name, int age) {
        this.name = name;
        this.age = age;
    }
 
    public Object clone() {
        Object o = null;
        try {
            o = super.clone();
        } catch (CloneNotSupportedException e) {
            System.out.println(e.toString());
        }
        return o;
    }
}
//浅拷贝克隆对象——学生
class Student implements Cloneable {
    String name;
    int age;
    Professor p;
 
    Student(String name, int age, Professor p) {
        this.name = name;
        this.age = age;
        this.p = p;
    }
 
    public Object clone() {
        Student o = null;
        try {
            o = (Student) super.clone();
        } catch (CloneNotSupportedException e) {
            System.out.println(e.toString());
        }
        o.p = (Professor) p.clone();
        return o;
    }
}


深拷贝方法修改对象:

public class DeepCopy {
    public static void main(String args[]) {
        long t1 = System.currentTimeMillis();
        Professor p = new Professor("wangwu", 50);
        Student s1 = new Student("zhangsan", 18, p);
        Student s2 = (Student) s1.clone();
        s2.p.name = "lisi";
        s2.p.age = 30;
        System.out.println("name=" + s1.p.name + "," + "age=" + s1.p.age);// 学生1的教授不改变。
        long t2 = System.currentTimeMillis();
        System.out.println(t2-t1);
    }
}


当然我们还有一种深拷贝方法,就是将对象串行化:

import java.io.*;
 
class Professor2 implements Serializable {
 
    private static final long serialVersionUID = 1L;
    String name;
    int age;Professor2(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
 
class Student2 implements Serializable {
      private static final long serialVersionUID = 1L;
      String name;// 常量对象。
      int age;
      Professor2 p;// 学生1和学生2的引用值都是一样的。
      Student2(String name, int age, Professor2 p) {
           this.name = name;
           this.age = age;
           this.p = p;
       }
 
    public Object deepClone() throws IOException, OptionalDataException,
            ClassNotFoundException {
        // 将对象写到流里
        ByteArrayOutputStream bo = new ByteArrayOutputStream();
        ObjectOutputStream oo = new ObjectOutputStream(bo);
        oo.writeObject(this);
        // 从流里读出来
        ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());
        ObjectInputStream oi = new ObjectInputStream(bi);
        return (oi.readObject());
    }
}
 
public class DeepCopy2 {
    public static void main(String[] args) throws OptionalDataException,
            IOException, ClassNotFoundException {
        long t1 = System.currentTimeMillis();
        Professor2 p = new Professor2("wangwu", 50);
        Student2 s1 = new Student2("zhangsan", 18, p);
        Student2 s2 = (Student2) s1.deepClone();
        s2.p.name = "lisi";
        s2.p.age = 30;
        System.out.println("name=" + s1.p.name + "," + "age=" + s1.p.age); // 学生1的教授不改变。
        long t2 = System.currentTimeMillis();
        System.out.println(t2-t1);
    }

2.浅拷贝和深拷贝的适用场景
2.1 浅拷贝和深拷贝的前端应用


浅拷贝和深拷贝都是只针对于像Object,Array这样的复杂对象,经常用于前端js对象的封装和改变。其两者的区别在于——浅拷贝只复制对象的第一层属性、深拷贝可以对对象的属性进行递归复制,

浅拷贝就是增加了一个指针指向已存在的内存(JavaScript并没有指针的概念,这里只是用于辅助说明),浅拷贝只是拷贝了内存地址,子类的属性指向的是父类属性的内存地址,当子类的属性修改后,父类的属性也随之被修改。就如同:一件衣服两个人穿不管你穿还是我穿都还是同一件衣服。

深拷贝就是增加一个指针,并申请一个新的内存,并且让这个新增加的指针指向这个新的内存地址使用深拷贝,在释放内存的时候就不会像浅拷贝一样出现释放同一段内存的错误,当我们需要复制原对象但有不能修改原对象的时候,深拷贝就是一个,也是唯一的选择,就如同买不同的新衣服。像es6的新增方法都是深拷贝,所以推荐使用es6语法。

2.2浅拷贝和深拷贝的使用实例
浅拷贝:

1.Object.assign:Object.assign 是 Object 的一个方法,该方法可以用于 JS 对象的合并等多个用途,其中一个用途就是可以进行浅拷贝。该方法的第一个参数是拷贝的目标对象,后面的参数是拷贝的来源对象(也可以是多个来源)。

2、扩展运算符:使用扩展运算符也可以完成浅拷贝

3、Array.prototype.concat:数组的 concat 方法其实也是浅拷贝,使用场景比较少,使用concat连接一个含有引用类型的数组时,需要注意修改原数组中的元素的属性,因为它会影响拷贝之后连接的数组

4、Array.prototype.slice

数组的 slice 方法其实也是浅拷贝,使用场景比较少,同cancat

5、使用第三方库&手动实现

社区存在一些优秀工具函数库,在开发过程中可以直接利用这些库暴露的函数直接实现浅拷贝,比如lodash就提供了clone方法供用户进行浅拷贝

深拷贝:

1.可以通过 for in 实现。

function deepCopy1(obj) {
  let o = {}
  for(let key in obj) {
    o[key] = obj[key]
  }
  return o
}
 
let obj = {
  a:1,
  b: undefined,
  c:function() {},
}
console.log(deepCopy1(obj))
2. 可以借用JSON对象的parse和stringify(对象的深拷贝)

function deepClone(obj){
    let _obj = JSON.stringify(obj),
        objClone = JSON.parse(_obj);
    return objClone
}    
let a=[0,1,[2,3],4],
    b=deepClone(a);
a[0]=1;
a[2][0]=1;
console.log(a,b);


3.递归 自身调用自身(对象的深拷贝)

function deepClone1(obj) {
  //判断拷贝的要进行深拷贝的是数组还是对象,是数组的话进行数组拷贝,对象的话进行对象拷贝
  var objClone = Array.isArray(obj) ? [] : {};
  //进行深拷贝的不能为空,并且是对象或者是
  if (obj && typeof obj === "object") {
    for (key in obj) {
      if (obj.hasOwnProperty(key)) {
        if (obj[key] && typeof obj[key] === "object") {
          objClone[key] = deepClone1(obj[key]);
        } else {
          objClone[key] = obj[key];
        }
      }
    }
  }
  return objClone;
}

4.concat(数组的深拷贝)

使用concat合并数组,会返回一个新的数组。

5.有些同学可能见过用系统自带的JSON来做深拷贝的例子,下面来看下代码实现

function cloneJSON(source) {
    return JSON.parse(JSON.stringify(source));
}


 2.3 浅拷贝与深拷贝的关系(待更新)
浅拷贝与深拷贝是一道经久不衰的面试题。

我们要答好这道题,就必须先理清其关系,简单来说,深拷贝可以视为浅拷贝+递归。

宏任务和微任务

 JavaScript 语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。

        为了协调事件、用户交互、脚本、UI 渲染和网络处理等行为,防止主线程的不阻塞,Event Loop 的方案应用而生。Event Loop 包含两类:一类是基于 Browsing Context,一种是基于 Worker。

        二者的运行是独立的,也就是说,每一个 JavaScript 运行的"线程环境"都有一个独立的 Event Loop,每一个 Web Worker 也有一个独立的 Event Loop。

一、宏任务和微任务
先做题:

setTimeout(function(){
            console.log('setTimeout') 
        },0)         
        //同步
        console.log('script start')    
        async function async1(){
            //同步
            console.log('async1 start');
            await async2()
            //异步
            console.log('async1 end')
        }    
        async function async2(){
            //同步
            console.log('async2 end')
        }    
        //微任务
        new Promise(function(resolve){
            //同步
            console.log('promise')
            for (var i = 0; i <10000; i++) {
                if (i === 10) {
                    console.log("3")
                }
                i == 9999 && resolve('4')
            }
            resolve();
        }).then(function(val){
            console.log(val)
            //异步
            console.log('promise1')
        }).then(function(res){
            console.log(res)
            console.log('promise2')
        })    
        //微任务
        async1();     
        //同步
        console.log('script end')

宏任务:
        setTimeout setInterval Ajax DOM事件  script(整体代码)   I/OUI交互事件   postMessageMessage  Channel setImmediate(Node.js 环境)

微任务:
        Promise async/await  Object.observe MutationObserver    process.nextTick(Node.js 环境)

微任务比宏任务的执行时间要早

        异步和单线程

        异步和单线程是相辅相成的,js是一门单线程脚本语言,所以需要异步来辅助

异步和同步的区别:
        异步不会阻塞程序的执行,

        同步会阻塞程序的执行,

前端使用异步的场景:
定时任务:
        setTimeout,setInverval

网络请求:
        ajax请求,动态<img>加载

        事件绑定

任务队列和event loop(事件循环)
        1)所有的同步任务都在主线程上执行,行成一个执行栈。

        2)除了主线程之外,还存在一个任务列队,只要异步任务有了运行结果,就在任务列队中植入一个时间标记。

        3)主线程完成所有任务(执行栈清空),就会读取任务列队,先执行微任务队列在执行宏任务队列。

        4)重复上面三步。

        只要主线程空了,就会读取任务列队,这就是js的运行机制,也被称为 event loop(事件循环)。

考察的是事件循环和回调队列。注意以下几点:
        Promise 优先于 setTimeout 宏任务,所以 setTimeout 回调会最后执行

        Promise 一旦被定义就会立即执行

        Promise 的 resolve 和 reject  是异步执行的回调。所以 resolve() 会被放到回调队列中,在主函数执行完和 setTimeout 之前调用

        await 执行完后,会让出线程。async 标记的函数会返回一个 Promise 对象

解析:
        首先,事件循环从宏任务(macrostack)队列开始,这个时候,宏任务队列中,只有一个 script (整体代码)任务。从宏任务队列中取出一个任务来执行。

        首先执行 console.log('script start'),输出 ‘script start'

        遇到 setTimeout 把 console.log('setTimeout') 放到 macrotask 队列中

        执行 aync1() 输出 ‘async1 start' 和 'async2' ,把 console.log('async1 end') 放到 micro 队列中

        执行到 promise ,输出 'promise1' ,把 console.log('promise2') 放到  micro 队列中

        执行 console.log('script end'),输出 ‘script end'

        macrotask 执行完成会执行 microtask ,把 microtask quene 里面的 microtask 全部拿出来一次性执行完,所以会输出 'async1 end' 和 ‘promise2'

        开始新一轮的事件循环,去除执行一个 macrotask 执行,所以会输出 ‘setTimeout'
 

什么是递归?应用场景

什么是递归?

递归,在计算机科学中是指一种通过重复将问题分解为同类的子问题而解决问题的方法。简单来说,递归表现为函数调用函数本身。在知乎看到一个比喻递归的例子,个人觉得非常形象,大家看一下:

递归最恰当的比喻,就是查词典。我们使用的词典,本身就是递归,为了解释一个词,需要使用更多的词。当你查一个词,发现这个词的解释中某个词仍然不懂,于是你开始查这第二个词,可惜,第二个词里仍然有不懂的词,于是查第三个词,这样查下去,直到有一个词的解释是你完全能看懂的,那么递归走到了尽头,然后你开始后退,逐个明白之前查过的每一个词,最终,你明白了最开始那个词的意思。

递归的概念

就是函数自己调用自己本身,或者在自己函数调用的下级函数中调用自己。

递归的步骤

  1. 假设递归函数已经写好
  2. 寻找递推关系
  3. 将递推关系的结构转换为递归体
  4. 将临界条件加入到递归体中

经典案例 1: 求和

求 1-100 的和

function sum(n) {
  if (n == 1) return 1
  return sum(n - 1) + n
}

经典案例 2: 斐波拉契数列

0,1,2,3,5,8,13,21,34,55,89...求第 n 项

// 递归方法
function fib(n) {
  if (n === 1 || n === 2) return n - 1
  return fib(n - 1) + fib(n - 2)
}
console.log(fib(10)) // 34
//非递归方法 //
function fib(n) {
  let a = 0
  let b = 1
  let c = a + b
  for (let i = 3; i < n; i++) {
    a = b
    b = c
    c = a + b
  }
  return c
}
console.log(fib(10)) // 34

经典案例 3: 爬楼梯

JS 递归 假如楼梯有 n 个台阶,每次可以走 1 个或 2 个台阶,请问走完这 n 个台阶有几种走法

function climbStairs(n) {
  if (n == 1) return 1
  if (n == 2) return 2
  return climbStairs(n - 1) + climbStairs(n - 2)
}

经典案例 4: 深拷贝

原理: clone(o) = new Object; 返回一个对象

function clone(o) {
  var temp = {}
  for (var key in o) {
    if (typeof o[key] == 'object') {
      temp[key] = clone(o[key])
    } else {
      temp[key] = o[key]
    }
  }
  return temp
}

经典案例 5:递归组件

  • 递归组件: 组件在它的模板内可以递归的调用自己,只要给组件设置 name 组件就可以了。
  • 不过需要注意的是,必须给一个条件来限制数量,否则会抛出错误: max stack size exceeded
  • 组件递归用来开发一些具体有未知层级关系的独立组件。比如:联级选择器和树形控件

<template>
  <div v-for="(item,index) in treeArr"> {{index}} <br/>
      <tree :item="item.arr" v-if="item.flag"></tree>
  </div>
</template>
<script>
export default {
  // 必须定义name,组件内部才能递归调用
  name: 'tree',
  data(){
    return {}
  },
  // 接收外部传入的值
  props: {
     item: {
      type:Array,
      default: ()=>[]
    }
  }
}
</script>

防抖和节流?应用场景

1. 防抖策略
防抖策略(debounce)是当事件被触发后,延迟n秒后再执行回调函数,如果在这n秒内事件被再次触发,则重新计时.

好处是:它能够保证用户在频繁触发某些事件的时候,不会频繁的执行回调,只会被执行一次.

防抖的概念:如果有人进电梯(触发事件),那电梯将在10秒钟后出发(执行事件监听器),这时如果又有人进电梯了(在10秒内再次触发该事件),我们又得等10秒再出发(重新计时)。

防抖的应用场景:

用户在输入框连续输入一串字符时,可以通过防抖策略,只在输入完后,才执行查询的请求,这样可以有效减少请求次数,节约请求资源.

实现
函数防抖:

function debounce(fn,wait){
    var timer = null;
    return function(){
        clearTimeout(timer)
        timer = setTimeout(()=>{
            fn()
        },wait)
    }
}
 
function fn1(){
    console.log(1)
}
window.onscroll = debounce(fn1,500)


2. 节流策略
节流策略(throttle),可以减少一段时间内事件的触发频率.

节流阀的概念:

高铁的卫生间是否被占用,由红绿灯控制,假设一个每个人上洗手间要五分钟,则五分钟之内别人不可以使用,上一个使用完毕之后,将红灯设置为绿灯,表示下一个人可以使用了.下一个人在使用洗手间时需要先判断控制灯是否为绿色,来知晓洗手间是否可用.

节流阀为空,表示可以执行下一次操作,不为空,表示不能使用下次操作.
当前操作执行完之后要将节流阀重置为空,表示可以执行下次操作了.
每次执行操作之前,先判断节流阀是否为空
节流策略的应用场景:

鼠标不断触发某事件时,如点击,只在单位事件内触发一次.
懒加载时要监听计算滚动条的位置,但不必要每次滑动都触发,可以降低计算频率,而不必要浪费CPU资源. 
这里我们用一个鼠标跟随效果的例子:

 1.渲染UI结构并美化样式

<!-- UI 结构 -->
<img src="./assets/angel.gif" alt="" id="angel" />
/* CSS 样式 */
html, body {
 margin: 0;
 padding: 0;
 overflow: hidden; }
#angel {
 position: absolute; }


2.使用节流优化鼠标跟随效果

 预定义一个timer节流阀
当设置鼠标跟随效果后,清空timer节流阀,方便下次开启延时器
当执行事件是,先判断节流阀是否为空,如果不为空,则证明距离上次操作执行间隔还不到设置的时间

var angel=$(''.angel)
var timer=null;//预定义一个节流阀
$(document).on('mousemove',function(e){
if(timer){return}//判断节流阀是否为空
timer=setTimerout(function(){
$(angel).css('left',e.pageX+'px').css('top',e.pageY+'px')
timer=null;//清空节流阀,方便下次开启延时器
},16)
 
})


3. 防抖和节流的区别
防抖:如果事件被频繁触发,防抖保证只能有一次触发生效,前面N多次触发都会被忽略.

节流:如果时间被频繁触发,节流能减少事件触发的频率,因此,节流是有选择性的执行一部分事件. 

总结: 函数节流(throttle)与 函数防抖(debounce)都是为了限制函数的执行频次,以优化函数触发频率过高导致的响应速度跟不上触发频率,出现延迟,假死或卡顿的现象。 
 

以上暂时整理了

JavaScript常见字符串方法、数组方法、对象方法

js检测数据类型的方法

js原型链

ES6新的数据类型和应用场景

深拷贝、浅拷贝和应用场景

宏任务和微任务

什么是递归?应用场景

防抖和节流?应用场景

这些常见面试题,后续的话还会更新


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

前端_彭于晏

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

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

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

打赏作者

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

抵扣说明:

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

余额充值