webpack面试解析

参考:
上一篇webpack相关的系列:webpack深入学习,搭建和优化react项目
爪哇教育@字节面试官解析webpack-路白

1、Webpack中的module是什么?

通常来讲,一个 module 模块就是指一个文件中导出的内容,webpack 会将这些 module 打包成一个或多个 chunk 。
在webpack中module就是一个个打包模块,它通过模块之间的依赖关系构建出打包依赖路径,最终构建出一个bundle。
webpack支持ESModule、CommonJS、AMD、各种资源模块(image\Json\font等等)的打包。

  1. ESM
    import导入 & export导出,主要在浏览器环境中使用。
  2. commonJS
    主要在nodeJS中使用,module.exports暴露模块,require导入模块。
  3. AMD
    define定义模块 require导出模块
  4. css / less
    @import

2、chunk和bundle的区别是什么?

chunk是【webpack打包过程】中的Module集合,是打包过程中module集合的概念。
webpack打包是从一个入口模块开始,入口模块引用其他模块,然后其他的模块再引用更多模块,构成了一个引用关系集合,这些module就形成了一个chunk。
chunk会根据webpack的配置进行拆分,一般包含三种情况

  1. 项目入口(entry)。如果存在多个入口模块,可能会出现多条打包路径,每一条路径都会形成一个chunk。
  2. 通过import()动态引入的代码
  3. 通过splitChunks拆分出来的代码

Bundle是webpack最终输出的一个或者多个打包好的文件。
chunk是webpack的打包过程概念,bundle是打包完成后的结果,也就是说大部分的chunk在打包完成后,就成了bundle。

Chunk和Bundle的关系是什么?
大多数情况下,一个entry入口会生成一个chunk,一个bundle,但是也有例外,比如如果加入了devtool:sourcemap的配置,一个入口,一个chunk生成了两个Bundle。

Chunk和Bundle的关系探究 从一个空文件夹编写一个可使用webpack打包的项目。
  1. 空文件夹中npm init,在src文件夹下简单写几个文件。(先只写几个简单的js文件
  2. 安装webpack / webpack-cli
  3. 编写webpack配置。
module.exports = {
    mode:'production', // webpack要求必填
    entry: {
        index: './src/index.js, 
    }, // 打包入口
    output: {
        path: path.join(__dirname, 'dist'), // 打包输出的路径
        filename: '[name].js' , // 打包输出的bundle名称。
    }
}

// 上面的配置方式会输出一个chunk(index.js) / 对应一个Bundle(index.js).

但是如果在配置中多加一行:devtool: "source-map" // 生成代码调试可用的源码-打包后代码映射关系,打包出来的bundle就会多生成一个index.js.map。但是chunk还是只有一个,也就是说一个chunk对应了两个Bundle。
在这里插入图片描述

多入口的打包情况
  1. 入口为一个key对应一个入口文件数组,仍然只会输出一个chunk。
module.exports = {
    // ...
    entry: {
        index: ['./src/index.js','./src/add.js'], 
    }, // 打包入口
}

// 上面的配置方式会输出一个chunk(index.js) / 对应一个Bundle(index.js).
  1. 入口为多个key,一个入口对应一个chunk
module.exports = {
    // ...
    entry: {
        index: './src/index.js', 
        common: './src/common.js'
    }, 
}
下载了一个lodash依赖,然后加入其他的优化配置 会出现几个chunk 、 bundle
module.exports = {
    mode:'production', // webpack要求必填
    entry: {
        index: './src/index.js, 
        other: './src/multiple.js'
    }, // 打包入口
    output: {
        path: path.join(__dirname, 'dist'), // 打包输出的路径
        filename: '[name].js' , // 打包输出的bundle名称。
    },
    optimization: { // 优化持久缓存的配置,比如动态引入导致的hashName变动 、 代码分包等。
        runtimeChunk: 'single', // runtime
        splitChunks: {
            cacheGroups: {
                commons: {
                    chunks: 'initial',
                    minChunks: 2,
                    minSize:0
                },
                vendor: {
                    test: /node_modules/,
                    chunks: 'initial',
                    name: 'vendor',
                    enforce: true
                }
            }
        }
    }
}

出现了5个包

  1. 两个entry
  2. runtimeChunk
    官网说明
    webpack之optimization.runtimeChunk作用
    runtimeChunk:single会生成一个在所有生成chunk之间运行的文件,文件中包含chunk映射关系清单等,如果没有设置runtime,这个映射关系清单信息就会放在入口文件打包出来的chunk内部,假如我们修改了一个模块,那这个模块的hash值变动后,所有相关的chunk hash值都会变动,导致缓存失效,配置了runtime后就只有runtime和被修改的模块的hash值才会变动,其他模块的hash值不变,浏览器缓存仍然存在。

目的:为了更好地利用浏览器缓存,不至于每一次打包都导致主包的hash值变动。

  1. SplitChunks common包和 SplitChunks vendor包
    commons包的内容:只要被两个chunk同时引用的公共代码,就提取到common里面。
    vendor包的内容:顾名思义,提取node_module的依赖到这个包里。
SplitChunks

官网说明
splitChunks探索
如何使用 splitChunks 精细控制代码分割
webpack splitChunks配置(一)chunks属性的使用

SplitChunks是 Webpack 中一个可以配置代码分割规则,提取或分离代码的插件,主要作用是提取公共代码,防止代码被重复打包,以及拆分过大的js文件,合并零散的js文件。

SplitChunks插件配置选项

意思是只要符合下面的条件就会强制分块。

// webpack4的默认配置(开箱即用)

splitChunks: {
    // 表示选择哪些 chunks 进行分割,可选值有:async,initial和all
    chunks: "async",
    // 表示新分离出的chunk必须大于等于minSize,默认为30000,约30kb。
    minSize: 30000,
    // 表示一个模块至少应被minChunks个chunk所包含才能分割。默认为1。
    minChunks: 1,
    // 表示按需加载文件时,并行请求的最大数目。默认为5。
    maxAsyncRequests: 5,
    // 表示加载入口文件时,并行请求的最大数目。默认为3。
    maxInitialRequests: 3,
    // 表示拆分出的chunk的名称连接符。默认为~。如chunk~vendors.js
    automaticNameDelimiter: '~',
    // 设置chunk的文件名。默认为true。当为true时,splitChunks基于chunk和cacheGroups的key自动命名。
    name: true,
    // cacheGroups 下可以可以配置多个组,每个组根据test设置条件,符合test条件的模块,就分配到该组。模块可以被多个组引用,但最终会根据priority来决定打包到哪个组中。默认将所有来自 node_modules目录的模块打包至vendors组,将两个以上的chunk所共享的模块打包至default组。
    cacheGroups: {
        vendors: {
            test: /[\\/]node_modules[\\/]/,
            priority: -10
        },
        // 
    default: {
            minChunks: 2,
            priority: -20,
            reuseExistingChunk: true
        }
    }
}

// 分包条件
// 模块在代码中被复用或者来自 node_modules 文件夹
// 模块的体积大于等于30kb(压缩之前)
// 当按需加载 chunks 时,并行请求的最大数量不能超过5
// 页面初始加载时,并行请求的最大数量不能超过3
理解 chunks的三个选项
  • chunks: “initial”
    initial表示 同时打包异步同步,但是异步内部的引入将不再拆分,直接打包一起进一个文件中。也就是说每一次遇到一个异步加载的模块就相当于多开了一个入口,接下来只会根据这个入口内的依赖路径生成chunk。如果一个文件既被异步引入又被同步引入,它们将分别被打包到两个文件。
  • chunks: “async”
    async表示只拆分异步加载的模块,如果异步加载的模块内有同步加载的模块,则会将同步加载的模块并入main.js的入口文件里。
  • chunks: “all”
    all表示以上两者都包括,如果一个文件被同步引入又被异步引入,会被提取出来打包到一个文件,其他的文件引入的都是这个被打包出来的同一份文件。 也就是说chunk 可以在异步和非异步 chunk 之间共享。

在这里插入图片描述

Plugin和Loader分别是什么,如何工作

  1. Loader
    由于webpack 本身只能打包commonjs规范的js文件,对css,图片等格式的文件没法打包,因此引入了Loader来帮助打包,Loader可以看作是一个各种类型的模块翻译器,将非js模块转化为webpack能识别的js模块,比如css-loader会把css文件最后打包成css.js。
    loader运行在打包文件之前(loader为在模块加载时的预处理文件)

  2. Plugin
    可以完成各种loader不能完成的功能。plugin扩展了webpack的功能,webpack基于事件流框架 Tapable,它会在运行的各个阶段广播出事件,插件会监听对应的事件,在打包过程中对打包代码做相应处理。。从打包优化和压缩,到重新定义环境变量,还可以用来处理各种各样的任务。因此webpack可以做到针对各种情况灵活处理。
    Plugin在整个编译周期中都可以使用。

其他的webpack打包过程概念:webpack编译会创建的两个核心对象
  1. Compiler
    是一个包含了webpack环境的所有配置信息的对象,可以理解为webpack的实例,是全局唯一的,它内部包括options / loader / plugin的信息和 webpack 整个生命周期相关的钩子。

  2. Compliation
    包含了当前的模块资源和编译生成资源,webpack在开发模式下运行的时候,每当检测到一个文件变化,就会创建一次新的Compliation。

事件节点:
run:开始编译
make:从entry开始递归分析依赖并对依赖进行build
build-moodule:使用loader加载文件并build模块
normal-module-loader:对loader加载的文件用acorn编译,生成抽象语法树AST
program:开始对AST进行遍历,当遇到require时触发call require事件
seal:所有依赖build完成,开始对chunk进行优化(抽取公共模块、加hash等)
optimize-chunk-assets:压缩代码
emit:把各个chunk输出到结果文件

如何实现一个Plugin / loader

web前端高级webpack - webpack常见面试题及手写loader和plugin

实现loader

在编写 loader 前,我们首先需要了解 loader 的本质
其本质为函数,函数中的 this 作为上下文会被 webpack 填充,因此我们不能将 loader设为一个箭头函数
函数接受一个参数,为 webpack 传递给 loader 的文件源内容
函数中 this 是由 webpack 提供的对象,能够获取当前 loader 所需要的各种信息
函数中有异步操作或同步操作,异步操作通过 this.callback返回,返回值要求为 string 或者 Buffer
一般在编写loader的过程中,保持功能单一,避免做多种功能
代码如下所示:

// 导出一个函数,source为webpack传递给loader的文件源内容 
module.exports = function(source) { 
    const content = doSomeThing2JsString(source); 
     
    // 如果 loader 配置了 options 对象,那么this.query将指向 options 
    const options = this.query; 
     
    // 可以用作解析其他模块路径的上下文 
    console.log('this.context'); 
     
    /* 
     * this.callback 参数: 
     * error:Error | null,当 loader 出错时向外抛出一个 error 
     * content:String | Buffer,经过 loader 编译后需要导出的内容 
     * sourceMap:为方便调试生成的编译后内容的 source map 
     * ast:本次编译生成的 AST 静态语法树,之后执行的 loader 可以直接使用这个 AST,进而省去重复生成 AST 的过程 
     */ 
    this.callback(null, content); // 异步 
    return content; // 同步 
} 

实现一个能够删除源码中的console语句的Loader

const parser =  require('@babel/parser') ;// 可以把js源码转成 ast语法树
const traverse = require('@babel/traverse').default // 可以递归遍历 ast节点
const generator = require('@babel/generator').default; // 把修改好的ast语法树 再转成源码字符串
const types = require('@babel/types') // 操作节点的增删改

module.exports = function(source){
  const ast = parser.parse(source,{sourceType:'module'})
  // console.log(ast.program.body);
  traverse(ast,{
    CallExpression(path){
      // console.log(path)
      if(types.isMemberExpression(path.node.callee) && types.isIdentifier(path.node.callee.object,{name:'console'})){
        path.remove()
      }  
    }
  })
  const output = generator(ast,{},source);
  return output.code
}
实现plugin

如果自己要实现plugin,需要遵循一定的规范:

  1. 插件必须是一个函数或者是一个包含 apply 方法的对象,这样才能访问compiler实例
  2. 传给每个插件的 compiler 和 compilation 对象都是同一个引用,因此不建议修改
  3. 异步的事件需要在插件处理完任务时调用回调函数通知 Webpack 进入下一个流程,不然会卡住
class MyPlugin {
    // Webpack 会调用 MyPlugin 实例的 apply 方法给插件实例传入 compiler 对象
  apply (compiler) {
    // 找到合适的事件钩子,实现自己的插件功能
    compiler.hooks.emit.tap('MyPlugin', compilation => {
        // compilation: 当前打包构建流程的上下文
        console.log(compilation);
        
        // do something...
    })
  }
}

打包的时候生成一个显示当前项目文件列表的myfile.md说明文件。

class Myplugin{
	// 可以再打包的时候产生一个新的文件
	constructor(options){
	    // new 这个插件是传进来的参数
	   this.options = options
	}
	apply(compiler){
    const hooks = compiler.hooks;
    // 监听事件
	if(hooks){
	    hooks.emit.tap('myplugin',function(complication,callback){
	    var str = '文件列表是:\n';
	    for(let k in complication.assets){
	        str += `文件名:${k},,,大小是 ${ complication.assets[k].size()} \n\n`
	    }
	    complication.assets['myfile.md'] = {
	        source(){
	            return str
	        },
	        size(){
	           return str.length
	        }
	       }
	       callback&&callback()
	    })
	}else{
	    compiler.plugin('emit',function(complication,callback){
	        var str = '文件列表是:\n';
	        for(let k in complication.assets){
	          str += `文件名:${k} , 大小是 ${ complication.assets[k].size()} \n\n`
	        }
	        complication.assets['myfile.md'] = {
	          source(){
	            return str
	          },
	          size(){
	            return str.length
	          }
	        }
	        callback&&callback()
	    })
	}
	}
}
module.exports = Myplugin

简单描述一下webpack打包过程?

流程可以大致划分为 以下7个阶段

  1. 初始化参数:从配置文件webpack.config.js和 Shell 语句中读取与合并参数,得出最终的参数
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译
  3. 确定入口:根据配置中的 entry 找出所有的入口文件,一个入口就会形成一个依赖图路径,最后打成一个包。
  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
  5. 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会,一般对应compiler的emit事件。
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

webpack热更新

Webpack HMR 原理解析
Webpack 的热更新又称热替换(Hot Module Replacement),缩写为 HMR。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。 HMR的核心就是客户端从webpack服务端拉取更新后的文件信息,准确的说是chunk 需要更新的部分的模块哈希值。
webpack-dev-server(WDS) 与浏览器之间维护了一个 Websocket,当本地资源发生变化时,WDS 会向浏览器推送更新,并带上构建时的 hash,让客户端与上一次资源进行对比。
客户端对比出差异后会向 WDS 发起 Ajax 请求来获取更改内容(文件列表、hash值的JSON串),这样客户端就可以再借助这些信息继续向 WDS 发起 jsonp 请求获取该chunk的增量更新。
后续的部分(拿到增量更新之后如何处理?哪些状态该保留?哪些又需要更新?)由 HotModulePlugin 来完成,提供了相关 API 以供开发者针对自身场景进行处理。像react-hot-loader 和 vue-loader 都是借助这些 API 实现 HMR。

如何优化webpack打包?

在淘宝优化了一个大型项目,分享一些干货
「吐血整理」再来一打Webpack面试题
一般来说我们的优化有两个方向:

  1. 优化用户体验
    减少首屏加载时间
    提升各项交互的流畅度,如表单验证和页面切换
  2. 优化开发体验
    优化打包速度,减少构建耗时

在webpack中可以使用按需加载、异步加载分包、提取公共代码等方式去优化。

  1. 利用optimization.splitChunks分包。
    提取依赖包,把分包的正则属性配置路径为node_modules。
    提取重复模块,把重复的模块尽量提取到同一个chunk里,比如说设置一个common包的规则,一旦达到这个规则的公共代码就会被分出来一个包,一般来说设置chunks:'all’的属性即可。

为了保证首屏渲染时间,需要使用懒加载,也就是异步引入的方式优化,把首屏暂时用不到的包异步引入,这样分包的时候就会自动把这些文件提取到别的chunk里了。如果担心分的包太细碎会发起太多请求,可以使用MinSize和限制最高分包数等属性来限制。

  1. 开启 optimization.runtimeChunk 属性,将模块依赖关系单独拉出来一个分包,这样如果一个模块变动,与它相关的其他模块内容就不会跟着连锁变动了,名字hash值也不会变,就可以继续利用之前的缓存设置。

  2. 配置css和js文件的代码压缩:css-minimizer-webpack-plugin 和 webpack-parallel-uglify-plugin 插件。

  3. 可以通过多线程来提升Webpack打包速度的工具:HappyPack(不维护了)、thread-loader,一般用在babel-loader之类转译文件多、时间长的Loader上效果会比较明显。

  4. webpack5还提出了一个Module Federation(模块联邦)的属性配置,有利于加快应用启动和编译速度。 比如UMI的项目就可以直接在config里面配置MFSU之后,编译速度大大加快。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/385837.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

全国计算机等级考试二级,MySQL数据库考试大纲(2023年版)

基本要求: 1.掌握数据库的基本概念和方法。 2.熟练掌握MySQL的安装与配置。 3.熟练掌握MySQL平台下使用SQL语言实现数据库的交互操作。 4.熟练掌握 MySQL的数据库编程。 5.熟悉 PHP 应用开发语言,初步具备利用该语言进…

Vue-自定义属性和插槽(五)

目录 自定义指令 基本语法 (全局&局部注册) 指令的值 练习:v-loading 指令封装 总结: 插槽(slot) 默认插槽 插槽 - 后备内容(默认值) 具名插槽 具名插槽基本语法: 具名插槽简化语法: 作…

RocksDB:高性能键值存储引擎初探

在现代的分布式系统和大数据应用中,一个高效、可靠的存储引擎是不可或缺的。RocksDB,由Facebook于2012年开发并随后开源,正是为了满足这类需求而诞生的。它是一个持久化的键值存储系统,特别适合在闪存(Flash&#xff0…

AtCoder Beginner Contest 340 C - Divide and Divide【打表推公式】

原题链接:https://atcoder.jp/contests/abc340/tasks/abc340_c Time Limit: 2 sec / Memory Limit: 1024 MB Score: 300 points 问题陈述 黑板上写着一个整数 N。 高桥将重复下面的一系列操作,直到所有不小于2的整数都从黑板上移除: 选择…

SCI论文作图规范

SCI论文作图规范包括以下几个方面: 一、图片格式 SCI论文通常接受的图片格式包括TIFF、EPS和PDF等。其中,TIFF格式是一种高质量的图像格式,适用于需要高分辨率和颜色准确性的图片;EPS格式是一种矢量图形格式,适用于需…

Leetcode 1035 不相交的线

题意理解: 在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。 现在,可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直线需要同时满足满足: nums1[i] nums2[j]且绘制的直线不与任何其他连线&#xff…

Spring 事务

Spring 事务传播(Propagation)特性 REQUIRED 支持一个当前的事务,如果不存在创建一个新的。SUPPORTS 支持一个当前事务,如果不存在以非事务执行。MANDATORY 支持一个当前事务,如果不存在任何抛出异常。REQUIRES_NEW 创…

百面嵌入式专栏(面试题)驱动开发面试题汇总 2.0

沉淀、分享、成长,让自己和他人都能有所收获!😄 📢本篇我们将介绍驱动开发面试题 。 1、Linux系统的组成部分? Linux内核、Linux文件系统、Linux shell、Linux应用程序。 2、Linux内核的组成部分? (1)第一种分类方式:内存管理子系统、进程管理子系统、文件管理子系…

react【六】 React-Router

文章目录 1、Router1.1 路由1.2 认识React-Router1.3 Link和NavLink1.4 Navigate1.5 Not Found页面配置1.6 路由的嵌套1.7 手动路由的跳转1.7.1 在函数式组件中使用hook1.7.2 在类组件中封装高阶组件 1.8 动态路由传递参数1.9 路由的配置文件以及懒加载 1、Router 1.1 路由 1.…

VC++ 绘制折线学习

win32 有三个绘制折线的函数; Polyline,根据给定点数组绘制折线; PolylineTo,除了绘制也更新当前位置; PolyPolyline,绘制多条折线,第一个参数是点数组,第二个参数是一个数组、指…

论文阅读-One for All : 动态多租户边缘云平台的统一工作负载预测

论文名称:One for All: Unified Workload Prediction for Dynamic Multi-tenant Edge Cloud Platforms 摘要 多租户边缘云平台中的工作负载预测对于高效的应用部署和资源供给至关重要。然而,在多租户边缘云平台中,异构的应用模式、可变的基…

HTTP的基本格式

HTTP的基本格式 .HTTPhttp的协议格式HTTPhttp的协议格式 . HTTP 应用层,一方面是需要自定义协议,一方面也会用到一些现成的协议. HTTP协议,就是最常用到的应用层协议. 使用浏览器,打开网站,使用手机app,加载数据,这些过程大概率都是HTTP来支持的 HTTP是一个超文本传输协议, 文…

家政小程序系统源码开发:引领智能生活新篇章

随着科技的飞速发展,小程序作为一种便捷的应用形态,已经深入到我们生活的方方面面。尤其在家庭服务领域,家政小程序的出现为人们带来了前所未有的便利。它不仅简化了家政服务的流程,提升了服务质量,还为家政服务行业注…

最新wordpress外贸主题

日用百货wordpress外贸主题 蓝色大气的wordpress外贸主题,适合做日用百货的外贸公司搭建跨境电商网站使用。 https://www.jianzhanpress.com/?p5248 添加剂wordpress外贸建站主题 橙色wordpress外贸建站主题,适合做食品添加剂或化工添加剂的外贸公司…

pm2启动的node项目访问不了,npm start却可以访问

netstat -ntlp输入该命令,查看启动的服务端口是否有被监听到,如3001,4000之类的,是node项目启动时候自己配的那个, 若没有,则执行 pm2 delete [app-id/app-name] 先删除启动的这个项目 例如pm2 delete my…

【电路笔记】-并联电感

并联电感 文章目录 并联电感1、概述2、并联电感示例13、互耦并联电感器4、并联电感示例25、并联电感示例36、总结当电感器的两个端子分别连接到另一个或多个电感器的每个端子时,电感器被称为并联连接在一起。 1、概述 所有并联电感器上的压降将是相同的。 然后,并联的电感器…

Elasticsearch:适用于 iOS 和 Android 本机应用程序的 Elastic APM

作者:来自 Elastic Akhilesh Pokhariyal, Cesar Munoz, Bryce Buchanan 适用于本机应用程序的 Elastic APM 提供传出 HTTP 请求和视图加载的自动检测,捕获自定义事件、错误和崩溃,并包括用于数据分析和故障排除目的的预构建仪表板。 适用于 …

【go语言】一个简单HTTP服务的例子

一、Go语言安装 Go语言(又称Golang)的安装过程相对简单,下面是在不同操作系统上安装Go语言的步骤: 在Windows上安装Go语言: 访问Go语言的官方网站(golang.org)或者使用国内镜像站点&#xff0…

STM32 I2C

目录 I2C通信 软件I2C读写MPU6050 I2C通信外设 硬件I2C读写MPU6050 I2C通信 R/W:0写1读 十轴:3轴加速度,3轴角速度,3轴磁场强度和一个气压强度 软件I2C读写MPU6050 MyI2C.c #include "stm32f10x.h" …

Java中抽象类和接口的区别

抽象类和接口都是 Java 中多态的常见使用方式. 都需要重点掌握. 同时又要认清两者的区别(重要!!! 常见面试题)。 核心区别: 抽象类中可以包含普通方法和普通字段, 这样的普通方法和字段可以被子类直接使用(不必重写而重写抽象方法), 而接口中不能包含普通方法(接口…
最新文章