从今天起记录常见的前端面试题和答案

1、首先是这个牛逼的网址:https://www.cnblogs.com/chenwenhao/p/11267238.html#_label11
2、知乎干货回答:
https://zhuanlan.zhihu.com/p/354540389


cookie内部机制等等。cookie和session的关系

这篇文章写的蛮详细:https://blog.csdn.net/john1337/article/details/104571244
cookie由服务端生成,具有不可跨域名性。
后端使用setcookie() 函数生成了cookie之后会把它放在Set-Cookie的响应头字段,浏览器收到之后根据set-cookie这个指令把cookie设置到浏览器,浏览器根据domain和某域名发出的请求绑定,(满足一定条件下浏览器自动完成,无需前端代码辅助),使用Cookie请求头字段存放。
在这里插入图片描述
cookie:
1.可以实现多个页面之间数据共享
2.cookie保存在浏览器本地
3.cookie和域名是关联起来的。
4.默认如果cookie不设置过期时间的话,浏览器关闭 cookie就销毁了。
5.如果设置cookie的过期时间,cookie没有过期的时候,关闭浏览器在重新打开 cookie还是存在的


浏览器的渲染过程

在这里插入图片描述

1. 浏览器会将HTML解析成一个DOM树,DOM 树的构建过程是一个深度遍历过程:当前节点的所有子节点都构建好后才会去构建当前节点的下一个兄弟节点。

2. 将CSS解析成 CSS Rule Tree 。(上两步同步进行)

3. 根据上两棵树来构造 Rendering Tree。注意:Rendering Tree 渲染树并不等同于 DOM 树,因为一些像Header或display:none的东西就没必要放在渲染树中了。

4. 有了Render Tree,浏览器已经能知道网页中有哪些节点、各个节点的CSS定义以及他们的从属关系。下一步操作称之为layout,计算出每个节点在屏幕中的位置信息。

5. 再下一步就是绘制,即遍历render树,并使用UI后端层绘制每个节点。

浏览器解析文档的过程中,如果遇到 script 标签,会立即解析脚本,停止解析文档(因为 JS 可能会改变 DOM 和 CSS,如果继续解析会造成浪费)。
如果是外部 script, 会等待脚本下载完成之后在继续解析文档。


CSS Module

http://www.ruanyifeng.com/blog/2016/06/css_modules.html

总结:每一个css文件作为一个模块,比如react 内可以通过import style from ‘./App.css’;使用,通过styles.classname获取到每一个类。
css模块的使用是为了制造局部样式,防止css污染全局,每一个组件有他自己对应的样式,给按需引入提供了基础。
1、通过css-loader打包,每一个页面引入的css文件中的类名会被编译成随机数和hash组成的字符串,从而实现了局部变量的特性。
2、同时支持全局变量,使用:global(.className),css-loader将不会编译这个类名。
3、CSS Module还提供了样式复用的特性,使用composes 可以复用其他的样式。

/* Button.css */

.btn {
    /* 所有通用的样式 */
    color: #fff;
    border: none;
    border-radius: 5px;
    box-sizing: border-box;
}

.primary {
    composes: btn;
    background-color: #1aad19;
}

伪元素和伪类的区别

伪类是元素选择器的一种“:开头”,:first-child、:nth-child(),因为伪类是类似于添加类所以一个DOM节点可以有多个为元素

伪元素既可以使用“:”,也可以使用“::”,伪元素在一个选择器中只能出现一次,::before可以插在DOM节点之前,::after可以插在DOM结点之后,以及伪元素要配合content属性一起使用,它不会出现在DOM中,所以不能通过js来操作,仅仅是在 CSS 渲染层加入。


回流和重排怎么优化?

浏览器本身存在优化机制,因此我们主要是减少对render tree 的操作,减少一些style信息的请求。
1、尽量减少html标签嵌套深度,优化遍历速度。
2、将多次改变样式和Dom结构的操作合并成一次,比如插入数据最好不要一条一条插进去,而是把数据全部处理好了再统一返回,样式操作也是。
3、display:none的元素的变化不会触发重绘回流,因此可以把某些复杂变化的元素先隐藏了,等修改完再显示,这样就只会触发两次重绘了。对于不能隐藏的元素,比如复杂动画,可以使用绝对定位,让它脱离文档流再操作。
4、浏览器本身有一套优化机制,当用户使用元素的某些值,比如offsetWidth之类的会强制进行重绘,所以如果有多个地方使用这些值的时候最好缓存一下。
5、避免JS修改样式,尽量通过改变 class 名,这样当该元素的class属性可被操控时仅会产生一个reflow,避免设置多项内联样式,因为每个都会造成回流。

现代的浏览器都是很聪明的,由于每次重排都会造成额外的计算消耗,因此大多数浏览器都会通过队列化修改并批量执行来优化重排过程。浏览器会将修改操作放入到队列里,直到过了一段时间或者操作达到了一个阈值,才清空队列。但是!当你获取布局信息的操作的时候,会强制队列刷新,比如当你访问以下属性或者使用以下方法:
offsetTop、offsetLeft、offsetWidth、offsetHeight
scrollTop、scrollLeft、scrollWidth、scrollHeight
clientTop、clientLeft、clientWidth、clientHeight
getComputedStyle()
getBoundingClientRect
具体可以访问这个网站:https://gist.github.com/paulirish/5d52fb081b3570c81e3a
以上属性和方法都需要返回最新的布局信息,因此浏览器不得不清空队列,触发回流重绘来返回正确的值。因此,我们在修改样式的时候,最好避免使用上面列出的属性,他们都会刷新渲染队列。如果要使用它们,最好将值缓存起来。


性能优化指标

首屏渲染时间和白屏时间。

参考:

  1. Performance API调优:https://blog.csdn.net/z9061/article/details/101454438

白屏时间是指浏览器从响应用户输入网址地址,到浏览器开始显示内容的时间。
首屏时间是指浏览器从响应用户输入网络地址,到首屏内容渲染完成的时间。(基本上图片下载完就好了)

白屏时间 = 地址栏输入网址后回车 - 浏览器出现第一个元素
首屏时间 = 地址栏输入网址后回车 - 浏览器第一屏渲染完成

影响白屏时间的因素:网络,服务端性能,前端页面结构设计。
影响首屏时间的因素:白屏时间,资源下载执行时间。

优化首屏渲染:
1、本地缓存,CDN缓存,这样请求资源的时候就会从本地环境里取,速度加快。
2、懒加载减少首次请求的资源体积,体积小的图片使用base64和iconfont字体图标,还有使用预加载的加载动画,骨架屏等等。

CDN缓存:https://www.cnblogs.com/yfceshi/p/7220542.html

关于图片加载慢的问题:
1、图片懒加载。
2、图片先使用模糊缩略图,再根据用户视口窗加载真实大图。

在这里插入图片描述

第二种:
参考:https://www.cnblogs.com/wangmeijian/p/6822674.html

<!DOCTYPE html>
<html>
<head>
    <title>大图片加载从模糊到清晰</title>
    <style type="text/css">
    .content{
        position: relative;
    }
    .thumbnails{
        width: 300px;
        position: absolute;
        left: 0;
        top: 0;
        z-index: 1;
        filter: blur(4px);
        transition: all 0.7s;
    }
    .complete{
        filter: blur(0);
    }
    </style>
</head>
<body>
    <h3>大图片加载从模糊到清晰</h3>
    <div class="content">
        <img class="thumbnails" src="data:image/jpg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/xxxxx">
    </div>
    <script type="text/javascript">
        var ele = document.querySelector('.thumbnails');
        // 为了看到效果加个延时
        setTimeout(function(){
            // 若图片URL失效请自行替换
            var imgUrl = 'http://img8.zol.com.cn/bbs/upload/10569/10568721.jpg';
            var imgObject = new Image();

            imgObject.src = imgUrl;
            imgObject.onload = function(){
                ele.src = imgUrl;
                ele.setAttribute('class', 'thumbnails complete');
            }
        }, 1000)
        <script src='http://runjs.cn/gist/jwjikcng/all'></script>
    </script>
</body>
</html>

懒加载方式

懒加载其实就是减少一次性对服务器请求资源,导致性能降低的解决方法,

1、首先是图片懒加载。
对于图片,比较小格式的就使用base64或者字体图标、svg代替,比较大的图片可以使用懒加载,在图片标签里添加一个data-xxx的属性存放真实的图片路径,监听用户的下滑事件到这张图片的时候就把真实图片替换到src里。还有一种是先用模糊的图片base64放在src里,后面再淡入淡出替换,这样用户体验会比较好。
监听方法可以使用节流函数。

<script type="text/javascript">
	var imgs = document.querySelectorAll('img');
	var lazyload = function(){
		var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
		var winTop = window.innerHeight;
		for(var i=0;i < imgs.length;i++){
			if(imgs[i].offsetTop < scrollTop + winTop ){
				imgs[i].src = imgs[i].getAttribute('data-src');
			}
		}
	}
	function throttle(method,delay){
		var timer = null;
		return function(){
			var context = this, args=arguments;
			clearTimeout(timer);
			timer=setTimeout(function(){
				method.apply(context,args);
			},delay);
		}
	}
	window.onscroll = throttle(lazyload,200);
</script>

2、对于react来说,还可以进行路由懒加载和组件懒加载
在16以前(还没有React.lazy的时候)react-loadable插件比较常用。

主要是使用React.lazy和suspense配合。
路由懒加载底层原理:https://www.jianshu.com/p/61d6920c9e8f
需要Webpack处理捆绑包的动态导入,遇到lazy包裹的组件时就会将这部分代码单独分割成一个包,等到需要的时候再加载。(甚至可以精确到某一个值变动时才加载)
React.lazy必须通过调用动态的import()加载一个函数,此时会返回一个Promise,并解析(resolve)为一个带有包含React组件的默认导出的模块。

import {Route} from "react-router-dom";
import React,{Suspense} from 'react';

const LazyView = React.lazy(()=>import("./home"));
function App(){
	return <div> 
	<h1>路由懒加载</h1> 
	<Route path="/" exact render={()=>{
		return (
		<Suspense fallback={<div>组件Loading进来之前的占位内容</div>}>                    			   
			<LazyView /> 
		</Suspense>  }} />)
		 </div> }  
		 export default App;   

浏览器安全性

前端主要防范XSS 与 CSRF 两种跨站攻击

https://blog.tonyseek.com/post/introduce-to-xss-and-csrf/
https://blog.csdn.net/qq_1290259791/article/details/80920798

xss 跨站脚本攻击:
讲XSS的:https://blog.csdn.net/weixin_36135696/article/details/105576879
用户在输入层面插入攻击脚本,改变页面的显示,或则窃取网站 cookie。当用户浏览该页时,这些嵌入在html的恶意代码就会被执行,用户浏览器被攻击者控制。
常见攻击方法:1、在富文本中插入script脚本。2、用户输入信息的地方换成攻击脚本,存到数据库里,后来获取数据时就会执行脚本。
预防方法:
1、不相信用户的所有操作,对用户输入进行一个转义。比如把<>变成& lt;等。
2、富文本插入脚本,可以使用一个xss插件,对获取的html进行转义一次。
3、增删改数据库操作的接口必须用HTTP动词规定的,GET只能查询。(避免url内直接插入数据)

csrf 跨站请求伪造,以你的名义,发送恶意请求。
讲csrf 的https://blog.csdn.net/stpeace/article/details/53512283

(1)验证 HTTP Referer 字段
HTTP 头中有一个字段叫 Referer,它记录了该 HTTP 请求的来源地址。
问题: 如果真的是恶意用户,有方法是可以修改Referer的,所以这样判断并不安全。

(2)使用token而非cookie/session验证防止请求伪造。可以这么做,①在请求地址中添加 token 并验证。②在 HTTP 头中自定义属性并验证。把token放到 HTTP 头中自定义的属性里。

token是服务端生成的一串字符串,以作客户端进行请求的令牌,当第一次登陆后,服务器生成一个token便将此token返回给客户端,以后客户端只要带上这个token前来请求数据即可,无需再次带上用户名和密码。
token 可以在用户登陆后产生并放于 session 之中,然后在每次请求时把 token 从 session 中拿出,与请求中的 token 进行比对。


react-redux / redux / saga

1、使用react-redux + redux的具体案例:https://www.cnblogs.com/dongyuezhuang/p/11640151.html
2、Redux和React-Redux使用关系:https://blog.csdn.net/qq_42767631/article/details/83096818

saga其实就是一个redux中间件,帮助处理异步函数,否则redux只支持reducer纯函数。
react-redux是redux为了支持react专门开发的库,可以使用connect把组件和redux提供的state和使用action的方法,连接起来,作为props传入组件。
redux的重点是三个对象:store、action、reducer。重点通过provider传递所有store,也可以通过connect获取。
最后我发现还是dva简明易懂一点。所有的状态管理清楚多了。

react-router

原理:(其中中间有一段写的很生动清晰)https://www.jianshu.com/p/53dc287a8020
原理:(详细讲解)https://segmentfault.com/a/1190000004527878
react-router是一个前端控制路由的方案。主要基于history.js库,history.js库可以用来兼容在不同浏览器、不同环境下对历史记录的管理,拥有统一的API。history主要分成三类:
①老浏览器的history: 主要通过hash来实现,对应createHashHistory
②高版本浏览器: 通过html5里面的history,对应createBrowserHistory
③node环境下: 主要存储在memeory里面,对应createMemoryHistory

我自己的理解:react-router就相当于一个大型的页面展示控制组件,可以选择以什么样的方式记录组件对应的url(hash或者browser),其实我们只是在一个html页面内操作,它只是把url作为key值,选择对应的组件展示,每次进行页面跳转操作的时候,就调用history的前进后退等API把路径值记录在浏览器上,实现了单页面应用也可以跳转的功能。

闭包

阮一峰的闭包讲解:https://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html
讲的很清楚的(先看这个):https://zhuanlan.zhihu.com/p/22486908

闭包是函数内部的函数,这个内部函数可以通过作用域链访问外部函数的变量。

JS函数在运行时会生成一个执行环境(作用域)和作用域链,一般将闭包返回出去使用,这样就可以隐藏局部变量,外界无法直接修改函数数据,只能通过提供的闭包方法间接修改。

优点:
①形成了函数的私有变量,保证了执行的安全性,避免了污染全局变量。
②闭包在被引用时,这些变量的值始终保持在内存中,方便了后续使用。(假如f2是f1的闭包,把f2赋值给一个全局变量,那f2就会一直存在内存中,包括f2所在的函数作用域链。)
缺点:
①因为闭包会占用内存,因此尽量谨慎使用闭包,否则后期负担比较大。
②闭包泄露的问题主要是由于IE浏览器有bug,在使用完闭包之后,依然回收不了闭包里面引用的变量。其他浏览器基本已经不存在内存泄露的问题,但是出于谨慎起见,结束运行时还是最好释放内部变量,比如引用的DOM节点数据、定时器等。

宏任务和微任务(事件循环)

https://www.cnblogs.com/wangziye/p/9566454.html
https://zhuanlan.zhihu.com/p/78113300

跨域

1、JSONP:只能使用get请求,主要是以前ajax时候用,请求内带上datatype:jsonp。
2、Cors:主要由后端设置,服务器设定可以接收什么样的域名请求、请求方法、和请求头,主要由Acess-control-allow-origin / header / methods这三个请求头控制。
3、中间服务器转发:同源策略主要是针对浏览器与服务器之间的通讯来说的,服务器之间不用遵循这个标准,因此可以使用node建立中间服务器或者niginx建立跳板机做中转,中间服务器设置可以接收任何请求域名和请求头和方式即可,再转发给服务器

深比较和浅比较

其实主要是针对于对象来说的,主要应用是在PureComponent中,shouldComponent中使用浅比较优化性能。
深比较也称原值相等,深比较是指检查两个对象的所有属性是否都相等,深比较需要以递归的方式遍历两个对象的所有属性,操作比较耗时,深比较不管这两个对象是不是同一对象的引用。
浅比较:(看对象内的属性名是不是一样)https://blog.csdn.net/juzipidemimi/article/details/80892440

优化项目

大佬手把手教你怎么优化react项目:https://zhuanlan.zhihu.com/p/120748634
1、html的文档层级尽量少,引入css文件在header标签内,引入js文件在body最底部,避免js解析引擎阻塞渲染。
2、操作时尽量减少回流和重绘。处理DOM和改变样式最好一次性处理,不要一个个处理。少写内联样式,修改样式时直接修改class,这样只会进行一次重绘。使用浏览器会强制回流的属性,比如client\offset开头的属性时要缓存一下。(上面有更详细)
3、使用component时善用PureComponent,如果必须要有props造成的修改,也要使用shouldComponentUpdate做一下判断。使用reactHooks的时候可以使用memo包裹,善用useMemo和useCallback缓存组件和函数数据,对于传进子组件里的函数一定要用useCallback。
4、尽量减少请求数量,较小的图片使用base64或者iconfont、svg。开启http缓存。
5、首屏渲染优化,使用React.lazy进行懒加载,减少首屏加载的资源量,加快速度。(懒加载通过对组件进行分割打包成多个chunk来减少一次性加载的资源大小。从而减少用户不必要的等待。路由懒加载讲解)(SPA项目的缺点改进)
6、从优化打包、减少构建时间那方面来说:antd、lodash等库一定要按需引入,减少打包体积,不使用的包要及时删掉。使用tree-shaking和代码分割打包,减少冗余代码打包。生产环境和开发环境有一些打包策略区别,比如source-map这类辅助工具包,生产环境就可以不用加载了。

react hooks和component的对比(你为什么使用Hooks)

1、(精读《Function VS Class 组件》)
2、(精读《useEffect 完全指南》)
3、hooks对比:https://segmentfault.com/a/1190000021601144


1、相比起hooks来说,components比较"重",components起码要多写一个constructor初始化props和state,然后再用this.state.变量的形式去使用数据。
2、component内的数据是可变的,hook内的数据是不可变的,因为component数据是挂在this上的,实例是可变化的,所以component每次变化拿到的都是最新值,而hook的不可变指的是它每一次执行会生成一个当次渲染环境的快照,(闭包生成作用域)变量取值取得是渲染那次的环境值,如果想要取到最新值,第一可以使用useRef存最新值,但是它不会触发useEffect,或者使用useReducer也可以绕过不可变原则取到最新值。某些使用情况下来说还是Hook比较合理。function淡化了this的使用,减少了学习成本。
3、hooks的业务逻辑代码更加集中、更清晰,是什么依赖导致了业务数据变化很清楚,hooks是状态驱动数据,业务逻辑代码根据每个数据的状态变化集中在一起管理,不像components会根据生命周期分割,这样看上去更完整。
4、function组件更易于复用,不用写HOC。

常见设计模式

https://www.cnblogs.com/imwtr/p/9451129.html
学习设计模式,有助于写出可复用和可维护性高的程序
设计模式的原则是“找出 程序中变化的地方,并将变化封装起来”,它的关键是意图,而不是结构。

React生命周期

https://www.jianshu.com/p/514fe21b9914
两张图
在这里插入图片描述

在这里插入图片描述

ES6 promise

promise提供了一个异步编程的解决方法,它相当于一个容器,保存着一个未来将会完成的事件。
有了promise可以解决回调地狱的问题,使用链式调用的写法,将异步操作以同步操作的流程表达出来。Promise 提供统一的 API,使异步操作更方便。

《ES6解析----阮一峰》
Promise对象有以下两个特点。
(1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:Pending(进行中)、Resolved(已完成,又称 Fulfilled)和Rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从Pending变为Resolved和从Pending变为Rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由JavaScript引擎提供,不用自己部署。
resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从Pending变为Resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从Pending变为Rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
Promise实例生成以后,可以用then方法分别指定Resolved状态和Reject状态的回调函数。
then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为Resolved时调用,第二个回调函数是Promise对象的状态变为Reject时调用。

链式调用中某个promise再返回promise,调用顺序是把第一个(包括内部的)Promise全部执行完了,再执行下一个。

如果调用resolve函数和reject函数时带有参数,那么它们的参数会被传递给回调函数。reject函数的参数通常是Error对象的实例,表示抛出的错误;resolve函数的参数除了正常的值以外,还可能是另一个Promise实例,表示异步操作的结果有可能是一个值,也有可能是另一个异步操作,比如像下面这样。

var p1 = new Promise(function (resolve, reject) {
  // ...
});

var p2 = new Promise(function (resolve, reject) {
  // ...
  resolve(p1);
})
上面代码中,p1和p2都是Promise的实例,但是p2的resolve方法将p1作为参数,即一个异步操作的结果是返回另一个异步操作。

注意,这时p1的状态就会传递给p2,也就是说,p1的状态决定了p2的状态。如果p1的状态是Pending,那么p2的回调函数就会等待p1的状态改变;如果p1的状态已经是Resolved或者Rejected,那么p2的回调函数将会立刻执行。

var p1 = new Promise(function (resolve, reject) {
  setTimeout(() => reject(new Error('fail')), 3000)
})

var p2 = new Promise(function (resolve, reject) {
  setTimeout(() => resolve(p1), 1000)
})

p2
  .then(result => console.log(result))
  .catch(error => console.log(error))
// Error: fail
上面代码中,p1是一个Promise,3秒之后变为rejected。p2的状态在1秒之后改变,resolve方法返回的是p1。由于p2返回的是另一个 Promise,导致p2自己的状态无效了,由p1的状态决定p2的状态。所以,后面的then语句都变成针对后者(p1)。又过了2秒,p1变为rejected,导致触发catch方法指定的回调函数。

catch后的promise还会执行吗?reject后的promise还会执行吗?
1、catch是then的语法糖,catch后返回的promise是Fulfilled状态,因此会调用后续的then。
2、reject后的promise是拒绝态,首先会被then的第二个参数,reject(xx)捕获,如果没有第二个参数,就会向下冒泡到被catch捕获。(then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为Resolved时调用,第二个回调函数是Promise对象的状态变为Reject时调用。所以说首先会被then的第二个函数处理


经过测试,catch段执行完后,原有的promise 就结束了,而返回了一个新Fulfilled状态的Promise。之前自己所理解的,Promise的状态一但确定就不会再变了,所以造成了疑问function test(res) {
   //创建一个rejected状态的Promise
    return Promise.reject(res)
      .then(res => {   //此处不会执行!!!
            console.log(res += '!');
            return res;
        })
        .catch(res => {
            console.log(res);
            return res;  //catch执行完后,返回了一个新的Fulfilled的状态的Promise,
//等同于return Promise.resolve(res);所以后面的then会执行,而catch就不会执行了。
        })
        .then(res => {
            console.log(res += '!');  
        }) 
        .catch(res => {
            console.log(res+"?");
            return res;  
        });
}
test('hello').then((res) =>{
   //test方法中将错误统一栏截处理了,可以不返回内容,
   //然后此处判断res来确定要不要执行!!
});

JS继承,原型与原型链

仔细看:https://cloud.tencent.com/developer/article/1589613
总结:
JS内的继承有以下几种继承方式:
1、原型继承
实现在于重写了children的原型对象,subType.prototype中就会存在一个指针指向superType的原型对象。

function Parent(name){
	this.name=name;
}
function children(name){
	this.name = name;
}

// 直接将children的原型对象重写,赋值为Parent(),实现原型继承。
// 这样以后new生成的每一个children,都会有Parent的影子。
children.prototype = new Parent(); //(*)标记*底下说明有用

缺点:
①重写原型对象,导致在(*)之前定义在原型链上的方法直接被覆盖。
②指向的都是同一个Parent,对于复杂类型(Obj)存储方式是存储引用值,因此后面其他子类修改引用类型的属性,会直接修改同一个地址,影响到其他子类的继承。

2、构造函数继承
为了解决上面的问题引入了构造函数继承,利用call或者apply把父类中通过this指定的属性和方法复制(借用)到子类创建的实例中,因为this对象是在运行时基于函数的执行环境绑定的,因此可以实现实例的唯一性。
实现在于:在子类型构造函数的内部调用超类型构造函数。

function parent(){
  this.colors = ["red", "blue", "green"];
}
function child(){
   //继承了parent
  parent.call(this);
}
var instance1 = new child();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
var instance2 = new child();
alert(instance2.colors); //"red,blue,green"

缺点:方法都在构造函数中定义,因此函数复用就无从谈起了。而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。
(复用:在原本的基础上再加入新的方法和属性,子类在内部调用父类,方法也是在子类内部封装,如果instance1想要加入特定属性,就没有办法。

3、组合式继承
思路:使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。
就是:构造函数要通过定义的子类构造方法,这样this才会指向执行中的唯一值(把子类构造函数赋给子类原型的constructor),同时为了可以实现外部定义属性,新建时将子类的原型指向父类。

function SuperType(name){
  this.name = name;
  this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
   alert(this.name);
};
function SubType(name, age){
//继承属性
  SuperType.call(this, name);
  this.age = age;
}
//继承方法
*重点* SubType.prototype = new SuperType();
*重点* SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
    alert(this.age);
};

var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29

var instance2 = new SubType("Greg", 27);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27

缺点:每次定义实例要调用两次父类构造函数。

4、寄生组合式继承
3的加强版,ES5实现继承的最佳方式。
寄生:创建一个封装基础过程的函数,该函数内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。
寄生组合:修正组合式继承调用两次父类构造函数的缺点,那就是只有赋值prototype时候调用一次,之后在寄生函数里创建一个和父类无关的对象,把父类的所有东西复制给它。以后再调用prototype就只要调用这个"复制父类"。

   // 实现继承的核心函数
   function inheritPrototype(subType,superType) {
      function F() {};
      //F()的原型指向的是superType
      F.prototype = superType.prototype; 
      //subType的原型指向的是F()
      subType.prototype = new F(); 
      // 重新将构造函数指向自己,修正构造函数
      subType.prototype.constructor = subType; 
   }
   // 设置父类
   function SuperType(name) {
       this.name = name;
       this.colors = ["red", "blue", "green"];
       SuperType.prototype.sayName = function () {
         console.log(this.name)
       }
   }
   // 设置子类
   function SubType(name, age) {
       //构造函数式继承--子类构造函数中执行父类构造函数
       SuperType.call(this, name);
       this.age = age;
   }
   // 核心:因为是对父类原型的复制,所以不包含父类的构造函数,也就不会调用两次父类的构造函数造成浪费
   inheritPrototype(SubType, SuperType)
   // 添加子类私有方法
   SubType.prototype.sayAge = function () {
      console.log(this.age);
   }
   var instance = new SubType("Taec",18)
   console.dir(instance)

class继承(ES6继承)

ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。

Class 可以通过extends关键字实现继承。子类必须在constructor方法中调用super方法,否则新建实例时会报错。

Class的继承链
大多数浏览器的 ES5 实现之中,每一个对象都有__proto__属性,指向对应的构造函数的prototype属性。Class 作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。

(1)子类的__proto__属性,表示构造函数的继承,总是指向父类。

(2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。

class A {
}

class B extends A {
}

B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true

其他笔记:

1、理解constructor和new

https://blog.csdn.net/zzh1251994430/article/details/108966817
class内把constructor暴露出来了,不像ES5中使用child.prototype.constructor=child,class中,constructor内的数据就是它的构造函数,在构造函数内部调用父类的构造函数,super可以将方法数据添加到父类的this上,然后继承这个this作为自己的this,从而取得子类的this。
(super调用了父类构造函数,加工数据,最后返回回来变成class自己的)

constructor始终指向创建当前对象的构造函数。
new 一个对象,就是创建一个构造函数的实例。

constructor可以当做prototype的属性,
a.constructor = a._proto _.constructor = A.prototype.constructor =A
在这里插入图片描述

2、super
  1. Class中的 super(),它在这里表示父类的构造函数,用来新建父类的 this 对象
    super()相当于Parent.prototype.constructor.call(this)
  2. 子类没有自己的this对象,而是继承父亲的this对象,然后进行加工。如果不调用super,子类就得不到this对象

ES5的继承,实质上是先创造子类的实例对象this,然后再将父类的方法添加到this上(Parent.call(this)).
ES6的继承,需要先创建父类的this,子类调用super继承父类的this对象,然后再加工。

 class Demo{
	 constructor(x,y) {
	     this.x = x;
		 this.y = y;
	 }
	 customSplit(){
		 return [...this.y]
	 }
 }
 
 class Demo2 extends Demo{
	 constructor(x,y){
		 super(x,y);
	 }
	 customSplit(){
	 	return [...this.x]
	 }
	 task1(){
		 return super.customSplit();
	 }
	 task2(){
	 	return this.customSplit();
	 }
 }
 


工程化相关

1、CommonJS和ES6模块化的区别
export 和module.export ,import和required的区别
速读:https://www.cnblogs.com/Super-scarlett/p/12837057.html
详细:https://juejin.cn/post/6938581764432461854

  • CommonJS 模块输出的是一个值的拷贝 ,ES6 模块输出的是值的引用。

    CommonJS 模块输出的是值的拷贝,CommonJS会缓存基本类型元素,浅拷贝对象类型,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值,除非在模块外直接给变量赋值,用户只有通过模块提供的获取、修改函数才能修改内部值,拿到最新值。

    ES6 模块的运行机制与 CommonJS 不一样。ES6静态导入,动态引用,不会缓存值,等到脚本执行时,再根据编译时生成的静态接口去取值。

  • CommonJS 模块是动态导入,运行时加载,ES6 模块是静态导入,编译时输出接口。
    因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块是静态引入,JS引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。它的对外接口只是一种静态定义,使得在编译时就能确定依赖关系。

CommonJs支持动态导入,就是可以在语句中,使用require语法,来看如下案例。

let lists = ["./index.js", "./config.js"]
lists.forEach((url) => require(url)) // 动态导入

if (lists.length) {
    require(lists[0]) // 动态导入
}

ES6 模块是静态导入,就是Es Module语句import只能声明在该文件的最顶部,不能动态加载语句。
Es Module语句运行在代码编译时。

2、babel为什么能实现按需导入
babel原理(AST树):https://juejin.cn/post/6844904138073980942
只有静态引入的ES6 Module才支持按需导入,是因为ES6模块在编译阶段就可以生成引用,babel在编译阶段会将按需引入的包地址修改为模块地址,模块使用时只是按照提供的接口地址导入。

3、CommonJS和ES6的循环导入
“循环引用”(circular dependency)指的是,a脚本的执行依赖b脚本,而b脚本的执行又依赖a脚本。
通常,”循环引用"表示存在强耦合,如果处理不好,还可能导致递归加载,使得程序无法执行,因此应该避免出现。

commonjs
CommonJs的循环引用的重要原则:一旦出现某个模块被”循环引用”,就只输出已经执行的部分,还未执行的部分不会输出。
es6
Es6是动态只读引用,所以引入后只要有这个引入地址都可以执行,比方第一次执行a.js的时候是unsefined,但是经过b.js处理再引用a.js的值,就不是undefined,也可以照常输出。

webpack相关

本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。
基本概念讲解:https://zhuanlan.zhihu.com/p/101541041

Entry:编译入口,webpack 编译的起点
Compiler:编译管理器,webpack 启动后会创建 compiler 对象,该对象一直存活知道结束退出
Compilation:单次编辑过程的管理器,比如 watch = true 时,运行过程中只有一个 compiler 但每次文件变更触发重新编译时,都会创建一个新的 compilation 对象
Dependence:依赖对象,webpack 基于该类型记录模块间依赖关系
Module:webpack 内部所有资源都会以“module”对象形式存在,所有关于资源的操作、转译、合并都是以 “module” 为基本单位进行的
Chunk:编译完成准备输出时,webpack 会将 module 按特定的规则组织成一个一个的 chunk,这些 chunk 某种程度上跟最终输出一一对应
Loader:资源内容转换器,其实就是实现从内容 A 转换 B 的转换器
Plugin:webpack构建过程中,会在特定的时机广播对应的事件,插件监听这些事件,在特定时间点介入编译过程
AST树:抽象语法树,将js拆分成对象形式,提供了修改js的方式,是一颗二叉树。打个比方:babel编译:解析(parsing) — 将代码字符串转换成 AST抽象语法树,转译(transforming) — 对抽象语法树进行变换操作,生成(generation) — 根据变换后的抽象语法树生成新的代码字符串。

webpack打包流程

知乎万字长文,先看这个:https://zhuanlan.zhihu.com/p/363928061
这个说的比较简洁清晰,内含代码:https://www.cnblogs.com/yxy99/p/5852987.html
很长、很详细,从源码开始分析的一篇文章:https://www.jianshu.com/p/1b1291be8d6c

额外。作者详细说了自己是怎么优化webpack打包速度的,赞:https://segmentfault.com/a/1190000008377195

webpack打包流程,不知道有没糊,仔细看还是看得清的。
在这里插入图片描述

webpack 构建流程
Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程 :
1、初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。
2、创建编译器对象:用上一步得到的参数初始化 Compiler 对象。
2、开始编译:注入内置插件、加载所有配置的插件,初始化编译环境,执行Compiler的 run 方法开始执行编译。
3、确定入口:根据配置中的 entry 找出所有的入口文件。

4、编译模块:从入口文件出发,使用Loader处理模块,再找出该模块依赖的模块,加入依赖树图,继续使用Loader处理,递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。
5、完成模块编译:得到了每个模块被翻译后的最终内容以及依赖关系图。

6、输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。
7、输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。

编写plugin
这个写的挺好:https://blog.csdn.net/frontend_frank/article/details/106205260

plugin是一个具有 apply 方法的 JavaScript 对象。apply 方法会被 webpack compiler 调用,并且 compiler 对象可在整个编译生命周期访问。
plugin触发的原理是:complier在初始化编译环境的时候,初始化plugin,向plugin内的apply方法注入compiler,在apply方法内,plugin会在compiler对象挂监听事件,通过compiler.plugin(事件名称,fun(compilation, callback)) 函数注册,之后监听到 Webpack 广播出来的事件后,就可以通过 传入的compilation对象去操作 Webpack。

一个plugin例子

class HelloPlugin{
  // 在构造函数中获取用户给该插件传入的配置
  constructor(options){
  }
  // Webpack 会调用 HelloPlugin 实例的 apply 方法给插件实例传入 compiler 对象
  apply(compiler) {
    // 在emit阶段插入钩子函数,用于特定时机处理额外的逻辑;
    compiler.hooks.emit.tap('HelloPlugin', (compilation) => {
      // 在功能流程完成后可以调用 webpack 提供的回调函数;
    });
    // 如果事件是异步的,会带两个参数,第二个参数为回调函数,在插件处理完任务时需要调用回调函数通知webpack,才会进入下一个处理流程。
    compiler.plugin('emit',function(compilation, callback) {
      // 支持处理逻辑
      // 处理完毕后执行 callback 以通知 Webpack 
      // 如果不执行 callback,运行流程将会一直卡在这不往下执行 
      callback();
    });
  }
}
 
module.exports = HelloPlugin;

事件流机制 Tapable

webpack本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是Tapable。

Webpack 的 Tapable 事件流机制保证了插件的有序性,将各个插件串联起来, Webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条webapck机制中,去改变webapck的运作,使得整个系统扩展性良好。

Tapable也是一个小型的 library,是Webpack的一个核心工具。类似于node中的events库,核心原理就是一个订阅发布模式。作用是提供类似的插件接口。

webpack中最核心的负责编译的Compiler和负责创建bundles的Compilation都是Tapable的实例,可以直接在 Compiler 和 Compilation 对象上广播和监听事件。

常用loader、plugin

loader
对模块的源代码进行转换,将不同的语言转换为JS,或将内联图像转换为data url。如:文件,url-loader、file-loader。转换编译,babel-loader、ts-loader。模板,html-loader。样式,style-loader、css-loader、less-loader。清理,eslint-loader。框架,vue-loader。

plugin
解决loader无法实现的其他事儿。比如 HtmlWebpackPlugin、CleanWebpackPlugin、webpack-bundle-analyzer、DllPlugin、HotModuleReplacementPlugin。

常用:
uglifyJS:压缩js,减少代码量。
happyPack:多路复用,加快打包速度。

热更新
参考: https://www.cnblogs.com/magicg/p/13679273.html

webpack-dev-server插件可以实现项目的热更新。只需要局部刷新页面上发生变化的模块,同时可以保留当前的页面状态,比如复选框的选中状态、输入框的输入等。
一个带有热替换功能的webpack.config.js 文件的配置如下,做了这么几件事

  1. 引入了webpack库
  2. 使用了new webpack.HotModuleReplacementPlugin()
  3. 设置devServer选项中的hot字段为true

每一次Webpack打包都会对本次生成文件做一个唯一的hash标记值,webpack-dev-server会启动一个本地服务器,本地服务器通过websoket和客户端通信,webpack本地监听文件变化,一旦变化重新编译,然后通过websoket传递文件,webpack接收到通知后会检测更新后的hash标记,通知被打包到客户端上的webpack热更新文件,也就是 HotModuleReplacementPlugin插件进行局部替换。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值