博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
js进阶之你必须要会的技术!
阅读量:285 次
发布时间:2019-03-01

本文共 12260 字,大约阅读时间需要 40 分钟。

1. 深浅拷贝

1.1 浅拷贝

原理

浅拷贝会创建一个新的对象,这个对象有着原始对象属性值的一份精准拷贝:

  • 如果原始对象属性类型为基本类型,拷贝的就是基本类型的值,因此修改原、新对象的基本类型属性互不影响
  • 如果原始对象属性类型为引用类型,拷贝的就是内存地址(或指向该地址的指针),因此修改原、新对象的引用类型属性会相互影响
function clone(origin) {
var result = {
}; for (var prop in origin) {
if (origin.hasOwnProperty(prop)) {
result[prop] = origin[prop]; } } return result;}var jay = {
name: "jayChou", age: 40, family: {
wife: "Quinlivan" }}var otherJay = clone(jay);otherJay.age = 18;otherJay.family.wife = "otherGirl";console.log(jay); // // {
// name: "jayChou",// age: 40, // 没被改变// family: {
// wife: "otherGirl" // 同时被改变,说明是同一个引用// }// }console.log(otherJay);// // {
// name: "jayChou",// age: 18,// family: {
// wife: "otherGirl" // 被改变了// }// }

实现浅拷贝的方式

  • Object.assign():该方法将所有可枚举的自身属性从一个或多个源对象复制到目标对象。它返回目标对象。

    Object.assign(新对象, ...源对象)
  • Array.Prototype.concat():该方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。

    let arr2=arr.concat();    //将arr浅拷贝,生成arr2
  • Array.Prototype.slice():该方法返回一个新的副本对象,该对象是一个由 beginend决定的原先的浅拷贝(包括begin,不包括end,左闭右开)。原始序列不会被改变。

    let arr = [1, 3,  {
    username: 'kobe'} ];let arr3 = arr.slice(); //将arr浅拷贝到arr3arr3[2].username = 'wade'console.log(arr); //wade

1.2 深拷贝

原理

完全复制另外一个对象,引用也是自己创建。即完整复制舒服的值(而非引用)。目的在于避免拷贝后数据对原数据产生影响

实现深拷贝的方式

  • JSON方法实现:利用JSON的parse()和stringfy()实现对某一个对象的深拷贝(无法处理源对象中的函数

    let arr = [1, 3, {
    username: 'kobe'} ];let arr4 = JSON.parse(JSON.stringify(arr));arr4[2].username = 'james'; console.log(arr, arr4) //[1,3,{username:'kobe'}] [1,3,{username:'james'}]
  • 手写递归方法:递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝。

    //定义检测数据类型的功能函数function checkedType(target) {
    return Object.prototype.toString.call(target).slice(8, -1)} //实现深度克隆---对象/数组 function clone(target) {
    //判断拷贝的数据类型 //初始化变量result 成为最终克隆的数据 let result, targetType = checkedType(target) if (targetType === 'object') {
    result = {
    } } else if (targetType === 'Array') {
    result = [] } else {
    return target } //遍历目标数据 for (let i in target) {
    //获取遍历数据结构的每一项值。 let value = target[i] //判断目标结构里的每一值是否存在对象/数组 if (checkedType(value) === 'Object' || checkedType(value) === 'Array') {
    //对象/数组里嵌套了对象/数组 //继续遍历获取到value值 result[i] = clone(value) } else {
    //获取到value值是基本的数据类型或者是函数。 result[i] = value; } } return result}
  • 函数库lodash:该函数库也有提供 _.cloneDeep用来做深拷贝。

    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

2. 防抖节流

2.1 防抖

含义

对于高频触发的函数,我们并不想频发触发事件,比如说搜索框实时发请求,onmousemove, resize, onscroll等等,这个时候就需要对函数增加防抖功能了

代码

2.2 节流

含义

在某个规定的时间内,节流函数至少执行一次。

代码

3. 基础总结

  1. typeof:返回的是数据类型的字符串表达

    var a;console.log(typeof a) // 'undefined'
  2. instanceof:是否是该构造函数的实例

  3. 理解数据类型:

  4. undefined和null的区别?

    undefined代表未定义,null代表定义了未赋值。

  5. 什么时候给变量赋值为null?

    • 初始赋值,表明将要赋值为对象
    • 结束前,让对象成为垃圾对象(被垃圾回收器回收)

4. 函数高级

4.1 原型与原型链

显式原型与隐式原型

  1. 每个函数都有一个prototype属性,它默认指向一个Object实例空对象(原型对象),原型对象中有一个属性constructor,它指向函数对象。

  2. 每个函数function都有一个prototype,即显式原型(属性);每个实例对象都有一个 _ _proto _ _,可称为隐式原型(属性)。

  3. 实例对象的隐式原型的值为其构造函数的显式原型的值。

原型链

  1. 访问一个对象的属性时,先在自身属性中查找, 如果找到就返回,如果没找到,沿着_ _ proto_ _这条链向上找,直到找到就返回,如果没找到,返回undefined
  2. 别名:隐式原型链
  3. 作用:查找对象属性(方法)
  4. Function的prototype与_ _ proto_ _是指向一个地方。
  5. 所有函数的_ _ proto_ _都是相等的,因为都是New Function()创建的,都等于Function.prototype。
  6. 函数的显式原型指向的对象默认是空的Object实例对象(Object不满足)
  7. Object的原型对象是原型链尽头!(Object.prototype._ _ proto_ _=null)

属性问题

  1. 当我们为对象设置属性的时候,是不看原型链的,如果原型链中也有此属性,在读取该属性的时候,会读取属性内部的属性而不是原型对象的属性。
  2. 读取对象属性的时候会自动到原型链中寻找

instanceof

  1. A instanceof B:A是否是B这个构造函数的实例对象

    A:实例对象

    B:构造函数

    如果函数B的显式原型在A对象的原型链上,返回true,否则返回false。

面试题

  1. function A(){
    }A.prototype.n=1;var b = new A();//在原型中,= 一定要理解成 指向A.prototype = {
    n:2, m:3}var c = new A();console.log(b.n,b.m,c.n,c.m); //1,undefined,2,3

  2. function F(){
    }Object.prototype.a=function(){
    console.log('a()')}Function.prototype.b=function(){
    console.log('b()')}var f= new F()f.a() // a()f.b() // error F.a() // a()F.b() // b()

变量提升

4.2 执行上下文与执行上下文栈

全局执行上下文

  1. 在执行全局代码前将window确定为全局执行上下文
  2. 对全局数据进行预处理
    • var定义的全局变量 ===> undefined,添加为window属性
    • function声明的全局函数 ===> 赋值(fun),添加为window的方法
    • this ===> 赋值为window

函数执行上下文

  1. 调用函数,准备执行函数体之前,创建对应的函数执行上下文对象
  2. 对局部数据进行预处理:
    • 形参变量 ====> 赋值(实参数据) ==> 添加为执行上下文的属性
    • arguments ====> 赋值(实参列表),添加为执行上下文的属性
    • var定义的局部变量 ===> undefined,添加为执行上下文的属性
    • function声明的函数 ===> 赋值(fun),添加为执行上下文的属性
    • this ===> 赋值(调用函数的对象)
  3. 开始执行函数体代码

执行上下文栈

  1. 在全局代码执行前,JS引擎就会创建一个栈来存储管理所有的执行上下文对象
  2. 在全局执行上下文(window)确定后,将其添加到栈中(压栈)
  3. 在函数执行上下文创建后,将其添加到栈中(压栈)
  4. 在当前函数执行完后,将栈顶的对象移除(出栈)
  5. 当所有的代码执行完后,栈中只剩下window

面试题1

面试题2

知识点:先执行变量提升,再执行函数提升

function a(){
}var a;console.log(typeof a) // 'function'

面试题3

if(!(b in window)){
var b = 1;}console.log(b); //undefined

面试题4

var c = 1;function c(c){
console.log(c); var c = 3;}c(2) //error 先变量提升 后函数提升

4.3 作用域与作用域链

理解

  1. 作用域就是一块"地盘",一个代码段所在的区域
  2. 它是静态的(相对于上下文对象),在编写代码时就产生了

分类

  1. 全局作用域
  2. 函数作用域
  3. 块作用域(ES6)

作用

隔离变量,不同作用域下同名变量不会有冲突!

作用域和执行上下文区别

  1. 区别1:
    • 全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了。而不是在函数调用时
    • 全局执行上下文环境是在全局作用域确定之后,js代码马上执行之前创建
    • 函数执行上下文是在调用函数时,函数体代码执行之前创建
  2. 区别2:
    • 作用域是静态的,只要函数定义好了就一直存在,且不会再变化
    • 执行上下文是动态的,调用函数时创建,函数调用结束时就会自动释放

联系

  1. 上下文环境(对象)是从属于所在的作用域
  2. 全局上下文环境 ===> 全局作用域
  3. 函数上下文环境 ===> 对应的函数作用域

作用域链

  1. 多个上下级关系的作用域形成的链,它的方向是从下向上的(从内到外),查找变量时就是沿着作用域链来查找的
  2. 查找一个变量的查找规则:
    • 在当前作用域下的执行上下文中查找对应的属性,如果有就直接返回,否则进入2
    • 在上一级作用域的执行上下文中查找对应的属性,如果有就直接返回,否则进入3
    • 再次执行2的相同操作,直到全局作用域,如果还找不到就抛出找不到的异常

面试题1

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f9vNtAQY-1617031286848)(E:\学习笔记\图片\image-20210319153242434.png)]

面试题2

4.4 闭包

如何产生闭包?

当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量(函数)时,就产生了闭包

闭包的个数 = 调用外部函数的次数

闭包到底是什么?

  1. 闭包是嵌套的内部函数
  2. 闭包是包含被引用变量(函数)的对象

注:执行函数定义就会产生闭包(不用调用内部函数)

常见的闭包

  1. 将函数作为另外一个函数的返回值

  2. 将函数作为实参传递给另一个函数调用

闭包的作用

  1. 使函数内部的变量在函数执行完后,仍然存活在内存中(延长了局部变量的生命周期)
  2. 让函数外部可以操作(读写)到函数内部的数据(变量/函数)

两个问题

  1. 函数执行完后,函数内部声明的局部变量是否还存在?

    一般是不存在,存在于闭包中的变量才可能存在,但也必须要有引用指向该函数。

  2. 在函数外部能直接访问函数内部的局部变量吗?

    不能,但我们可以通过闭包的形式让外部操作它

闭包的生命周期

  1. 产生:在嵌套内部函数定义执行完时就产生了(不是在调用)
  2. 死亡:在嵌套的内部函数成为垃圾对象时

闭包应用:自定义JS模块

  1. JS模块:具有特定功能的JS文件

  2. 将所有的数据和功能都封装在一个函数内部

  3. 只向外暴露一个包含n个方法的对象或函数

  4. 模块的使用者,只需要通过模块暴露的对象调用方法来实现相应功能

  5. 方式1:

    function myModule(){
    //私有数据 var msg = 'My atguigu' //操作数据的函数 function doSomething(){
    console.log('doSomething') } function doOtherthing(){
    console.log('doOtherthing') } //向外暴露 return {
    doSomething:doSomething, doOtherthing:doOtherthing }}
  6. 方式2:

    (function myModule(){
    //私有数据 var msg = 'My atguigu' //操作数据的函数 function doSomething(){
    console.log('doSomething') } function doOtherthing(){
    console.log('doOtherthing') } //向外暴露 window.myModele2 = {
    doSomething:doSomething, doOtherthing:doOtherthing }})()

内存泄漏

  1. 缺点:
    • 函数执行完后,函数内的局部变量没有释放,占用内存时间会变长
    • 容易造成内存泄漏(内存被垃圾对象占用)
  2. 解决:
    • 能不用闭包就不用
    • 及时释放
  3. 常见的内存泄漏:
    • 意外的全局变量
    • 没有及时清理的计时器或回调函数
    • 闭包

内存溢出

  1. 内存溢出是一种程序运行出现的错误,当程序运行需要的内存超过了剩余的内存时,就会抛出内存溢出的错误。
  2. 内存泄漏是指没有及时释放占用的内存,内存泄漏积累多了就容易导致内存溢出

面试题1

面试题2

5. 面向对象高级

5.1 对象创建模式

方式1:Object构造函数模式

  1. 先创建空Obeject对象,然后动态添加属性、方法
  2. 使用场景:起初不确定对象内部数据
  3. 问题:语句太多

方式2:对象字面量模式

  1. 使用{}创建对象,同时指定属性、方法
  2. 使用场景:起初时对象内部数据是确定的
  3. 问题:如果创建多个对象,有重复代码

方式3:工厂模式

  1. 通过工厂函数动态创建对象并返回
  2. 使用场景:需要创建多个对象
  3. 问题:对象没有一个具体的类型,都是Object类型
function createPerson(name,age){
var obj = {
name:name, age:age, setName: function(name){
this.name=name } } return obj;}

方式4:自定义构造函数模式

  1. 自定义构造函数,通过new创建对象
  2. 使用场景:需要创建多个类型确定的对象
  3. 问题:每个对象都有相同的数据,浪费内存
function Person(name,age){    this.name=name;    this.age=age;    this.setName=function(name){        this.name=name;    }}var p1 = new Person('Tom',12);p1.setName('Jack');console.log(p1.name,p1.age); function Student(name,price){    this.name=name    this.price=price}var s = new Student('Bob',13000)console.log(s instanceof Student)

方式5:构造函数+原型的组合模式

  1. 自定义构造函数,属性在函数中初始化,方法添加到原型上
  2. 使用场景:需要创建多个类型确定的对象
function Person(name,age){
this.name=name; this.age=age;}Person.prototype.setName = function(name) {
this.name = name;}

5.2 继承模式

原型链继承

  1. 定义父类的构造函数,给父类的原型添加方法
  2. 定义子类的构造函数
  3. 创建父类的对象赋值给子类的原型
  4. 将子类的原型的构造属性设置为子类型
  5. 给子类型原型添加方法
  6. 给子类型原型添加方法
  7. 创建子类型的对象:可以调用父类型的方法
  8. 关键:子类型的原型为父类型的实例
//父类型function Father(){
this.supProp = 'father property'}Father.propertype.show = function(){
console.log(this.supProp)}//子类型function Son(){
this.subProp = 'son property'}Son.prototype = new Father() // 这是关键,子类型的原型为父类型的实例Son.prototype.constructor = Son // 让子类的原型的constructor指向子类型Son.propertype.show2 = function(){
console.log(this.subProp)}var son = new Son()son.show() // father property

借用构造函数继承

function Person(name,age){
this.name = name; this.age = age;}function Student (name,age,price){
Person.call(this,name,age) //相当于 this.Person(name.age) // this.name = name; // this.age = age; this,price = price;}var s = new Student('Tom',20,14000)console.log(s.name,s.age,s.price);

组合继承

function Person(name,age){
this.name = name; this.age = age;}Person.prototype.setName = function(name){
this.name = name;}function Student (name,age,price){
Person.call(this,name,age) //相当于 this.Person(name.age) // this.name = name; // this.age = age; this.price = price;}Student.prototype = New Person() // 为了能看到父类的方法Student.prototype.constructor = Student //修正constructor属性Student.prototype.setPrice = function (price){
this.price = price}var s = new Student('Tom',24,15000)s.setName('Bob')s.setPrice(16000)console.log(s.name,s.age,s.price) // Bob 24 16000
function Person(name,age,sex,job){
this.name = name; this.age = age; this.sex = sex; this.job = job; } Person.prototype.eat = function (){
console.log(`我是${
this.job}${
this.name},我正在吃东西`) } function Boss(name,age,sex,job,thing){
Person.call(this,name,age,sex,job); this.thing=thing } Boss.prototype = new Person(); Boss.prototype.constructor = Boss; Boss.prototype.bossThing = function (thing){
this.thing=thing console.log(`我是${
this.job}${
this.name},我要${
this.thing}`) } function manager(name,age,sex,job,thing){
Person.call(this,name,age,sex,job); this.thing=thing } manager.prototype = new Person(); manager.prototype.constructor = manager; manager.prototype.managerThing = function (thing){
this.thing=thing console.log(`我是${
this.job}${
this.name},我要${
this.thing}`) } function worker(name,age,sex,job,thing){
Person.call(this,name,age,sex,job); this.thing=thing } worker.prototype = new Person(); worker.prototype.constructor = worker; worker.prototype.workerThing = function (thing){
this.thing=thing console.log(`我是${
this.job}${
this.name},我要${
this.thing}`) } var xiaolong = new Boss('WXL',21,'男','老板','打员工'); var donglin = new manager('QDL',22,'男','管理层',''); var zhixiong = new worker('李智雄',22,'男','员工',''); xiaolong.bossThing('打老婆') xiaolong.eat() donglin.managerThing('骂人') donglin.eat() zhixiong.workerThing('辞职回家种田') zhixiong.eat()

6. 线程机制与事件机制

6.1 进程与线程

进程

  1. 程序的一次执行,它占有一片独有的内存空间
  2. 可以通过windows任务管理器查看进程

线程

  1. 是进程内的一个独立执行单元
  2. 是程序执行的一个完整流程
  3. 是CPU的最小的调度单元

相关知识

  1. 应用程序必须运行在某个进程的某个线程上
  2. 一个进程中至少有一个运行的线程:主线程,进程启动后自动创建
  3. 一个进程中也可以同时运行多个线程,我们会说程序是多线程运行
  4. 一个进程内的数据可以供其中的多个线程直接共享
  5. 多个进程之间的数据是不能直接共享
  6. 线程池(Thread Pool):保存多个线程对象的容器,实现线程对象的反复利用

浏览器是单进程还是多进程

  1. 单进程:
    • firefox
    • 老版IE
  2. 多进程:
    • Chrome
    • 新版IE

6.2 浏览器内核

内核模块

  1. JS引擎模块:负责js程序的编译与运行

  2. html,css文档解析模块:腐恶页面文本的解析

  3. DOM/CSS模块:负责DOM/CSS在内存中的相关处理

  4. 布局和渲染模块:负责页面的布局和效果的绘制(内存中的对象)

    ​ (运行在主线程上

    …………………………………………………………………………………………………………

    ​ (运行在分线程上

  5. 定时器模块:负责定时器的管理

  6. 事件响应模块:负责事件的管理

  7. 网络请求模块:负责ajax请求

6.3 定时器引发的思考

定时器真是定时执行的吗?

  1. 定时器并不能保证真正定时执行
  2. 一般会延迟一丁点(可以接受),也有可能延迟很长时间(不能接受)

定时器回调函数是在分线程执行的吗?

在主线程执行的,js是单线程的

定时器是如何实现的?

事件循环模型(后面讲)

6.4 JS是单线程执行的

如何证明js执行时单线程的?

  1. setTimeout()的回调函数是在主线程执行的
  2. 定时器回调函数只有在运行栈中的代码全部执行完之后才有可能执行

为什么js要用单线程模式,而不是多线程模式?

Javascript的单线程,与它的用途有关作为,浏览器脚本语言,Javascript的主要用途是与用户交互,以及操作DOM,这决定了它只能是单线程,否则会带来很复杂的同步问题。

代码的分类

  1. 初始化代码
  2. 回调代码

js引擎执行代码的基本流程

  1. 先执行初始化代码:包含一些特别的代码,比如设置定时器、绑定事件监听、发送ajax请求
  2. 后面在某些时刻才会执行回调代码

6.5 浏览器的时间循环(轮询)模型

  1. 模型原理图

  2. 模型的2个重要组成部分:

    • 事件(定时器/DOM事件/Ajax)管理模块
    • 回调队列
  3. 模型的运转流程:

    • 执行初始化代码,将事件回调函数交给对应模块管理
    • 当事件发生时,管理模块会将回调函数及其数据添加到回调队列中
    • 只有当初始化代码执行完后(可能要一定时间),才会遍历去回调队列中的回调函数执行。

转载地址:http://cfox.baihongyu.com/

你可能感兴趣的文章