函数的节流和防抖
防抖函数:防止抖动,将多次触发变成最后一次触发;代码实现重在清零clearTimeout
节流函数:控制流量,将多次执行变成每隔一个时间节点去执行,代码实现重在开锁关锁
应用场景:
防抖:1.浏览器窗口缩放,resize事件,监听浏览器窗口的resize事件(不管窗口怎么变化,只执行最后一一次,所以每次触发浏览器的resize事件时,应该重新触发回调函数,即响应事件)
- 表单的按钮提交事件,例如登录,发短信,避免用户点击太快,以至于发送了多次请求
- search搜索框输入,只需用户最后一次输入完在发送请求;
- 文本编辑器实时保存,当无任何更改操作1s后进行保存
节流:1. 鼠标不断点击触发,mousedown (单位时间内只触发一次) mousemove事件
- 商品预览图的放大镜效果
- input框实时搜索并发送请求展示下拉列表,没隔一一秒发送一次请求(也可做防抖)
- scroll事件,每隔1s计算一次位置信息
####使用场景
#####1.debounce
- search搜索联想,用户在不断输入值时,用防抖来节约请求资源。
- window触发resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发-次。
#####2.throttle
- 鼠标不断点击触发,mousedown(单位时间内只触发一次)
- 监听滚动事件,比如是否滑到底部加载更多,用throttle来判断
捕获和冒泡
捕获:就是从根元素开始向目标元素递进的一个关系;从上而下
冒泡:从目标元素开始向根元素冒泡的过程,即事件开始时由最具体的元素接收,然后逐级向上传播到较为不具体的节点;
事件委托:又叫事件代理,即事件在外层元素中的捕获;事件委托是利用事件的冒泡原理来实现的,就是事件从最深的节点开始,然后逐步向上传播事件;其实就是委托元素的父级代为执行事件。
用事件委托的时候,不需要去遍历元素的子节点,只需要给父级元素添加事件就好了,其他的都是在js里面的执行,这样可以大大的减少dom操作,这才是事件委托的精髓所在。
适合用事件委托的事件: click,mousedown, mouseup,keydown, keyup, keypress。值得注意的是,mouseover和mouseout虽然也有事件冒泡,但是处理它们的时候需要特别的注意,因为需要经常计算它们的位置,处理起来不太容易。
不适合的就有很多了,举个例子,mousemove,每次都要计算它的位置,非常不好把控,在比如说focus, blur之类的, 本身就没用冒泡的特性,自然就不能用事件委托了。
为什么要使用事件委托?
第一个好处是效率高,比如,不用for循环为子元素添加事件了
原理:每绑定一个事件处理器都是有代价的:
要么增加了页面负担(更多的JavaScript代码)
要么增加了运行期的执行时间(因为js代码多)
绑定事件时候访问或者修改dom越多,越耗时
浏览器跟踪每个事件处理器也需要耗更多的内存,并且并不是所有事件用户都会用到,很多事没有必要的。
第二个好处是,js新生成的子元素也不用新为其添加事件了,程序逻辑上比较方便
JS的运行机制
1. js单线程:JavaScript语言的一大特点就是单线程,即同一时间只能做一件事情。
2. js事件循环:js代码执行过程中会有很多任务,这些任务总的分成两类:同步任务和异步任务
需要注意的是除了同步任务和异步任务,任务还可以更加细分为macrotask(宏任务)和microtask(微任务),js引擎会优先执行微任务
微任务包括了 promise 的回调、node 中的 process.nextTick 、对 Dom 变化监听的 MutationObserver。
宏任务包括了 script 脚本的执行、setTimeout ,setInterval ,setImmediate 一类的定时事件,还有如 I/O 操作、UI 渲 染等。
首先js 是单线程运行的,在代码执行的时候,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行。
在执行同步代码的时候,如果遇到了异步事件,js 引擎并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务
当同步事件执行完毕后,再将异步事件对应的回调加入到与当前执行栈中不同的另一个任务队列中等待执行。
任务队列可以分为宏任务对列和微任务对列,当当前执行栈中的事件执行完毕后,js 引擎首先会判断微任务对列中是否有任务可以执行,如果有就将微任务队首的事件压入栈中执行。
当微任务对列中的任务都执行完成后再去判断宏任务对列中的任务。
Event Loop
Event Loop是一个程序结构,用于等待和发送消息和事件。JavaScript是种单线程语言,所有任务都在一个线程上完成。
一旦遇到大量任务或者遇到一个耗时的任务,比如加载一个高清图片,网页就会出现"假死”,因为JavaScript停不下来,也就无法响应用户的行为。为了防止主线程的阻塞,JavaScript 有了同步和异步的概念
浏览器可以理解成只有1个宏任务队列和1个微任务队列,先执行全局Script代码,执行完同步代码调用栈清空后,从微任务队列中依次取出所有的任务放入调用栈执行,微任务队列清空后,从宏任务队列中只取位于队首的任务放入调用栈执行,然后继续执行微队列中的所有任务,再去宏队列取一个,以此构成事件循环。
宏任务(macrotask) : setTimeout/setInterval
微任务(microtask) : Promise
宏任务和微任务
js中的一个机制,就是遇到宏任务,先执行宏任务,将宏任务放入eventqueue(事件队列),然后在执行微任务。
宏任务和微任务都包含一些事件
宏任务:setTimeout,setInterval,Ajax,DOM事件
微任务:Promise async/await
要想明白这个机制 就要理解js单线程。因为JS是单线程语言,只能同时做一件事儿。js任务需要排队顺序执行,如果一个任务时间过长,后边的任务也会等着。假如,我们在请求一个网址时,图片加载很慢,网页总不能一直卡不出来,这个时候就可以用异步来解决了
异步就是由单线程这个背景而来的,解决了单线程等待的这个问题,异步的特点不会阻塞后面代码的执行。也就是请求处理过程中,你不能闲着,会产生异步的请求,回头再处理,然后继续执行下面的请求
异步和单线程是相辅相成的,js是一门单线程脚本语言,所以需要异步来辅助
JS 如何基于 EventLoop 实现所谓的“异步”效果
在 Javascript 中本质上是基于栈的形式去执行我们的代码,但是执行任务是如何被推到栈中的呢?
这里我们就不得不提出事件队列的概念,所谓事件队列(Event Queue)正是负责将即将执行的函数发送到栈中进行处理,它队列数据结构保证所有发送执行的正确顺序。
栈中如果执行完毕时。此时 JavaScript 会继续进入事件队列(Event Queue)中查找是否存在需要执行的任务,如果存在那么会继续执行这个队列中的任务。(需要注意的是事件队列中的执行顺序是基于队列先进先出的顺序)。
比如这样的一个场景,假如我的页面正在执行一段逻辑时。此时用户点击了拥有绑定事件的按钮。那么此时这个事件会立即加入队列中,但是它并不会被立即执行。
它仍然需要等待队列前的所有排队任务被执行完毕之后才会被执行。
我们以浏览器中的 SetTimeout 为例来举一个简单的例子来帮助你理解这一过程:
function fn() {
const a = 'test'
console.log(a)
setTimeout(() => {
console.log('hello')
}, 0)
console.log(a+1)
}
fn();
1、fn() 被推入栈中。此时 JS 会在栈中调用这个函数,fn 首先会依次执行一行一行代码。
2、当在栈(fn)中处理 setTimeout 操作时,它会被发送到相应的定时器线程去处理,定时器线程等待指定时间满足后将该操作发送回事件队列。
需要注意的是时间满足后,定时器线程会将需要执行的 callback 函数发送到事件队列中,此时事件循环会检查当前栈中是否存在正在执行的函数。如果为空,则从事件队列中添加新函数推入栈中进行执行。如果不是,则继续处理当前栈中的函数调用。
Javascript 本身是单线程的,但我们可以借助浏览器相关的其他线程以及事件队列的机制来实现异步。
因此,我们基于这样的事件循环模型就实现了达到了所谓的“异步”效果
JS中typeof与instanceof的区别
typeof: 用来检测一个数据的类型,返回一个字符串,标识被检测数据的类型。
typeof一般只能返回如下几个结果: "number"、 "string"、"boolean"、 "object"、 "function"、"undefined"和"symbol"
typeof Symbol() // 'symbol' typeof null //’object' typeof undefined //'undefined' typeof true // boolean'
typeof NaN // 'number'
运算数为数字typeof(x) ="number"
字符串typeof(x) = "string"
布尔值typeof(x) = "boolean"
对象,数组和null typeof(x) ="object"
函数typeof(x) = "function"
typeof的运算数未定义,返回的就是"undefined"
Instanceof: 用来判断一个构造函数的prototype属性所指向的对象是否存在要检测对象的原型链上,返回是布尔值
语法:a instanceof b 例:[] instanceof Array
JS数据类型
JS的数据类型分为两种:原始数据类型和引用数据类型
1、基本数据类型包括: Number、String、Boolean、 Null、Undefined、Symbol(ES6)
2、引用数据类型包括:Object,Array,Function,Date
3.存储位置不同:原始数据类型存储在栈中,占据空间小,大小固定,属于频繁使用数据。引用数据类型存储在堆中,占据空间大,大小不固定。
4.传值方式不同:基本数据类型按值传递,无法改变一个基本数据类型的值。引用数据类型:按引用传递,引用类型值可改变。
JavaScript 中如何检测一个变量是一个 String 类型?
三种方法(typeof、constructor、Object.prototype.toString.call())
解析:
①typeof typeof('123') === "string" // true
typeof '123' === "string" // true
②constructor '123'.constructor === String // true
③Object.prototype.toString.call() Object.prototype.toString.call('123') === '[object String]' // true
详解Cookie, Session, Token
Token: 令牌,是用户身份的验证方式。
最简单的token组成:uid(用户唯-的身份标识)、time (当前时间的时间戳)、sign (签名)。对Token认证的五点认识:一个Token就是一些信息的集合;在Token中包含足够多的信息,以便在后续请求中减少查询数据库的几率;
服务端需要对cookie和HTTPAuthrorization Header进行Token信息的检查;
基于上一点,你可以用一套token认证代码来面对浏览器类客户端和非浏览器类客户端;因为token是被签名的,所以我们可以认为一个可以解码认证通过的token是由我们系统发放的,其中带的信息是合法有效的;
Session: 会话,代表服务器与浏览器的一次会话过程,这个过程是连续的,也可以时断时续。cookie中存放着一个
sessionID,请求时会发送这个ID;session因为请求(request对象)而产生;session是一个容器, 可以存放会话过程中的任何对象;session的创建与使用总是在服务端,浏览器从来都没有得到过session对象;session是一种http存储机制,目的是为武装的http提供持久机制。
Cookie: 储存在用户本地终端上的数据,服务器生成,发送给浏览器,下次请求统一网站给服务器。
cookie与session区别: cookie数据存放在客户端上,session数据放在服务器上;
cookie不是很安全,且保存数据有限;
session一定时间内保存在服务器上,当访问增多,占用服务器性能。
session与token区别:作为身份认证,token安全行比session好;
Session认证只是简单的把User信息存储到Session里;Token,如果指的是OAuth Token或类似的机制的话,提供的是认证和授权,认证是针对用户,授权是针对App。其目的是让某App有权利访问某用户的信息。
token与cookie: Cookie是不允许垮域访问的,但是token是支持的,前提是传输的用户认证信息通过HTTP头传输;
跨域
跨域:指的是浏览器不能执行其它网站的脚本,它是由浏览器的同源策略造成的,是浏览器对JavaScript施加的安全限制。同源策略:是浏览器最核心也是最基本的安全功能,所谓同源指的是:协议、域名、端口号都相同,只要有一个不相同,那么都是非同源。浏览器在执行脚本的时候,都会检查这个脚本属于哪个页面,即检查是否同源,只有同源的脚本才会被执行;而非同源的脚本在请求数据的时候,浏览器会报一个异常。有一点必须要注意:跨域并不是请求发不出去,请求能发出去服务端能收到请求并正常返回结果,只是浏览器阻止了返回数据的加载渲染
同源策略限制的情况:1、Cookie、LocalStorage 和IndexDB无法读取
- DOM和Js对象无法获得
- AJAX请求不能发送
注意:对于像img、iframe、script 等标签的src属性是特例,它们是可以访问非同源网站的资源的。
js跨域如何解决
目前暂时已知的跨域解决方法是:
1.jsonp跨域,原理: script标签没有跨域限制的漏洞实现的一种跨域方法,只支持get请求。安全问题会受到威胁。
2.cors跨域, CORS 全称叫跨域资源共享,通过后端服务器实现,给响应头加Access-Control Allow-Origin:*;
3.nginx反向代理
正向代理和反向代理
正向代理:
这里我再举一个例子:大家都知道,现在国内是访问不了Google的,那么怎么才能访问Google呢?我们又想,美国人不是能访问Google吗(这不废话,Google就是美国的),如果我们电脑的对外公网IP地址能变成美国的IP地址,那不就可以访问Google了。你很聪明,VPN就是这样产生的。我们在访问Google时,先连上VPN服务器将我们的IP地址变成美国的IP地址,然后就可以顺利的访问了。这里的VPN就是做正向代理的。
正向代理服务器位于客户端和服务器之间,为了向服务器获取数据,客户端要向代理服务器发送一个请求,并指定目标服务器,代理服务器将目标服务器返回的数据转交给客户端。这里客户端是要进行一些正向代理的设置的
PS:这里介绍一下什么是VPN,VPN通俗的讲就是一种中转服务,当我们电脑接入VPN后,我们对外IP地址就会变成VPN服务器的公网IP,我们请求或接受任何数据都会通过这个VPN服务器然后传入到我们本机。这样做有什么好处呢?比如VPN游戏加速方面的原理,我们要玩网通区的LOL,但是本机接入的是电信的宽带,玩网通区的会比较卡,这时候就利用VPN将电信网络变为网通网络,然后在玩网通区的LOL就不会卡了(注意: VPN是不能增加带宽的,不要以为不卡了是因为网速提升了)。
反向代理:
反向代理和正向代理的区别就是:正向代理代理客户端,反向代理代理服务器。
反向代理,其实客户端对代理是无感知的,因为客户端不需要任何配置就可以访问,我们只需要将请求发送到反向代理服务器,由反向代理服务器去选择目标服务器获取数据后,在返回给客户端,此时反向代理服务器和目标服务器对外就是一个服务器,暴露的是代理服务器地址,隐藏了真实服务器IP地址。
深度优先和广度优先
广度优先:尝试访问尽可能靠近它的目标节点,然后逐层向下遍历,直至最远的节点层级。
深度优先:从起始节点开始,一直向下找到最后一个节点,然后返回,又继续下一条路径。直到找遍所有的节点。
setTimeout与setInterval的区别???
setTimeout:间隔一段时间之后执行一次调用;
setInterval:每隔一段时间循环调用,直至清除。
业务场景的区别:setTimeout用于延迟执行某方法或功能。
setInterval则一般用于刷新表单,对于一些表单的假实时指定时间刷新同步。
语法:setTimeout(fun,time); //fun为一个函数,time为等待的时间。
等待time时间后,把要执行的任务(fun)加入到Event Queue中,又因为是单线程任务要一个一个执行,如果前面的任务需要的时间太久,那么只能等着,导致真正的延迟时间远远大于time。
setTimeout设置的时间,不是等主线程执行完开始计时,而是当执行到这条语句时就开始计时了~
如何实现JS异步加载
- 动态生成<script>标签:
- async属性: async是HTML 5的新属性,该属性规定一旦脚本可用,则会异步执行(一旦下载完毕就会立刻执行)。
需要注意的是: async属性仅适用于外部脚本(只有在使用src属性时)。
- defer属性: defer属性规定是否对脚本执行进行延迟,直到页面加载为止。
window.onload和DOMContentLoaded的区别
window.onload: DOM结构和静态资源加载完毕
DOMContentloaded: DOM结构加载完毕
$(document).load 和 $(document).ready 的区别
页面加载完成有两种事件
1.$(document).load是当页面所有资源全部加载完成后(包括DOM文档树,css文件,js文件,图片资源等),执行一个函数
问题:如果图片资源较多,加载时间较长,onload后等待执行的函数需要等待较长时间,所以一些效果可能受到影响
2.$(document).ready()是当DOM文档树加载完成后执行一个函数 (不包含图片,css等)所以会比load较快执行
在原生的js中不包括ready()这个方法,只有load方法也就是onload事件
jQuery与原生JS的对应关系: $(document).ready(function() { // ...代码... }); ---DOMContentLoaded
$(document).load(function() { // ...代码... }); --- onload
jQuery获取的dom对象和原生的dom对象有何区别?
js原生获取的dom是一个对象,jQuery对象就是一个数组对象,其实就是选择出来的元素的数组集合,所以说他们两者是不同的对象类型不等价。var $box = $('#box');var box = $box[0];
Proxy和Object.defineProperty的区别
Object.defineProperty():直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。IE8不兼容。
Proxy:用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等),proxy在目标对象的外层搭建了一层拦截,外界对目标对象的某些操作,必须通过这层拦截。IE不兼容。
- Proxy代理整个对象;Object defineProperty只代理对象上的某个属性,Proxy使用上比Object defineProperty方便的;
- vue中,Proxy在调用时递归;Object. defineProperty在一开始就全部递归,Proxy性能优于Object.defineProperty;
- 对象上定义新属性时,Proxy可以监听到;Object.defineProperty监听不到;
- 数组新增删除修改时,Proxy可以监听到;Object.defineProperty监听不到。
- Proxy不兼容IE;Object.defineProperty不兼容IE8及以下。
- Proxy会返回一个新的代理对象不会对原对象进行改动,只是对原对象进行代理并给出一个新的代理对象; Object.defineproperty是去修改原对象,修改原对象的属性。
注:vue3.0使用了Proxy替换了原先遍历对象使用Object. defineProperty方法给属性添加set,get访问器的笨拙做法。也就是说不应遍历了,而是直接监控data对象了。
浏览器缓存???
浏览器缓存:浏览器缓存就是把一个已经请求过的web资源(如html页面,图片,JS,数据)拷贝一份放在浏览器中。缓存会根据进来的请求保存输入内容的副本。当下一个请求到来的时候,如果是相同的URL,浏览器会根据缓存机制决定是直接使用副本响应访问请求还是向源服务器再次发起请求。
强制缓存:通过响应头实现: expires和cache-control。它表示在缓存期间不需要在发起请求。
协商缓存:如果缓存过期,可以使用协商缓存解决问题。协商缓存是需要发起请求。协商缓存需要客户端和服务端共同实现。
GET和POST两种基本请求方法的区别
浏览器中输入网址访问资源一般都是通过GET方式;在FORM提交中,可以通过Method指定提交方式为GET或者POST,默认为GET提交
Http协议定义了与服务器交互的不同方法,最基本的方法有4种,分别是GET、POST、PUT、DELETE。
URL全称是统一资源定位符,可以认为一个URL地址用于描述一个网络上的资源,而HTTP中的GET, POST, PUT, DELETE就对应着对这个资源的查,改,增,删4个操作。
在客户端和服务器之间进行请求---响应时,最常被用到的方法是GET和POST。
GET -从指定的资源请求数据,一般用于获取/查询资源信息。
POST -向指定的资源提交要被处理的数据,一般用于更新资源信息。
根据HTTP规范,GET用于信息获取,而且应该是安全的和幂等的:
- 所谓安全的意味着该操作用于获取信息而非修改信息。就是说,它仅仅是获取资源信息,不会修改,增加数据,不会影响资源的状态。
- 幂等的意味着对同一URL的多个请求应该返回同样的结果。
区别:
GET在浏览器回退时是无害的(回退操作实际上浏览器会从之前的缓存中拿结果),而POST会再次提交请求。
GET产生的URL地址可以被收藏为书签,而POST不可以。
GET请求会被浏览器主动cache,而POST不会,除非手动设置。
GET请求只能进行ur|编码,而POST支持多种编码方式。
GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。
GET请求在URL中传送的参数是有长度限制的,而POST么有。
对参数的数据类型,GET只接受ASCII字符,而POST没有限制。
GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。
GET参数通过URL传递,POST放在Request body中。
GET产生一个TCP数据包;POST产生两个TCP数据包:GET请求,浏览器会把http header和data-并发送出去,服务器响应200 (返回数据) ; POST请求,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok (返回数据)
1. GET与POST都有自己的语义,不能随便混用。
2.据研究,在网络环境好的情况下,发一次包的时间和发两次包的时间差别基本可以无视。而在网络环境差的情况下,两次包的TCP在验证数据包完整性上,有非常大的优点。
- 并不是所有浏览器都会在POST中发送两次包,Firefox就只发送一次
HTTP与HTTPS的区别
HTTP:超文本传输协议,是一个客户端和服务器端请求和应答的标准(TCP),
HTTPS:是以安全为目标的HTTP通道,是HTTP的安全版,即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。
1、https协议需要到CA申请证书,一般免费证书较少, 因而需要一定费用。
- http是超文本传输协议,信息是明文传输,https则是具有安全性的ssI/tls加密传输协议。
- http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
- http的连接很简单,是无状态的; HTTPS协议是由SSL/TLS+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。
webpack的构建过程
Webpack是一个前端资源打包工具。它根据模块的依赖关系进行静态分析,然后将这些模块按照指定的规则生成对应的静态资源。
6个核心概念:
入口(entry):入口起点(entrypoint)指示webpack应该使用哪个模块,来作为构建其内部依赖图的开始。
进入入口起点后,webpack会找出有哪些模块和库是入口起点(直接和间接)依赖的。每个依赖项随即被处理,最后输出到称之为bundles的文件中。
可以通过在webpack配置中配置entry属性,来指定一个入口起点(或多个入口起点)。默认值为./src
输出(output): output属性告诉webpack在哪里输出它所创建的bundles,以及如何命名这些文件,默认值为./dist.
基本上,整个应用程序结构,都会被编译到你指定的输出路径的文件夹中。你可以通过在配置中指定一个output字段,来配置这些处理过程。
Loader: loader让webpack能够去处理那些非JavaScript文件(webpack 自身只理解JavaScript)。loader 可以将所有类型的文件转换为webpack能够处理的有效模块,然后利用webpack的打包能力,对它们进行处理。
本质上,webpack loader将所有类型的文件,转换为应用程序的依赖图(和最终的bundle)可以直接引用的模块。
插件(plugins): loader被用于转换某些类型的模块,而插件则可以用于执行范围更厂的任务。播件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。插件接口功能极其强大,可以用来处理各种各样的任务。插件是webpack的支柱功能。webpack自身也是构建于你在webpack配置中用到的相同的插件系统之上!
插件目的在于解决loader无法实现的其他事。
想要使用一个插件,只需要require()它,然后把它添加到plugins数组中。
Module:模块,在Webpack中一切皆模块,一个模块即为一个文件。Webpack会从Entry开始递归找出所有的依赖模块。
Chunk:代码块,一个Chunk由多个模块组合而成,它用于代码合并与分割。
Webpack构建流程:
Webpack在启动后,会从Entry开始,递归解析Entry依赖的所有Module,每找到一个Module,就会根据Module.rules里配置的Loader规则进行相应的转换处理,对Module进行转换后,再解析出当前Module依赖的Module,这些Module会以Entry为单位进行分组,即为一个Chunk。因此一个Chunk,就是一个Entry及其所有依赖的Module合并的结果。最后Webpack会将所有的Chunk转换成文件输出Output。在整个构建流程中,Webpack会在恰当的时机执行Plugin里定义的逻辑,从而完成Plugin插件的优化任务。
如何实现图片懒加载
- 什么是懒加载:将图片src先赋值为一张默认图片,当用户滚动滚动条到可视区域图片时,再去加载后续真正的图片
- 为什么要引入懒加载:懒加载(LazyLoad)是前端优化的一种有效方式,极大的提升用户体验。图片一直是页面加载的流量大户,现在一张图片几兆已经是很正常的事,远远大于代码的大小。倘若一次ajax请求 10张图片的地址,一次性把10张图片都加载出来,肯定是不合理的。
- 懒加载实现的原理:先将img标签中的src链接设置为空,将真正的图片链接放在自定义属性(data-src),当js监听到图片元素进入到可视窗口的时候,将自定义属性中的地址存储到src中,达到懒加载的效果。
4.懒加载中涉及的属性:
1).document.documentElement.clientHeight; //表示浏览器 可见区域高度
document.body.clientHeight //是整个页面内容的高度,而非浏览器可见区域的高度
2).document.documentElement.scrollTop; //滚动条已滚动的高度:
chrome中 document. body.scrollTop //滚动条滚过的高度
那么要得到滚动条的高度:有一个技巧: var scrollTop=document.body.scrollTop IIdocument.documentElement.scrollTop;
这两个值总会有一个恒为0,所以不用担心会对真正的scrollTop造成影响。一点小技巧,但很实用。
3).offsetTop、offsetL eft
obj.offsetTop指obj距离上方或上层控件的位置,整型,单位像素。
obj.offsetLeft指obj距离左方或上层控件的位置,整型,单位像素。
obj.offsetWidth指obj控件自身的宽度,整型,单位像素。
obj. offsetHeight指obj控件自身的高度,整型,单位像素。
offsetParent不同于parentNode ,offsetParent返回的是在结构层次中与这个元素最近的position为absolute\relative\static的元素或者body
原型与原型链
原型:每个函数都有一一个prototype属性,定义函数时被自动赋值,值默认为{},即为原型对象
原型链:所有的实例对象都有_proto_ 属性指向构造函数的原型对象.访问一个对象的属性或方法时,先在对象自身属性中查找,如果没有,再沿着_proto_ 这条链向上找,这就是原型链。
作用域与作用域链
作用域:作用域就是变量与函数的可用范围。可以这样理解:作用域就是一个独立的地盘,让变量不会外泄、暴露出去。也就是说作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。
ES6之前,只有全局作用域和函数作用域。ES6之后,有了‘块级作用域’,可通过新增命令let和const来体现。
作用域链:由多级作用域组成的链式结构就是作用域链。
如何理解词法作用域与作用域链?
我们先对作用域做一个简单的介绍:
1、作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。程序设计概念,通常来说,一段程序代码中所用到的名字并不总是有效/可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。
2、ES5只有全局作用域没有函数作用域,ES6增加块级作用域
3、暂时性死区:在代码块内,使用 let 和 const 命令声明变量之前,该变量都是不可用的,语法上被称为暂时性死区。
4、JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。
5、函数的作用域在函数定义的时候就决定了。
词法作用域:
词法作用域就是在词法阶段定义的作用域。换句话说,词法作用域就是你在写代码的时候就已经决定了变量的作用域。
作用域链:
当查找变量先从上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
闭包
概念:当嵌套的内部函数引用了外部函数的变量时就产生了闭包
作用:1、延长局部变量的生命周期
2、让函数外部能操作内部的局部变量
应用:1、保护函数内部的变量
- 通过保护查安全实现JS私有属性和私有方法不能被外能访问;
- 使用闭包代替全局变量,防止变量污染。
使用场景:1、setTimeout传参,原生的setTimeout传递的第一个函数不能带参数,通过闭包可以实现传参效果。
2、函数防抖:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。
3、封装私有变量
优点:1、变量长期驻扎在内存中;
- 避免全局变量的污染;
- 私有成员的存在。
缺点:1、变量占用内存的时间可能会过长,占用的内存无法及时释放;
2、可能导致内存泄露
解决:及时释放:f= null; //让内部函数对象成为垃圾对象
内存溢出与内存泄漏
内存溢出:一种程序运行出现的错误,当程序运行需要的内存超过了剩余的内存时,就出抛出内存溢出的错误
内存泄露:占用的内存没有及时释放,内存泄露积累多了就容易导致内存溢出
常见的内存泄露:1、意外的全局变量;
2、没有及时清理的计时器或回调函数;
3、闭包
Javascript刷新页面的几种方法:
- window.history.go(0) //window.history.go(-n)表示返回上n级目录
- window.location.reload()
- window location=location
- windowlocation.assign(location)
- document execCommand('Refresh')
- window.navigate(location)
- window.location.replace(location)
- document.URL =location.href
自动刷新页面的方法:
1.页面自动刷新:把如下代码加入<head>区域中
<meta http-equiv="refresh"content= "20"> //其中20指每隔20秒刷新一次页面.
2.页面自动跳转:把如下代码加入<head>区域中
<meta http-equiv=' refresh"content=' 20;url=http://wwwwyxg.com"> //其中20指隔20秒后跳转到tp://wwwwxg.com页面
3.页面自动刷新js版
<script language="JavaScript">
function myrefresh(){
window.location.reload();]=
}
setTimeout('myrefresh()',1000); //指定1秒刷新一次
</script>
严格模式:
1、变量必须先声明,杜绝不小心将本地变量声明成一个全局变量
在常规模式下,如果我们声明一个变量时省略了var关键字,解析引擎会自动将其声明为全局变量,但在严格模式下,会直接抛出异常,不会为我们转为全局变量;
2、禁止删除变量和对象中不可删除的属性,显示报错
通过var声明的变量是不可删除的,在常规模式下,试图删除会静默失败,但在严格模式下会显式抛出异常;同样的,试图删除对象中不可删除的属性也会显式根错;
3、禁止对象属性重名
常规模式下,如果在对象中定义重复的属性,后定义的值会覆盖先定义的那个,ES5的严格模式规定,对象中不允许定义重复的属性,否则会显式报错。
4、禁止函数参数重名
常规模式下,如果在定义函数不小心声明了重复的参数名,后一个重名参数会覆盖前一个重名参数,
5、arguments不再追踪参数变化
常规模式下,在执行函数时如果更改参数的值,操作结果会立即反映到arguments对象中,反之,更改arguments对象 中的值,结果也会立即反映到参数上;
严格模式约束了这种行为,将arguments对象与参数分离,更改只会影响自己,不会对彼此都产生影响;
6、禁止使用arguments.callee,callee作为arguments对象的一个属性,可以在函数内部调用来获取当前正在执行的函数
7、禁止函数内部this关键字指向全局对象,this指向undefined???
常规模式下,JavaScript太过于灵活,如果对语言特性了解的不够深入,常常会因为失误的的调用,造成不一致的结果,8、函数必须声明在整个脚本或函数层面
9、新增一些保留字
ES5本质上来讲只是一个语言优化的过渡,约束了晦涩和不安全的语法,为以后的高级语法铺平道路。所以在ES5中新增了一些保留字,严格模式下,不能使用他们作为变量名或参数名:
implements, interface, let,package, private, protected,public, static, yield.
This指向
1、普通函数调用,this 指向window(严格模式指向undefined)
2、构造函数调用,this 指向实例对象
3、对象方法调用,this指向该方法所属的对象
4、通过事件绑定的方法,this 指向绑定事件的对象
5、回调函数,this指向window
6、定时器中的 this 指向 window
7、箭头函数没有自己的 this,会继承父作用域的 this。
判断是不是数组的几种方法
- 通过instanceof判断:检测Array.prototype属性是否存在实例的原型链上
a instanceof Array; //true
2、通过constructor判断:实例的构造函数属性constructor指向构造函数,constructor属性也可以判断是否为一个数组a.constructor === Array;//true
3、通过0bject.prototype.toString.call()判断:返回表示对象类型的字符串。
Object.prototype.toString.call(a) === '[object Array]’;//true
4、通过Array.isArray()判断:用于确定传递的值是否是一个数组,返回一个布尔值。
Array.isArray(a);//true
JS垃圾回收机制
垃圾回收基本思路:确定哪个变量不再使用,释放它占用的内存。这个过程是周期性的,即垃圾回收程序每隔一定时间就会自动运行。
机制:找出不再使用的变量,然后释放掉其占用的内存,但是这个过程不是实时的,因为其开销比较大,所以垃圾回收器会按照固定的时间间隔周期性的执行。
原理:垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存。
必要性:垃圾回收程序必须跟踪记录哪个变量还在使用,以及哪个变量不再使用,以便回收内存,否则,JavaScript的解释器将会消耗完系统中所有可用的内存,造成系统崩溃;
现在各大浏览器通常用采用的垃圾回收有两种方法:标记清除、引用计数。
标记清除:这是javascript中最常用的垃圾回收方式。当变量进入环境时,例如,在函数中声明一个变量,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。而当变量离开环境时,则将标记为“ 离开环境”。
垃圾收集器右在运行的时候会给存储在内存中的所有变量都加上标记。然后,它会去掉环境中的变量以及被环境中的变量引用的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间。
引用计数:另一种不太常见的垃圾回收策略是引用计数。引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其所占的内存空间给收回来。这样,垃圾收集器下次再运行时,它就会释放那些引用次数为0的值所占的内存。
减少JavaScript中的垃圾回收:
1、解除引用:将内存占用量保持在一个较小的值可以让页面性能更好,优化内存占用的最佳手段就是保证在执行代码时只保存必要的数据,如果数据不再必要,那么把它设置为null,从而释放其引用。
2、减少内存泄露:尽量少使用闭包;尽量使用let或const声明变量;及时清理定时器
说一说JS的IIFE
定义: IIFF- Immediately Invoked Function Expression,意为立即调用的函数表达式,也就是说,声明函数的同时立即调用这个函数。
目的: IIFE的目的是为了隔离作用域,防止污染全局命名空间。
Promise
实质: Promise是一个构造函数,有all、reject、resolve等静态方法,原型上有then、catch等同样很眼熟的方法
特点: 1、对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态:
Pending:初始状态,不是成功或失败状态。
Fulfiled:意味着操作成功完成。
Rejected:意味着操作失败。
只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
2、一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从Pending变为Resolved和从Pending变为Rejected。只要这两种情况发生,状态就不会再变了,会-直保持这个结果。当状态发生变化,promise. then绑定的函数就会被调用。
Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由JavaScript引擎提供,不用自己部署。resolve作用是将Promise对象状态由“未完成”变为‘成功”,也就是Pending -> Fulfilled, 在异步操作成功时调用,并将异步操作的结果作为参数传递出去;而reject函数则是将Promise对象状态由“未完成”变为“失败”,也就是Pending -> Rejected,在异步操作失败时调用,并将异步操作的结果作为参数传递出去。Promise.prototype.then方法:链式操作,Promise实例生成后,可用then方法分别指定两种状态回调参数。1 ->Promise对象状态改为Resolved时调用(必选) 2->Promise对象状态改为Rejected时调用(可选)
Promise.prototype.catch方法:捕捉错误,是Promise.prototype.then(null,rejection)的别名,用于指定发生错误时的回调函数。
Promise.all方法:用于将多个Promise实例,包装成一个新的Promise实例. Promise.all方法接受一个数组作参数,数组中的对象均为promise实例(如果不是一个promise,该项会被用Promise.resolve转换为一个promise)。它的状态由这三个promise实例决定。需要等待两个/多个异步事件完成后,再进行回调,当多个异步事件的状态都变为fufilled, Promise.all状态才会变为fufilled,并将三个promise返回的结果,按参数的顺序(而不是resolved的顺序)存入数组,传给成功的回调函数。当多个异步事件中有一个状态是rejected, Promise.all状态也会变为rejected,并把第一个被reject的promise的返回值,传给失败的回调函数
Promise.race方法:race本身就是赛跑的意思Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。all方法的效果实际上是「谁跑的慢,以谁为准执行回调」,race个方法的效果是「谁跑的快,以谁为准执行回调」.
Promise.resolve方法:可以看作是new Promise( (resolve,reject) => resolve(x))的简写,可以用于快速封装字面量对象或其他对象,将其封装成Prormise实例。状态为resolved.
Promise reject方法:可以看作是new Promise( (resolve,reject) => reject (x))的简写,返回一个状态为rejected的Promise实例。
async和await: async 是“异步”的简写,而await可以认为是async await的简写。
async可以理解成用于申明一个function是异步的,而await用于等待一个异步方法执行完成。
语法规定,await 只能出现在async函数中。async函数执行时,如果遇到await就会先暂停执行,等到触发的异步操作完成后,恢复async函数的执行。
async函数返回的是一个Promise对象,async/await的优势在于处理then链,让代码看起来更简洁,几乎跟同步代码一样;
注: promise返回的resolve对象可能用await去接,但是reject无法用await接收到,最好配合try catch进行错误处理
简单介绍promise链式调用的实现,以及返回的不同状态?
实现链式调用:使用.then()或者.catch()方法之后会返回一个promise对象,可以继续用.then()方法调用,再次调用所获取的参数是上个then方法return的内容
1、promise的三种状态是 fulfilled(已成功)/pengding(进行中)/rejected(已拒绝)
2、状态只能由 Pending --> Fulfilled 或者 Pending --> Rejected,且一但发生改变便不可二次修改;
3、Promise 中使用 resolve 和 reject 两个函数来更改状态;
4、then 方法内部做的事情就是状态判断:
如果状态是成功,调用成功回调函数
如果状态是失败,调用失败回调函数
Class类继承
ES6的class可以看作只是一-个语法糖,它的绝大部分功能,ES5都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。通过class关键字,可以定义类。里面有一个constructor方法,这就是构造方法;方法直接写在class内部,与constructor平级,方法之间不需要逗号分隔,加了会报错。使用new关键字调用,跟构造函数的用法完全一致。
如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
用extends关键字实现类之间的继承,super指向父类
注意:1、在类中声明方法的时候,方法前不加function关键字
2、方法之间不要用逗号分隔,否则会报错
3、类的内部所有定义的方法,都是不可枚举的(non-enumerable)
4、一个类中只能拥有一个constructor方法
5、类的数据类型就是函数,类本身就指向构造函数。使用的时候,也是直接对类
为什么要延迟加载js呢?
对于js的优化(关于js的延迟加载)的好处是有助于提高页面加载速度,js延迟加载就是等页面加载完成之后在加载js文件
之所以要优化是因为HTML元素是按其在页面中出现的次序调用的,如果用javascript来管理页面上的元素(使用文档对象模型dom),并且js加载于欲操作的HTML元素之前,则代码将出错。也就是说,我们写了js语句来获取DOM对象,但由于DOM结构还没有加载完成,因此获取到的是空对象。
js的同步加载和异步加载
同步加载:又称阻塞模式,是我们平时使用最多的方式,也就是直接将<script>写在<head>里。这种方式会阻止浏览器的后续处理,停止后续的解析,直到当前的加载完成。一般来说,同步加载是安全的,但如果我们js里设计到document内容输出、获取或修改DOM结构等行为,就会产生页面阻塞代码出错。所以一般就会建议把<script>写在页面最底部,以减少页面阻塞。(这种方式可能也是我们刚开始接触到js优化,最常使用的一种方式。)
异步加载:又称为非阻塞加载,在浏览器下载执行js的同时,还会继续后续页面的处理。这里也是一般面试会问到的一点,即js延迟加载的方式有哪些?
js延迟(异步)加载的6种方式,
defer属性、async属性、动态创建dom方式、使用jquery的getScript方法、使用setTimeout延迟方法、让js最后加载。
写的是六种方式,实际上自己在项目中真实用到的也就是让js最后加载。所以对这所谓的六种方式,可能仅作为一种知识储备,当以后的项目有这种问题需求了,可以有不同的解决思路。
1、defer属性:
HTML 4.01为<script>标签定义了defer属性(延迟脚本的执行)。其用途是:表明脚本在执行时不会影响页面的构造,浏览器会立即下载,但延迟执行,即脚本会被延迟到整个页面都解析完毕之后再执行。
defer属性只适用于外部脚本文件,只有IE支持defer属性。
并且defer属性解决了async引起的脚本顺序问题,使用defer属性,脚本将按照在页面中出现的顺序加载和运行。
2、async属性:
HTML 5为<script>标签定义了async属性。添加此属性后,脚本和HTML将一并加载(异步),代码将顺利运行。
浏览器遇到async脚本时不会阻塞页面渲染,而是直接下载然后运行。但这样的问题是,不同脚本运行次序就无法控制,只是脚本不会阻止剩余页面的显示。async属性只适用于外部脚本文件。
总结:defer和async的异同点:
相同:加载文件时不会阻塞页面渲染
对于内部的js不起作用
使用这两个属性的脚本中不能调用document.write方法
区别:如果脚本无需等待页面解析,且无依赖独立运行,那么应使用async。也就是每一个async属性的脚本都在它下载结束之后立即执行,同时会在window的load事件之前执行。如果脚本需要等待解析,且依赖于其它脚本,调用这些脚本时应使用defer,将关联的脚本按所需顺序置于HTML中。
3、动态创建DOM方式:
//这些代码应被放置在</body>标签前(接近HTML文件底部)
<script type-="text/javascript">
function downloadJSAtOnload() {
var element = document createElement("script");
element.src = "defer.js" ;
document body. appendChild(element);
}
if(window.addEventListener) { //添加监听事件
window.addEventListener("load",downloadJSAtOnload,false); //事件在冒泡阶段执行
} else if(window.attachEvent) {
window.attachEvent(" onload",downloadJSAtOnload);
} else {
window.onload = downloadJSAtOnload;
}
</script>
4、使用setTimeout延迟方法
5、让js最后加载:
将脚本元素放在文档体的底端(</body>标签前面),这样脚本就可以在HTML解析完毕后加载了,但此方案的问题是,只有在所有HTML DOM加载完成后才开始脚本的加载/解析过程。对于有大量js代码的大型网站,可能会带来显著的性能损耗
请用 js 去除字符串空格?
答案:replace 正则匹配方法、str.trim()方法
1、replace 正则匹配方法
去除字符串内所有的空格:str = str.replace(/\s\*/g,"");
去除字符串内两头的空格:str = str.replace(/^\s*|\s*\$/g,"");
去除字符串内左侧的空格:str = str.replace(/^\s\*/,"");
去除字符串内右侧的空格:str = str.replace(/(\s\*\$)/g,"");
2、str.trim()方法
trim()方法是用来删除字符串两端的空白字符并返回,trim方法并不影响原来的字符串本身,它返回是一个新的字符串。
缺陷:只能去除字符串两端的空格,不能去除中间的空格
适AT
JavaScript里面==和===的区别
==, 两边值类型不同的时候,要先进行类型转换,再比较。
===,不做类型转换,类型不同的一定不等。
=== 使用下面的规则用来判断两个值是否相等:
1、如果类型不同,就不相等,返回false ;
2、如果两个都是数值,并且是同一个值,那么相等,返回true;如果其中至少一个是NaN,那么不相等,返回false。(判断一个值是否是NaN,只能用isNaN()来判断)
3、如果两个都是字符串,每个位置的字符都一样,那么相等,返回true;否则不相等,返回false。
4、如果两个值都是true,或者都是false,那么那么相等,返回true。
5、如果两个值都引用同一个对象或函数,那么那么相等,返回true;否则不相等,返回false。
6、如果两个值都是null,或者都是undefined,那么相等,返回true。
== 使用如下规则:
1、如果两个值类型相同,进行 === 比较。
2、如果两个值类型不同,他们可能相等。根据下面规则进行类型转换再比较:
a、如果一个是null、一个是undefined,那么相等,返回true。
b、如果一个是字符串,一个是数值,把字符串转换成数值再进行比较。
c、如果任一值是 true,把它转换成 1 再比较;如果任一值是 false,把它转换成 0 再比较。
d、如果一个是对象,另一个是数值或字符串,把对象转换成基础类型的值再比较。对象转换成基础类型,利用它的toString或者valueOf方法。 js核心内置类,会尝试valueOf先于toString;例外的是Date,Date利用的是toString转换。
e、任何其他组合,那么不相等,返回false。
怎样添加、移除、移动、复制、创建和查找节点?
1)创建新节点: createDocumentFragment() //创建一个 DOM 片段
createElement() //创建一个具体的元素
createTextNode() //创建一个文本节点
2)添加、移除、替换、插入: appendChild() //添加
removeChild() //移除
replaceChild() //替换
insertBefore() //插入
3)查找: getElementsByTagName() //通过标签名称
getElementsByName() //通过元素的 Name 属性的值
getElementById() //通过元素 Id,唯一性
require 与 import 的区别????
https://blog.youkuaiyun.com/p3118601/article/details/100150075
遵循规范
require 是 AMD规范引入方式
import是es6的一个语法标准,如果要兼容浏览器的话必须转化成es5的语法
调用时间
require是运行时调用,所以require理论上可以运用在代码的任何地方(虽然这么说但是还是一般放开头)
import是编译时调用,所以必须放在文件开头
本质
require是赋值过程,其实require的结果就是对象、数字、字符串、函数等,再把require的结果赋值给某个变量
import是解构过程,但是目前所有的引擎都还没有实现import,我们在node中使用babel支持ES6,也仅仅是将ES6转码为ES5再执行,import语法会被转码为require
javascript 对象的几种创建方式
- 通过”字面量“ {} 方式创建
方法:将成员信息写到{}中,并赋值给一个变量,此时这个变量就是一个对象。
二、通过”构造函数“方式创建
方法:var obj = new 函数名();
这与通过类创建对象有本质的区别。通过该方法创建对象时,会自动执行该函数。这点类似于php通过创建对像时,会自动调用构造函数,因此该方法称为通过"构造函数“方式创建对象。
<script type="text/javascript">
function Person() {
this.name = "dongjc"; //通过this关键字设置默认成员
var worker = 'coding'; //没有this关键字,对象创建后,该变量为非成员
this.age = 32;
this.Introduce = function () {
alert("My name is " + this.name + ".I'm " + this.age);
};
alert("My name is " + this.name + ".I'm " + this.age);
};
var person = new Person();
person.Introduce();
</script>
此代码一共会两次跳出对话框,原因在于创建对象是自动执行了该函数。
三、通过object方式创建
方法:先通过object构造器new一个对象,再往里丰富成员信息。var obj = new Object();
<script type="text/javascript">
var person = new Object();
person.name = "dongjc";
person.age = 32;
person.Introduce = function () {
alert("My name is " + this.name + ".I'm " + this.age);
};
person.Introduce();
</script>
四、使用工厂模式创建对象(用函数来创建对象)
这种方式是使用一个函数来创建对象,减少重复代码,解决了前面三种方式的代码冗余的问题,但是方法不能共享的问题还是存在。
// 使用工厂模式创建对象
<script>
'use strict';
// 定义一个工厂方法
function createObject(name) {
var o = new Object();
o.name = name;
o.sayName = function() {
alert(this.name);
};
return o;
}
var o1 = createObject('zhang');
var o2 = createObject('li');
//缺点:调用的还是不同的方法
//优点:解决了前面的代码重复的问题
alert(o1.sayName === o2.sayName); //false
</script>
五、通过原型模式创建对象
每个方法中都有一个原型(prototype),每个原型都有一个构造器(constructor),构造器又指向这个方法。
举个例子:
function Animal(){}
alert(Animal.prototype.constructor==Animal);//true
原型创建对象:
<script>
'use strict';
// 原型模式创建对象
function Animal() {}
Animal.prototype.name = 'animal';
Animal.prototype.sayName = function() { alert(this.name); };
var a1 = new Animal();
var a2 = new Animal();
a1.sayName();
alert(a1.sayName === a2.sayName); //true
alert(Animal.prototype.constructor); //function Animal(){}
alert(Animal.prototype.constructor == Animal); //true
</script>
通过原型创建对象,把属性和方法绑定到prototype上,通过这种方式创建对象,方法是共享的,每个对象调用的是同一个方法。
- es6 class创建对象
class Person{
constructor(age, name) {
this.age = age;
this.name = name;
this.cry = function() { console.log(name + 'is crying!!! T^T'); }
}
sayName() { console.log(this.name); }
}
var person1 = new Person(11, '小白');
var person2 = new Person(12, '小黑');
JavaScript 继承的方式和优缺点
第一种:原型链继承

以上例子解释:①创建一个叫做Parent的构造函数,暂且称为父构造函数,里面有两个属性name、type②通过Parent构造函数的属性(即原型对象)设置Say方法,此时,Parent有2个属性和1个方法③创建一个叫做Son的构造函数,暂且称为子构造函数④设置Son的属性(即原型对象)值为父构造函数Parent的实例对象,即子构造函数Son继承了父构造函数Parent,此时Son也有2个属性和1个方法⑤对Son构造函数进行实例化,结果赋值给变量son1,即son1为实例化对象,同样拥有2个属性和1个方法⑥输出son1的Say方法,结果为"web前端"
优点:可以实现继承
缺点:①引用类型的属性被所有实例共享
②Son构造函数实例化对象无法进行参数的传递
因为Son.prototype(即原型对象)继承了Parent实例化对象,这就导致了所有Son实例化对象都一样,都共享有原型对象的属性及方法。代码如下:结果son1、son2都是['JS','HTML','CSS','VUE']

第二种:构造函数继承

以上例子解释:①创建父级构造函数Parent,有name、type两个属性②创建子级构造函数Son,函数内部通过call方法调用父级构造函数Parent,实现继承③分别创建构造函数Son的两个实例化对象son1、son2,对son1的type属性新增元素,son2没有新增,结果不一样,说明实现了独立

优点:①避免了引用类型的属性被所有实例共享
②还可以给实例化对象添加参数
缺点:①方法都在构造函数中定义,每次实例化对象都得创建一遍方法,基本无法实现函数复用
②call方法仅仅调用了父级构造函数的属性及方法,没有办法调用父级构造函数原型对象的方法
第三种:组合继承

以上例子解释:①创建一个叫做Parent的构造函数,里面有两个属性name、type②通过Parent构造函数的属性(即原型对象)设置Say方法,此时,Parent有2个属性和1个方法③创建子级构造函数Son,函数内部通过call方法调用父级构造函数Parent,实现继承④子构造函数Son继承了父构造函数Parent,此时Son也有2个属性和1个方法⑤分别创建构造函数Son的两个实例化对象son1、son2,传不同参数、给type属性新增不同元素、调用原型对象Say方法
优点:①利用原型链继承,实现原型对象方法的继承
②利用构造函数继承,实现属性的继承,而且可以参数组合函数基本满足了JS的继承,比较常用
缺点:无论什么情况下,都会调用两次父级构造函数:一次是在创建子级原型的时候,另一次是在子级构造函数内部
第四种:原型式继承
创建一个函数,将参数作为一个对象的原型对象

以上例子解释:①创建一个函数fun,内部定义一个构造函数Son②将Son的原型对象设置为参数,参数是一个对象,完成继承③将Son实例化后返回,即返回的是一个实例化对象
优缺点:跟原型链类似
第五种:寄生继承
在原型式继承的基础上,在函数内部丰富对象

以上例子解释:①再原型式继承的基础上,封装一个JiSheng函数②将fun函数返回的对象进行增强,新增Say方法,最后返回③调用JiSheng函数两次,分别赋值给变量parent1、parent2④对比parent1、parent2,结果为false,实现独立
优缺点:跟构造函数继承类似,调用一次函数就得创建一遍方法,无法实现函数复用,效率较低这里补充一个知识点,ES5有一个新的方法Object.create(),这个方法相当于封装了原型式继承。这个方法可以接收两个参数:第一个是新对象的原型对象(可选的),第二个是新对象新增属性,所以上面代码还可以这样:

第六种:寄生组合继承
利用组合继承和寄生继承各自优势组合继承方法我们已经说了,它的缺点是两次调用父级构造函数,一次是在创建子级原型的时候,另一次是在子级构造函数内部,那么我们只需要优化这个问题就行了,即减少一次调用父级构造函数,正好利用寄生继承的特性,继承父级构造函数的原型来创建子级原型。

造函数②利用Object.create(),将父级构造函数原型克隆为副本clone③将该副本作为子级构造函数的原型④给该副本添加constructor属性,因为③中修改原型导致副本失去默认的属性
优点:1. 这种方式的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了在 Parent. prototype 上面创建不必要的、多余的属性。
2. 与此同时,原型链还能保持不变;
3. 因此,还能够正常使用 instanceof 和 isPrototypeOf。
开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式
什么是原型链?
通过一个对象的__proto__可以找到它的原型对象,原型对象也是一个对象,就可以通过原型对象的__proto__,最后找到了我们的 Object. prototype, 从实例的原型对象开始一直到 Object. prototype 就是我们的原型链
复杂数据类型如何转变为字符串
1.首先,会调用 valueOf 方法,如果方法的返回值是一个基本数据类型,就返回这个值,
2.如果调用 valueOf 方法之后的返回值仍旧是一个复杂数据类型,就会调用该对象的 toString 方法,
3.如果toString方法调用之后的返回值是一个基本数据类型,就返回这个值,
4.如果toString方法调用之后的返回值是一个复杂数据类型,就报一个错误。
解析:
// 1;
var obj = {
valueOf: function() {
return 1;
}
};
console.log(obj + ""); //'1'
// 2;
var obj = {
valueOf: function() {
return [1, 2];
}
};
console.log(obj + ""); //'[object Object]';
// 3;
var obj = {
valueOf: function() {
return [1, 2];
},
toString: function() {
return 1;
}
};
console.log(obj + ""); //'1';
// 4;
var obj = {
valueOf: function() {
return [1, 2];
},
toString: function() {
return [1, 2, 3];
}
};
console.log(obj + ""); // 报错 Uncaught TypeError: Cannot convert object to primitive value
拓展:
var arr = [new Object(), new Date(), new RegExp(), new String(), new Number(), new Boolean(), new Function(), new Array(), Math]
console.log(arr.length) // 9
for (var i = 0; i < arr.length; i++) {
arr[i].valueOf = function() {
return [1, 2, 3]
}
arr[i].toString = function() {
return 'toString'
}
console.log(arr[i] + '')
}
1、若 return [1, 2, 3]处为 return "valueof",得到的返回值是 valueof toString 7valueof
说明:其他八种复杂数据类型是先调用 valueOf 方法,时间对象是先调用 toString 方法
2、改成 return [1, 2, 3],得到的返回值是 9toString
说明:执行 valueof 后都来执行 toString
javascript 的 typeof 返回哪些数据类型
7种,分别为number, boolean, string, undefined, object, function,symbol(ES6)
示例:
1、number:typeof(10);
typeof(NaN); // NaN在JavaScript中代表的是特殊非数字值,它本身是一个数字类型。
typeof(Infinity)
2、boolean:typeof(true);
typeof(false);
3、string:typeof("abc");
4、undefined:typeof(undefined);
typeof(a); // 不存在的变量
5、object: // 对象,数组,null返回object
typeof(null);
typeof(window);
6、function:typeof(Array);
typeof(Date);
- symbol:typeof Symbol() // ES6提供的新的类型
一次js请求一般情况下有哪些地方会有缓存处理?
答案:DNS缓存,CDN缓存,浏览器缓存,服务器缓存。
解析:
1、DNS缓存
DNS缓存指DNS返回了正确的IP之后,系统就会将这个结果临时储存起来。并且它会为缓存设定一个失效时间 (例如N小时),在这N小时之内,当你再次访问这个网站时,系统就会直接从你电脑本地的DNS缓存中把结果交还给你,而不必再去询问DNS服务器,变相“加速”了网址的解析。当然,在超过N小时之后,系统会自动再次去询问DNS服务器获得新的结果。
所以,当你修改了 DNS 服务器,并且不希望电脑继续使用之前的DNS缓存时,就需要手动去清除本地的缓存了。
本地DNS迟迟不生效或者本地dns异常等问题,都会导致访问某些网站出现无法访问的情况,这个时候我们就需要手动清除本地dns缓存,而不是等待!
2、CDN缓存
和Http类似,客户端请求数据时,先从本地缓存查找,如果被请求数据没有过期,拿过来用,如果过期,就向CDN边缘节点发起请求。CDN便会检测被请求的数据是否过期,如果没有过期,就返回数据给客户端,如果过期,CDN再向源站发送请求获取新数据。和买家买货,卖家没货,卖家再进货一个道理^^。
CDN边缘节点缓存机制,一般都遵守http标准协议,通过http响应头中的Cache-Control和max-age的字段来设置CDN边缘节点的数据缓存时间。
3、浏览器缓存
浏览器缓存(Browser Caching)是为了节约网络的资源加速浏览,浏览器在用户磁盘上对最近请求过的文档进行存储,当访问者再次请求这个页面时,浏览器就可以从本地磁盘显示文档,这样就可以加速页面的阅览。
浏览器缓存主要有两类:缓存协商:Last-modified ,Etag 和彻底缓存:cache-control,Expires。浏览器都有对应清除缓存的方法。
4、服务器缓存
服务器缓存有助于优化性能和节省宽带,它将需要频繁访问的Web页面和对象保存在离用户更近的系统中,当再次访问这些对象的时候加快了速度。
列举 3 种强制类型转换和 2 种隐式类型转换
强制: parseInt(), parseFloat(), Number(), Boolean(), String()
隐式: +, -
解析:
1.parseInt() 把值转换成整数:parseInt("1234blue"); // 1234
parseInt("0xA"); // 10
parseInt("22.5"); // 22
parseInt("blue"); // NaN
// parseInt()方法还有基模式,可以把二进制、八进制、十六进制或其他任何进制的字符串转换成整数。基是由parseInt()方法的第二个参数指定的,示例如下:
parseInt("AF", 16); // 175
parseInt("10", 2); // 2
parseInt("10", 8); // 8
parseInt("10", 10); // 10
// 如果十进制数包含前导0,那么最好采用基数10,这样才不会意外地得到八进制的值。例如:
parseInt("010"); // 8
parseInt("010", 8); // 8
parseInt("010", 10); // 10
// 2.parseFloat() 把值转换成浮点数,没有基模式:parseFloat("1234blue"); // 1234.0
parseFloat("0xA"); // NaN
parseFloat("22.5"); // 22.5
parseFloat("22.34.5"); // 22.34
parseFloat("0908"); // 908
parseFloat("blue"); // NaN
// 3.Number() 把给定的值转换成数字(可以是整数或浮点数),Number()的强制类型转换与parseInt()和parseFloat()方法的处理方式相似,只是它转换的是整个值,而不是部分值。示例如下:
Number(false) // 0
Number(true) // 1
Number(undefined) // NaN
Number(null) // 0
Number("5.5") // 5.5
Number("56") // 56
Number("5.6.7") // NaN
Number(new Object()) // NaN
Number(100) // 100
// 4.Boolean() 把给定的值转换成Boolean型
Boolean(""); // false
Boolean("hi"); // true
Boolean(100); // true
Boolean(null); // false
Boolean(0); // false
Boolean(new Object()); // true
// 5.String() 把给定的值转换成字符串
String(123) // "123"
// 6. + -
console.log(0 + '1') // "01"
console.log(2 - '1') // 1
你对闭包的理解?优缺点?
概念:闭包就是能够读取其他函数内部变量的函数。
三大特性:
* 函数嵌套函数。
* 函数内部可以引用外部的参数和变量。
* 参数和变量不会被垃圾回收机制回收。
优点:
* 希望一个变量长期存储在内存中。
* 避免全局变量的污染。
* 私有成员的存在。
缺点:
* 常驻内存,增加内存使用量。
* 使用不当会很容易造成内存泄露。
示例:
function outer() {
var name = "jack";
function inner() {
console.log(name);
}
return inner;
}
outer()(); // jack
function sayHi(name) {
return () => {
console.log( `Hi! ${name}` );
};
}
const test = sayHi("xiaoming");
test(); // Hi! xiaoming
虽然 sayHi 函数已经执行完毕,但是其活动对象也不会被销毁,因为 test 函数仍然引用着 sayHi 函数中的变量 name,这就是闭包。
但也因为闭包引用着另一个函数的变量,导致另一个函数已经不使用了也无法销毁,所以闭包使用过多,会占用较多的内存,这也是一个副作用。
解析:
由于在 ECMA2015 中,只有函数才能分割作用域,函数内部可以访问当前作用域的变量,但是外部无法访问函数内部的变量,所以闭包可以理解成“定义在一个函数内部的函数,外部可以通过内部返回的函数访问内部函数的变量“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。
new 一个对象的过程中发生了什么
function Person(name) {
this.name = name;
}
var person = new Person("qilei");
// new一个对象的四个过程:
// 1. 创建空对象;
var obj = {};
// 2. 设置原型链: 设置新对象的 constructor 属性为构造函数的名称,设置新对象的__proto__属性指向构造函数的 prototype 对象;
obj.constructor = Person;
obj.__proto__ = Person.prototype;
// 3. 改变this指向:使用新对象调用函数,函数中的 this 指向新实例对象obj:
var result = Person.call(obj); //{}.构造函数();
// 4. 返回值:如果无返回值或者返回一个非对象值,则将新对象返回;如果返回值是一个新对象的话那么直接返回该对象。
if (typeof(result) == "object") {
person = result;
} else {
person = obj;
}
for in 和 for of
1、for in
1. 一般用于遍历对象的可枚举属性。以及对象从构造函数原型中继承的属性。对于每个不同的属性,语句都会被执行。
2. 不建议使用 for in 遍历数组,因为输出的顺序是不固定的。
3. 如果迭代的对象的变量值是 null 或者 undefined, for in 不执行循环体,建议在使用 for in 循环之前,先检查该对象的值是不是 null 或者 undefined。
2、for of
1. for…of 语句在可迭代对象(包括 Array,Map,Set,String,TypedArray,arguments 对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句。
解析:
var s = {
a: 1,
b: 2,
c: 3
};
var s1 = Object.create(s);
for (var prop in s1) {
console.log(prop); //a b c
console.log(s1[prop]); //1 2 3
}
for (let prop of s1) {
console.log(prop); //报错如下 Uncaught TypeError: s1 is not iterable
}
for (let prop of Object.keys(s1)) {
console.log(prop); // a b c
console.log(s1[prop]); //1 2 3
}
如何判断 JS 变量的一个类型(至少三种方式)
答案:typeof、instanceof、 constructor、 prototype
解析:
1、typeof
typeof 返回一个表示数据类型的字符串,返回结果包括:number、boolean、string、object、undefined、function等6种数据类型。如果是判断一个基本的类型用typeof就是可以的。
typeof ''; // string 有效
typeof 1; // number 有效
typeof true; //boolean 有效
typeof undefined; //undefined 有效
typeof null; //object 无效
typeof []; //object 无效
typeof new Function(); // function 有效
typeof new Date(); //object 无效
typeof new RegExp(); //object 无效
2、instanceof
instanceof 是用来判断 A 是否为 B 的实例对,表达式为:A instanceof B,如果A是B的实例,则返回true, 否则返回false。在这里需要特别注意的是:instanceof检测的是原型
[] instanceof Array; //true
{} instanceof Object; //true
new Date() instanceof Date; //true
3、constractor
每一个对象实例都可以通过 constrcutor 对象来访问它的构造函数 。JS 中内置了一些构造函数:Object、Array、Function、Date、RegExp、String等。我们可以通过数据的 constrcutor 是否与其构造函数相等来判断数据的类型。
var arr = [];
var obj = {};
var date = new Date();
var num = 110;
var str = 'Hello';
var getName = function() {};
var sym = Symbol();
var set = new Set();
var map = new Map();
arr.constructor === Array; // true
obj.constructor === Object; // true
date.constructor === Date; // true
str.constructor === String; // true
getName.constructor === Function; // true
sym.constructor === Symbol; // true
set.constructor === Set; // true
map.constructor === Map // true
4、Object. prototype. toString
toString是Object原型对象上的一个方法,该方法默认返回其调用者的具体类型,更严格的讲,是 toString运行时this指向的对象类型, 返回的类型格式为[object, xxx], xxx是具体的数据类型,其中包括:String, Number, Boolean, Undefined, Null, Function, Date, Array, RegExp, Error, HTMLDocument, ... 基本上所有对象的类型都可以通过这个方法获取到。
Object.prototype.toString.call(''); // [object String]
Object.prototype.toString.call(1); // [object Number]
Object.prototype.toString.call(true); // [object Boolean]
Object.prototype.toString.call(undefined); // [object Undefined]
Object.prototype.toString.call(null); // [object Null]
Object.prototype.toString.call(new Function()); // [object Function]
Object.prototype.toString.call(new Date()); // [object Date]
Object.prototype.toString.call([]); // [object Array]
Object.prototype.toString.call(new RegExp()); // [object RegExp]
Object.prototype.toString.call(new Error()); // [object Error]
for in、Object.keys和Object.getOwnPropertyNames对属性遍历有什么区别
* for in 会遍历自身及原型链上的可枚举属性
* Object. keys 会将对象自身的可枚举属性的 key 输出
* Object. getOwnPropertyNames会将自身所有的属性的 key 输出
解析:
ECMAScript 将对象的属性分为两种:数据属性和访问器属性。
var parent = Object.create(Object.prototype, {
a: {
value: 123,
writable: true,
enumerable: true,
configurable: true
}
});
// parent继承自Object.prototype,有一个可枚举的属性a(enumerable:true)。
var child = Object.create(parent, {
b: {
value: 2,
writable: true,
enumerable: true,
configurable: true
},
c: {
value: 3,
writable: true,
enumerable: false,
configurable: true
}
});
//child 继承自 parent ,b可枚举,c不可枚举
1、for in
for (var key in child) {
console.log(key);
}
// b
// a
// for in 会遍历自身及原型链上的可枚举属性
如果只想输出自身的可枚举属性,可使用 hasOwnProperty 进行判断(数组与对象都可以,此处用数组做例子)
let arr = [1, 2, 3];
Array.prototype.xxx = 1231235;
for (let i in arr) {
if (arr.hasOwnProperty(i)) {
console.log(arr[i]);
}
}
// 1
// 2
// 3
2、Object. keys
console.log(Object.keys(child));
// ["b"]
// Object.keys 会将对象自身的可枚举属性的key输出
3、Object. getOwnPropertyNames
console.log(Object.getOwnPropertyNames(child));
// ["b","c"]
// 会将自身所有的属性的key输出
javaScript单一原则
单一原则就是一个对象、一个方法或者一个模块,只做一件事或者只对一类行为者负责。
遵循单一职责的优点:
1、可以降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多。
2、提高类的可读性,提高系统的可维护性。
3、变更引起的风险降低,变更时必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。
javaScript接口隔离原则?
接口隔离原则(英语:interface-segregation principles, 缩写:ISP)指明客户(client)应该不依赖于它不使用的方法,每一个接口都是一个角色,客户端不应该依赖于他不需要的接口。接口隔离原则(ISP)的目的是使系统解耦,从而容易重构,更改和重新部署。
扩展:
1、从字面意思上看,"单一职责原则"是从开发者的角度出发,"接口隔离原则"是从使用者的角度去考虑。如果说"单一职责原则"是对某一类行为的划分,而这行为的划分粒度就要取决于模块的使用者。
2、一个类对另一个类的依赖应该建立在最小的接口上。应该把每一个接口都细化,针对类去设计接口。如果一个接口中有太多方法,而对很多类来说里面的很多方法都是用不到的,那么,另外的类在实现这个接口时就要实现很多对它来说没用的方法,浪费人力物力。对一个类来说,实现很多它都能用得上的专用接口总比让它实现一个臃肿而又有很多它用不上的方法要来的划算。
javaScript开闭原则?
开闭原则:英文全称:Open Closed Principle(OCP)。开闭原则是对扩展开放,对修改关闭。也就是:添加一个新的功能时,应该在已有代码的基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。
扩展:
1、一个良好的系统应该是稳定的、灵活的。稳定:不会因为需求小小的变动而导致系统大幅度修改,即系统对修改的容忍程度比较高。灵活:软件系统应该在不需要修改的前提下可以轻易扩展。
2、对修改关闭,并不意味着不做任何修改,底层模块的变更,必然要有高层模块进行耦合,否则就是一个孤立无意义的代码片段。
3、开闭原则好处:
开闭原则有利于进行单元测试
开闭原则可以提高复用性
开闭原则可以提高可维护性
面向对象开发的要求
谈谈javaScript中__proto__、constructor和prototype?

1、__proto__和constructor属性是对象所有的。Function是对象,那么它也拥有这两个属性。
2、prototype属性是函数(Function)所独有的。Function本身是个对象,它的原型(Function.prototype)也是对象。
介绍一下ES6 中新增的 Symbol 类型?
简单介绍:ES6 中新增的 Symbol 类型,表示独一无二的值,它是JavaScript语言的第7种数据类型,前6种是Undefined、Null、Boolean、Number、String和Object。
产生背景:ES5的对象属性名都是字符串,很容易造成属性名冲突。比如,使用了一个他人提供的对象,想为这个对象添加新的方法,新方法的名字就有可能与现有方法产生冲突。如果有一种机制,保证每个属性的名字都是独一无二的,这样就从根本上防止了属性名冲突。这就是ES6引入Symbol的原因。
使用方法:Symbol 值通过Symbol函数生成。这就是说,对象的属性名可以有两种类型:一种是字符串,另一种是Symbol类型。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突
const mySymbol = Symbol();
const obj = {};
obj[mySymbol] = 'hello';
const mySymbol = Symbol();const obj = {};obj[mySymbol] = 'hello'; // Symbol值作为对象属性名时不能使用点运算符。obj.mySymbol // undefined
javaScript各数据类型内存分布?
栈:原始数据类型(Undefined Null Boolean Number String Symbol)
堆:引用数据类型(对象、数组和函数)
【扩展】
原始数据类型直接存储在栈(stack)中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储;
引用数据类型存储在堆(heap)中的对象,占据空间大、大小不固定,如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体
栈区(stack): 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。
数据结构中的栈则是一种相当简单的结构。就像是只有一个口的深深的文件桶,先进去的文件会被压在下面(push),而且我们每次只能取到最上面的文件(pop),体现了其先进后出(FILO)的特性。
数据结构中的堆是一种优先的队列,所谓优先队列是按照元素的优先级取出元素。举个例子,一般在饭桌上,无论你是先来后到,应该先是爷爷奶奶辈先动筷子,后面是父母,之后是你。
介绍一下对象内部属性[[Class]]?
所有的typeof返回值为‘object’的对象都包含一个内部属性[[Class]],我们将它可以看做内部的分类,而非传统面向对象意义的分类。这个属性无法直接访问,一般通过Object.prototype.toString来查看。
数组,正则表达式,对象的内部属性[[Class]]和创建该对象的内建原生构造函数相对应【直接调用了顶级原型Object上的toString方法】
console.log(Object.prototype.toString.call([1,2,3])); //[object Array]
console.log(Object.prototype.toString.call(/\d/)); //[object RegExp]
Null(),Undefined()这样的原生构造函数不存在,但是内部属性值仍然是 'Null' 'Undefined'
console.log(Object.prototype.toString.call(null)); //[object Null]
console.log(Object.prototype.toString.call(undefined)); //[object Undefined]
基本类型值被各自的封装对象自动包装,所以它们的内部属性值是 'String' 'Boolean' 'Number'【基本类型值没有.length和.toString()这样的属性和方法,通过封装继承了Obejct的同时,重写了一些方法,在查找toString方法的时候,优先找到自己原型链上的方法】
console.log(Object.prototype.toString.call("abc")); //[object String]
console.log(Object.prototype.toString.call(42)); //[object Number]
console.log(Object.prototype.toString.call(true)); //[object Boolean]
为什么JavaScript最大安全整数是2^53-1
在 JavaScript 中, Number 是一种 定义为 64位双精度浮点型(double-precision 64-bit floating point format) (IEEE 754)的数字数据类型。
【备注】所有数字以二进制存储,每个数字对应的二进制分为三段:符号位、指数位、尾数位。
比如说存储 8.8125 这个数,它的整数部分的二进制是 1000,小数部分的二进制是 1101。这两部分连起来是 1000.1101,但是存储到内存中小数点会消失,因为计算机只能存储 0 和 1。
1000.1101 这个二进制数用科学计数法表示是 1.0001101 * 2^3,这里的 3 (二进制是 0011)即为指数。
现在我们很容易判断符号位是 0,尾数位就是科学计数法的小数部分 0001101。指数位用来存储科学计数法的指数,此处为 3。指数位有正负,11 位指数位表示的指数范围是 -1023~1024,所以指数 3 的指数位存储为 1026(3 + 1023)。
可以判断 JavaScript 数值的最大值为 53 位二进制的最大值:2^53 -1。
Polyfill是什么?jQuery算是一个 Polyfill吗?
Polyfill 指的是用于实现浏览器并不支持的原生 API 的代码。
if(!Number.isNaN) {
Number.isNaN = function(num) {
return(num !== num);
}
}
jQuery不是一个Polyfill。因为它并不是实现一些标准的原生API,而是封装了自己API。一个Polyfill是抹平新老浏览器 标准原生API 之间的差距的一种封装,而不是实现自己的API。
简述一下DOM 和 BOM?
DOM指的是文档对象模型,它指的是把文档当做一个对象来对待,这个对象主要定义了处理网页内容的方法和接口。
BOM指的是浏览器对象模型,它指的是把浏览器当做一个对象来对待,这个对象主要定义了与浏览器进行交互的法和接口。
【扩展】
BOM的核心是window,而window对象具有双重角色,它既是通过js访问浏览器窗口的一个接口,又是一个Global(全局)对象。这意味着在网页中定义的任何对象,变量和函数,都作为全局对象的一个属性或者方法存在。window对象含有location对象、navigator对象、screen对象等子对象,并且DOM的最根本的对象document对象也是BOM的window对象的子对象。
js 模块规范进化史?
js中现在比较成熟的有四种模块加载方案:
第一种是 CommonJS 方案
第二种是 AMD 方案
第三种是 CMD 方案
第四种是 ES6 使用 import 和 export 的形式来导入导出模块方案
【CommonJS 方案】:
它通过 require 来引入模块,通过 module.exports 定义模块的 输出接口。这种模块加载方案是服务器端的解决方案,它是以同步的方式来引入模块的,因为在 服务端文件都存储在本地磁盘,所以读取非常快,所以以同步的方式加载没有问题。但如果是在 浏览器端,由于模块的加载是使用网络请求,因此使用异步加载的方式更加合适。
【AMD 方案】
这种方案采用异步加载的方式来加载模块,模块的加载不影响后面语句的 执行,所有依赖这个模块的语句都定义在一个回调函数里,等到加载完成后再执行回调函数。require.js 实现了 AMD 规范。
【CMD 方案】
这种方案和 AMD 方案都是为了解决异步模块加载的问题,sea.js 实现 了 CMD 规范。它和 require.js 的区别在于模块定义时对依赖的处理不同和对依赖模块的执行 时机的处理不同。
【ES6 import 和 export方案】
ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,旨在成为浏览器和服务器通用的模块解决方案。其模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。
JS模块化 CommonJS,AMD,CMD,ES6的区别?
1、nodeJS里面的模块是基于commonJS规范实现的,原理是文件的读写,导出文件要使用exports、module.exports,引入文件用require。每个文件就是一个模块;每个文件里面的代码会用默认写在一个闭包函数里面。
2、AMD规范则是非同步加载模块,允许指定回调函数,AMD 是 RequireJS 在推广过程中对模块定义的规范化产出。
3、AMD推崇依赖前置, CMD推崇依赖就近。对于依赖的模块AMD是提前执行,CMD是延迟执行。
4、在ES6中,我们可以使用 import 关键字引入模块,通过 exprot 关键字导出模块,但是由于ES6目前无法在浏览器中执行,所以,我们只能通过babel将不被支持的import编译为当前受到广泛支持的 require。
git merge 与 git rebase的区别??
git merge 和 git rebase 都是用于分支合并,关键在 commit 记录的处理上不同。
git merge 会新建一个新的 commit 对象,然后两个分支以前的 commit 记录都指向这个新 commit 记录。这种方法会 保留之前每个分支的 commit 历史。
git rebase 会先找到两个分支的第一个共同的 commit 祖先记录,然后将提取当前分支这之 后的所有 commit 记录,然后 将这个 commit 记录添加到目标分支的最新提交后面。经过这个合并后,两个分支合并后的 commit 记录就变为了线性的记录了。
escape, encodeURI, encodeURIComponent 区别?
escape 和另外两个不属于一类
1、escape是对字符串(string)进行编码,(而另外两种是对URL进行编码)
2、escape可以达到类似URL Encode的效果,但是它对于非ASCII字符使用了一种非标准的的实现,W3C把这个函数废弃了。
// escape 在处理 0xff 之外字符的时候,是直接使用字符的 unicode 在前面加上一个 %u
escape('编'); // "%u7F16"
// encodeURI则是先进行 UTF-8,再在 UTF-8 的每个字节码前加上一个 %
// '编'进行 UTF-8 编码,变成了三个字节:0xe7, 0xbc, 0x96
encodeURI('编'); // "%E7%BC%96"
encodeURIComponent('编'); // "%E7%BC%96"
encodeURI 和 encodeURIComponent 的区别在于需要转义的字符范围不一样
1、encodeURI方法不会对下列字符编码 ASCII字母 数字 ~!@#$&*()=:/,;?+'
2、encodeURIComponent方法不会对下列字符编码 ASCII字母 数字 ~!*()'
encodeURI('https://www.baidu.com/ a b c')
// 不会对 : / 进行编码
// "https://www.baidu.com/%20a%20b%20c"
encodeURIComponent('https://www.baidu.com/ a b c')
// 对 : / 进行编码
// "https%3A%2F%2Fwww.baidu.com%2F%20a%20b%20c"
// escape 会编码成下面这样,不伦不类,所以废弃。
escape('https://www.baidu.com/ a b c')
// "https%3A//www.baidu.com/%20a%20b%20c"
当你需要编码URL中的参数的时候,那么encodeURIComponent是最好方法。
const redirectUrl = 'http://www.a.com/home/users';
// encodeURIComponent(redirectUrl)
// http%3A%2F%2Fwww.a.com%2Fhome%2Fusers
const url = `http://www.a.com?redirectUrl=${encodeURIComponent(redirectUrl)}`;;
// http://www.a.com?redirectUrl=http%3A%2F%2Fwww.a.com%2Fhome%2Fusers
【总结】
如果只是编码字符串,不和URL有半毛钱关系,那么用escape。
如果你需要编码整个URL,然后需要使用这个URL,那么用encodeURI。
当你需要编码URL中的参数的时候,那么encodeURIComponent是最好方法。
谈谈ASCII,Unicode 和 UTF-8
ASCII 码
计算机信息都是一个二进制值。每一个二进制位(bit)有0和1两种状态,因此八个二进制位就可以组合出256种状态,这被称为一个字节(byte)。也就是说,一个字节一共可以用来表示256种不同的状态,每一个状态对应一个符号,就是256个符号,从00000000到11111111。
上个世纪60年代,美国制定了一套字符编码,对英语字符与二进制位之间的关系,做了统一规定。这被称为 ASCII 码,一直沿用至今。
ASCII 码一共规定了128个字符的编码,这128个符号(包括32个不能打印出来的控制符号),只占用了一个字节的后面7位,最前面的一位统一规定为0(大写的字母A是65,二进制01000001)。
Unicode
世界上存在着多种编码方式,同一个二进制数字可以被解释成不同的符号。因此,要想打开一个文本文件,就必须知道它的编码方式,否则用错误的编码方式解读,就会出现乱码。
Unicode将世界上所有的符号都纳入其中。每一个符号都给予一个独一无二的编码,那么乱码问题就会消失。
Unicode 当然是一个很大的集合,现在的规模可以容纳100多万个符号。每个符号的编码都不一样。
Unicode 只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。
UTF-8
UTF-8 是 Unicode 的实现方式之一。UTF-8 就是在互联网上使用最广的一种 Unicode 的实现方式。其他实现方式还包括 UTF-16(字符用两个字节或四个字节表示)和 UTF-32(字符用四个字节表示)。
UTF-8 最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。
手写个call函数
步骤:1、判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可 能出现使用 call 等方式调用的情况。
2、处理传入的参数,截取第一个参数后的所有参数。
3、判断传入上下文对象是否存在,如果不存在,则设置为 window 。
4、将函数作为上下文对象的一个属性。
5、使用上下文对象来调用这个方法,并保存返回结果。
6、删除刚才新增的属性。
7、返回结果。
// call 函数实现
Function.prototype.myCall = function(context) {
// 【1】判断调用对象是否为函数,即使我们是定义在函数的原型上的,
// 但是可能出现使用 call 等方式调用的情况。
if (typeof this !== "function") {
console.error("type error");
}
// 【2】处理传入的参数,截取第一个参数后的所有参数。
let args = [...arguments].slice(1), result = null;
// 【3】判断传入上下文对象是否存在,如果不存在,则设置为 window.
context = context || window;
// 【4】将函数作为上下文对象的一个属性。
context.fn = this;
// 【5】使用上下文对象来调用这个方法,并保存返回结果。
result = context.fn(...args);
// 【6】删除刚才新增的属性。
delete context.fn;
// 【7】返回结果。
return result;
}
手写apply 函数的实现步骤?
步骤:1、判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可 能出现使用 call 等方式调用的情
- 判断传入上下文对象是否存在,如果不存在,则设置为window 。
- 将函数作为上下文对象的一个属性。
- 判断参数值是否传入。使用上下文对象来调用这个方法,并保存返回结果。
5、删除刚才新增的属性。
6、返回结果
// apply 函数实现
Function.prototype.myApply = function(context) {
// 【1】判断调用对象是否为函数
if (typeof this !== "function") {
throw new TypeError("Error");
}
let result = null;
// 【2】判断 context 是否存在,如果不存在,则设置为 window
context = context || window;
// 【3】将函数作为上下文对象的一个属性。
context.fn = this;
// 【4】判断参数值是否传入
// 使用上下文对象来调用这个方法,并保存返回结果
if (arguments[1]) {
result = context.fn(...arguments[1]);
} else {
result = context.fn();
}
//【5】将属性删除
delete context.fn;
//【6】返回结果
return result;
};
手写 bind 函数步骤?
步骤:1、判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可 能出现使用 call 等方式调用的情况。
- 保存当前函数的引用,获取其余传入参数值。
- 创建一个函数返回。
- 函数内部使用 apply 来绑定函数调用,需要判断函数作为构造函数的 情况,这个时候需要传入当前函数的 this 给 apply 调用,其余情况都传 入指定的上下文对象。
// bind 函数实现
Function.prototype.myBind = function(context) {
// 【1】判断调用对象是否为函数
if (typeof this !== "function") {
throw new TypeError("Error");
}
// 【2】获取其余传入参数值, 保存当前函数的引用。
var args = [...arguments].slice(1),
fn = this;
// 【3】创建一个函数返回
return function Fn() {
// 根据调用方式,传入不同绑定值
return fn.apply(
this instanceof Fn ? this : context,
args.concat(...arguments)
);
};
};
toFixed() 四舍五入为什么会有bug?
答案:toFixed四舍五入的规则与数学中的规则不同,接近银行家舍入规则(其实质是一种四舍六入五取偶法)但也不是完全符合,一点规律也没有。
1.005001.toFixed(2); // 结果为 1.01 正确
1.005.toFixed(2) // 结果为 1.00
1.00500.toFixed(2) // 结果为 1.00
1.005.toFixed(2); // 1.01
1.015.toFixed(2); // 1.01
1.025.toFixed(2); // 1.03
1.035.toFixed(2); // 1.04 正确
1.045.toFixed(2); // 1.04
1.055.toFixed(2); // 1.06 正确
1.065.toFixed(2); // 1.07 正确
1.075.toFixed(2); // 1.07
解决方案如下:1、采用Math.round(X * 100) / 100进行处理。
- 重写toFixed方法
offsetWidth ,clientWidth 与 scrollWidth 的区别?
一、clientHeight和clientWidth属性
网页上的每个元素,都有clientHeight和clientWidth属性。这两个属性指元素的内容部分再加上padding的所占据的视觉面积,不包括border和滚动条占用的空间。

【获取浏览器窗口的高和宽:不包括工具栏/滚动条】
const width = document.documentElement.clientWidth;const
height = document.documentElement.clientHeight;
二、scrollHeight和scrollWidth属性
(1)document对象的scrollHeight和scrollWidth属性就是网页的大小,意思就是滚动条滚过的所有长度和宽度。
(2)如果网页内容能够在浏览器窗口中全部显示,不出现滚动条,那么网页的clientWidth和scrollWidth应该相等。
三、offsetTop和offsetLeft属性
每个元素都有offsetTop和offsetLeft属性,表示该元素的左上角与父容器(offsetParent对象)左上角的距离.

四、scrollTop和scrollLeft属性
滚动条滚动的垂直距离,是document对象的scrollTop属性;滚动条滚动的水平距离是document对象的scrollLeft属性。

懒加载和预加载 区别?
懒加载也叫延迟加载,指的是在长网页中延迟加载图片的时机,当用户需要访问时,再去加载, 这样可以提高网站的首屏加载速度,提升用户的体验,并且可以减少服务器的压力。它适用于图 片很多,页面很长的电商网站的场景。懒加载的实现原理是,将页面上的图片的 src 属性设置 为空字符串,将图片的真实路径保存在一个自定义属性中,当页面滚动的时候,进行判断,如果 图片进入页面可视区域内,则从自定义属性中取出真实路径赋值给图片的 src 属性,以此来实 现图片的延迟加载。
预加载指的是将所需的资源提前请求加载到本地,这样后面在需要用到时就直接从缓存取资源。通过预加载能够减少用户的等待时间,提高用户的体验。我了解的预加载的最常用的方式是使用 js 中的 image 对象,通过为 image 对象来设置 scr 属性,来实现图片的预加载。
区别:是一个是提前加载,一个是迟缓甚至不加载。 懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力。
mouseover 和 mouseenter 的区别??
当鼠标移动到元素上时就会触发 mouseenter 事件,类似 mouseover,它们两者之间的差别是 mouseenter 不会冒泡。
由于 mouseenter 不支持事件冒泡,导致在一个元素的子元素上进入或离开的时候会触发其 mouseover 和 mouseout 事件,但是却不会触发 mouseenter 和 mouseleave 事件。
谈谈ES6 引入的操作对象的API:Reflect 与 Proxy
Proxy 可以对目标对象的读取、函数调用等操作进行拦截,然后进行操作处理。它不直接操作对象,而是像代理模式,通过对象的代理对象进行操作,在进行这些操作时,可以添加一些需要的额外操作。
var target = {
name: 'poetries'
};
var logHandler = {
get: function(target, key) {
console.log(`${key} 被读取`);
return target[key];
},
set: function(target, key, value) {
console.log(`${key} 被设置为 ${value}`);
target[key] = value;
}
}
var targetWithLog = new Proxy(target, logHandler);
targetWithLog.name; // 控制台输出:name 被读取
targetWithLog.name = 'others'; // 控制台输出:name 被设置为 others
console.log(target.name); // 控制台输出: others
Reflect 可以用于获取目标对象的行为,它与 Object 类似,但是更易读,为操作对象提供了一种更优雅的方式。
Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。
Reflect不是一个constructor,不能使用new操作符或像一个function一样调用,所有Reflect的属性和方法都是静态的,使用方法和Math对象一样。
代替Object不能完成操作的时候异常捕获
// 代替Object不能完成操作的时候异常捕获
try { Object.defineProperty(obj, name, desc) } catch (e) {}
// 使用Reflect
if (Reflect.defineProperty(obj, name, desc)) {
// success
} else {
// failure
}
在apply函数中的可读性
// ES5
Function.prototype.apply.call(func, obj, arr);
// ES6
Reflect.apply(func, obj, arr);
当使用Proxy去包裹一个对象时,可以自定义其默认方法,但是如果想调用此对象的原生方法时,此时直接通过访问对象来调用是获取不到原生方法的,所以此时需要使用Reflect来解决这个问题。
// 解决Proxy操作后对象中需要访问原生方法的问题
const employee = {
firstName: 'Hal',
lastName: 'Shaw'
};
let logHandler = {
get: function(target, fieldName) {
console.log(target[fieldName]);
return Reflect.get(target, fieldName);
}
};
let func = () => {
let p = new Proxy(employee, logHandler);
p.firstName;
p.lastName;
p.testname;
p.name;
};
func();
观察者模式和发布订阅模式有什么不同?
发布订阅模式其实属于广义上的观察者模式
在观察者模式中,观察者需要直接订阅目标事件。在目标发出内容改变的事件后,直接接收事件 并作出响应。
在发布订阅模式中,发布者和订阅者之间多了一个调度中心。调度中心一方面从发布者接收事 件,另一方面向订阅者发布事件,订阅者需要在调度中心中订阅事件。通过调度中心实现了发布 者和订阅者关系的解耦。使用发布订阅者模式更利于我们代码的可维护性。
如何封装一个 javascript 的类型判断函数??
function getType(value) {
// 判断数据是 null 的情况
if (value === null) {
return value + "";
}
// 判断数据是引用类型的情况
if (typeof value === "object") {
// 调用value toString方法 (如:"[object Array]")
let valueClass = Object.prototype.toString.call(value),
// "Array]" => ["A", "r", "r", "a", "y", "]"]
type = valueClass.split(" ")[1].split("");
// 去掉末尾的 ‘]’
type.pop();
// 重新拼接, 转小写 => array
return type.join("").toLowerCase();
} else {
// 判断数据是基本数据类型的情况和函数的情况
return typeof value;
}
}
jsonp的原理与实现
jsonp是一种跨域通信的手段,原理如下:
1、首先是利用script标签的src属性来实现跨域。
2、通过将前端方法作为参数传递到服务器端,然后由服务器端注入参数之后再返回,实现服务器端向客户端通信。
3、由于使用script标签的src属性,因此只支持get方法
实现流程:
设定一个script标签:<script src="http://jsonp.js?callback=xxx"></script>
callback定义了一个函数名,而远程服务端通过调用指定的函数并传入参数来实现传递参数,将fn(response)传递回客户端
客户端接收到返回的js脚本,开始解析和执行fn(response)
jsonp简单实现
一个简单的jsonp实现,其实就是拼接url,然后将动态添加一个script元素到头部。
function jsonp(req){
var script = document.createElement('script');
var url = req.url + '?callback=' + req.callback.name;
script.src = url;
document.getElementsByTagName('head')[0].appendChild(script);
}
// 前端js示例
function hello(res){
alert('hello ' + res.data);
}
jsonp({ url : '', callback : hello });
js 语句末尾分号是否可以省略?
1、ECMAScript 规范中,语句结尾的分号并不是必需的。
2、一般最好不要省略分号,因为加上分号一方面有利于我们代码的可维护性,另一方面也可以避免我们在对代码进行压缩时出现错误。
简单描述一下 “单例模式” 在 JavaScript 中应用?
单例模式定义理解:
1、保证一个类仅有一个实例,并提供一个访问它的全局访问点。
2、JS 是无类语言,生搬硬套概念没有意义。单例模式的核心是确保只有一个实例,并提供全局访问。
3、全局变量不是单例模式,JS 中经常把全局变量当单例模式来使用,开发中尽量使用命名空间 or 闭包封装私有变量,降低全局变量带来的命名污染。
// 使用命名空间
var namespace1 = {
a: function() {
alert(1);
}
}
// 使用闭包封装私有变量
var user = (function(){
var __name = 'roy';
return function(){
alert(__name);
}
})();
JS中的应用场景举例:JS中单例模式用途非常广泛。试想一下,当我们单击登录按钮时,页面中会出现一个登录浮窗,而且这个登录浮窗是唯一的,无论登录多少次登录按钮,这个浮窗都只会被创建一次,那么这个浮窗就适合用单例模式来创建。
JS 中单例模式的实现:
1、抽离创建单例的逻辑. (fn 参数:创建对象的方法)
2、使用一个变量result来保存fn的计算结果(result变量在闭包中,永远不会被销毁)
var getSingle = function(fn) {
var result;
return function(){
return result || (result = fn.apply(this, arguments));
}
}
var createLoginLayer = function(){
var div = document.createElement('div');
div.innerHTML = '登录窗口';
div.style.display = 'none';
document.body.appendChild('div');
return div;
}
var createSigleLoginLayer = getSigle(createLoginLayer);
document.getElementById('loginBtn').onclick = function(){
var loginLayer = createSigleLoginLayer();
loginLayer.style.display = 'block';
}
JS 设计模式
1、工厂模式
【简单的工厂模】:可以理解为解决多个相似的问题【提示框,只是提示的文字需要修改】
【复杂的工厂模式】:将其成员对象的实列化推迟到子类中,子类可以重写父类接口方法以便创建的时候指定自己的对象类型【各种UI组件,根据你要的类型不同(比如:按钮,提示框,表格等)】
2、 单例模式
两个特点:一个类只有一个实例,并且提供可全局访问点 全局对象是最简单的单例模式:window
demo:登录弹出框只需要实例化一次,就可以反复用了
// 实现单例模式弹窗
var createWindow = (function(){
var div;
return function(){
if(!div) {
div = document.createElement("div");
div.innerHTML = "我是弹窗内容";
div.style.display = 'none';
document.body.appendChild(div);
}
return div;
}
})();
document.getElementById("Id").onclick = function(){
// 点击后先创建一个div元素
var win = createWindow();
win.style.display = "block";
}
3、模块模式
模块模式的思路是为单体模式添加私有变量和私有方法能够减少全局变量的使用
demo:返回对象的匿名函数。在这个匿名函数内部,先定义了私有变量和函数
var singleMode = (function(){
// 创建私有变量
var privateNum = 112;
// 创建私有函数
function privateFunc(){
// 实现自己的业务逻辑代码
}
// 返回一个对象包含公有方法和属性
return {
publicMethod1: publicMethod1,
publicMethod2: publicMethod1
};
})();
4、代理模式
代理对象可以代替本体被实例化,并使其可以被远程访问
demo: 虚拟代理实现图片的预加载
class MyImage {
constructor() {
this.img = new Image()
document.body.appendChild(this.img)
}
setSrc(src) {
this.img.src = src
}
}
class ProxyImage {
constructor() {
this.proxyImage = new Image()
}
setSrc(src) {
let myImageObj = new MyImage()
myImageObj.img.src = 'file://xxx.png' //为本地图片url
this.proxyImage.src = src
this.proxyImage.onload = function() {
myImageObj.img.src = src
}
}
}
var proxyImage = new ProxyImage()
proxyImage.setSrc('http://xxx.png') //服务器资源url
5、缓存代理
缓存代理的含义就是对第一次运行时候进行缓存,当再一次运行相同的时候,直接从缓存里面取,这样做的好处是避免重复一次运算功能,如果运算非常复杂的话,对性能很耗费,那么使用缓存对象可以提高性能;
demo:计算值的加法,如果之前已经算过,取缓存,如果没有算过重新计算。
6、命令模式
有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道请求的操作是什么,此时希望用一种松耦合的方式来设计程序代码;使得请求发送者和请求接受者消除彼此代码中的耦合关系。
demo:几个按钮绑定不同的事件,然后bindEvent(el, event);
7、模板方法模式
一、模板方法模式:一种只需使用继承就可以实现的非常简单的模式。
二、模板方法模式由两部分组成,第一部分是抽象父类,第二部分是具体的实现子类。
demo: 比如泡茶、冲咖啡的步骤都是一样的,抽出父类,Child.prototype = new Parent();然后重写里面的步骤(方法)
8、策略模式
定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换
demo:年终奖的薪水的几倍,是按照一个个等级来划分的,A级别是3倍,B级别是2倍,C级别是1倍,那么就可以写三个等级方法,然后封装在一个方法里,传入薪水和等级就ok了
9、发布订阅模式介绍
发布—订阅模式又叫观察者模式,它定义了对象间的一种一对多的关系,让多个观察者对象同时监听某一个主题对象,当一个对象发生改变时,所有依赖于它的对象都将得到通知。
demo: 比如你向买房,只要把手机给房产中介,房产中介一有消息就发布消息。
var list = {
arr: [],
subscribe: function(fn) {
this.arr.push(fn);
},
notify: function() {
this.arr.forEach(fn => fn());
}
};
var fn1 = function() {
console.log(1)
}
var fn2 = function() {
console.log(2)
}
list.subscribe(fn1);
list.subscribe(fn2);
list.notify();
10、中介者模式
中介者模式的作用是解除对象与对象之间的耦合关系,增加一个中介对象后,所有的相关对象都通过中介者对象来通信,而不是相互引用,所以当一个对象发送改变时,只需要通知中介者对象即可。中介者使各个对象之间耦合松散,而且可以独立地改变它们之间的交互。
demo:卖手机,颜色和数量判断加入购物车按钮是否可用
11、装饰者模式
动态的给类或对象增加职责的设计模式。
装饰器模式并不去深入依赖于对象是如何创建的,而是专注于扩展它们的功能这一问题上。装饰器模式相比生成子类更为灵活。
var Car = function() {}
Car.prototype.drive = function() {
console.log('乞丐版');
}
var AutopilotDecorator = function(car) {
this.car = car;
}
AutopilotDecorator.prototype.drive = function() {
this.car.drive();
console.log('启动自动驾驶模式');
}
var car = new Car();
car = new AutopilotDecorator(car);
car.drive(); //乞丐版;启动自动驾驶模式;
12、适配器模式
适配器模式主要解决两个接口之间不匹配的问题,不会改变原有的接口,而是由一个对象对另一个对象的包装。
demo:两个地图(2个类),他们有一个共同方法但是名字不同,这时候需要定义适配器类, 对其中的一个类进行封装。
class GooleMap {
show() {
console.log('渲染谷歌地图')
}
}
class BaiduMap {
display() {
console.log('渲染百度地图')
}
}
// 定义适配器类, 对BaiduMap类进行封装
class BaiduMapAdapter {
show() {
var baiduMap = new BaiduMap()
return baiduMap.display()
}
}
function render(map) {
if (map.show instanceof Function) {
map.show()
}
}
render(new GooleMap()) // 渲染谷歌地图
render(new BaiduMapAdapter()) // 渲染百度地图
一个列表,假设有 100000 个数据,这个该怎么办?
我们需要思考的问题:该处理是否必须同步完成?数据是否必须按顺序完成?
解决办法:
1、将数据分页,利用分页的原理,每次服务器端只返回一定数目的数据,浏览器每次只加载一部分。
2、使用懒加载的方法,每次加载一部分数据,其余数据当需要使用时再去加载。
3、使用数组分块技术,基本思路是为要处理的项目创建一个队列,然后设置定时器每过一段 时间取出一部分数据,然后再使用定时器取出下一个要处理的项目进行处理,接着再设置另一个 定时器。 补充:数组分块技术的重要性在于它可以将多个项目的处理在执行队列上分开,在每个项目处理之后,给予其他的浏览器处理机会运行,这样就可能避免长时间运行脚本的错误。
/*
* 参数array:要处理的项目的数组,
* 参数process:用于处理项目的函数,
* 参数contextarray:以及可选的运行该函数的环境;
* /
function chunk(array,process,context){
setTimeout(function(){
var item=array.shift();
process.call(context.item);
if(array.length>0){
setTimeout(arguments.callee,100);
}
},100);
获取可视区域的高度: innerHeight 和 clientHeight 区别?
这两个都是获取可视区域的高度,那他们有什么区别呢?

如何查找一篇英文文章中出现频率最高的单词?
function findMostWord(article) {
// 合法性判断
if (!article) return;
// 去除两端空格、转小写
article = article.trim().toLowerCase();
// 正则全局过滤英文词语
let wordList = article.match(/[a-z]+/g),
visited = [],
maxNum = 0,
maxWord = "";
// article中去了除非英文关键字
article = " " + wordList.join(" ") + " ";
// 遍历判断单词出现次数
wordList.forEach(
function(item) {
if (visited.indexOf(item) < 0) {
// 加入 visited
visited.push(item);
let word = new RegExp(" " + item + " ", "g"),
// 统计当前 关键词 出现频率
num = article.match(word).length;
// 判断并更新出现频率最高的 关键字&出现次数
if (num > maxNum) {
maxNum = num;
maxWord = item;
}
}
}
);
return maxWord + " " + maxNum;
}
测试代码:
const aaa = ` aaa eee rrrr s s s dddd ttt ff ttf
ff tt ff ff ff 888 9999 十九点, 订单, dddd, ee, 99
.jkdjd `;
console.log('findMostWord:' + findMostWord(aaa));
// findMostWord:ff 4
如何优化冒泡排序?
function bubbleSort(arr) {
// 判断数组元素大于1
if (Array.isArray(arr) || arr.length <= 1) return;
// lastIndex: 记录最后一次内循环(for循环)元素交换的位置;
// 每次循环到lastIndex, 优化性能;
// 因为lastIndex后的序列都是已排好;
let lastIndex = arr.length - 1;
while (lastIndex > 0) {
// flag:记录内循环(for循环)中是否发生了交换;
// 如果没有发生交换(即flag=true),则说明该序列已经为有序序列;
// 不需要再次执行 while 循环,直接结束
let flag = true,
// 每次循环到lastIndex;
k = lastIndex;
for (let j = 0; i < k; j++) {
if (arr[j] > arr[j + 1]) {
// while 循环结束标记为false
flag = false;
// 记录最后一次内循环(for循环)元素交换的位置
lastIndex = j;
// 交换数组中相邻元素位置
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
}
}
// 内循环结束后,跳出while循环
if (flag) break;
}
}
优化方案:
1、lastIndex:记录最后一次内循环(for循环)元素交换的位置,每次循环到lastIndex 即可,因为lastIndex后的序列都是已排好,无需再次循环;
2、flag:记录内循环(for循环)中是否发生了交换,如果没有发生交换(即flag=true),则说明该序列已经为有序序列,不需要再次执行 while 循环,直接结束
如何优化简单的选择排序算法?
选择排序的基本思想:每次循环取出最小(最大)元素作为首元素;
算法实现时,每次确定最小元素时都会通过不断比较并且交换,来保证首位置最小;
不难发现,在确定最小之前的交换都是没有意义的。我们可以设置一个变量 min 来记录最小值的下标,最后在执行交换操作即可。
function selectSort(array) {
let length = array.length;
// 如果不是数组或者数组长度小于等于 1,直接返回,不需要排序
if (!Array.isArray(array) || length <= 1) return;
for (let i = 0; i < length - 1; i++) {
let minIndex = i;
// 设置当前循环最小元素索引
for (let j = i + 1; j < length; j++) {
// 如果当前元素比最小元素索引,则更新最小元素索引
if (array[minIndex] > array[j]) {
minIndex = j;
}
}
// 交换最小元素到当前位置
[array[i], array[minIndex]] = [array[minIndex], array[i]];
}
return array;
}
描述一下 “插入排序” 算法
插入排序:基本思想是每一步将一个待排序的元素插入到前面已经排好序列中,直到插完所有元素为止。
插入排序核心--扑克牌思想:就想着自己在打扑克牌,接起来第一张,放哪里无所谓,再接起来第二张,比第一张小,放左边, 继续接,可能是中间数,就插在中间....依次。
function insertSort(array) {
// 如果不是数组或者数组长度小于等于 1,直接返回,不需要排序
if (!Array.isArray(array) || array.length <= 1) return;
let len = array.length;
for(let i=1; i<len; i++) {
// 从已排序区域[0,i)中选择目标位置
for(let j=0; j<i; j++) {
if(array[i]<array[j]) {
[array[j], array[i]] = [array[i], array[j]];
}
}
}
return array;
}
简单描述一下 “希尔排序” ?
希尔排序的基本思想是把数组按下标的一定增量分组,对每组使用直接插入排序 算法排序;随着增量逐渐减少,每组包含的元素越来越多,当增量减至 1 时, 整个数组恰被分成一组,算法便终止。

在上面这幅图中:
初始时,有一个大小为 10 的无序序列。
在第一趟排序中,我们不妨设 gap1 = N / 2 = 5,即相隔距离为 5 的元素组成一组,可以分为 5 组。
接下来,按照直接插入排序的方法对每个组进行排序。
在第二趟排序中,我们把上次的 gap 缩小一半,即 gap2 = gap1 / 2 = 2 (取整数)。这样每相隔距离为 2 的元素组成一组,可以分为 2 组。
按照直接插入排序的方法对每个组进行排序。
在第三趟排序中,再次把 gap 缩小一半,即gap3 = gap2 / 2 = 1。 这样相隔距离为 1 的元素组成一组,即只有一组。
按照直接插入排序的方法对每个组进行排序。此时,排序已经结束。
需要注意一下的是,图中有两个相等数值的元素 5 和 5 。我们可以清楚的看到,在排序过程中,两个元素位置交换了。
所以,希尔排序是不稳定的算法。
代码实现:
function hillSort(data) {
let gap = Math.floor(data.length / 2);
// 用于存储需要插入的数据
let temp;
// 注意i从gap开始,因为以data[0]为基准数,j=i-1
while (gap >= 1) {
for (let i = gap; i < data.length; i++) {
// 将第i个数保存,以供之后插入合适位置使用
temp = data[i];
// 因为前i-1个数都是从小到大的有序序列,只要当前比较的数(data[j-1])比temp大,就把这个数后移一位
// 这块j得用var声明,因为在for循环之外的作用域还要用j
for (var j = i - gap; j >= 0 && data[j] > temp; j = j - gap) {
data[j + gap] = data[j];
}
// 将temp插入合适的位置
data[j + gap] = temp;
}
gap = Math.floor(gap / 2);
}
return data;
}
简单描述一下 “归并排序” 算法?
基本思想与过程:先递归的分解数列,再合并数列(分治思想的典型应用)
1、将一个数组拆成A、B两个小组,两个小组继续拆,直到每个小组只有一个元素为止。
2、按照拆分过程逐步合并小组,由于各小组初始只有一个元素,可以看做小组内部是有序的,合并小组可以被看做是合并两个有序数组的过程。
3、对左右两个小数列重复第二步,直至各区间只有1个数。
分治(divide-and-conquer)策略:分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之。

可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。分阶段可以理解为就是递归拆分子序列的过程,递归深度为log2n。
function mergeSort(arr) {
if (arr.length == 1) return arr;
// 计算中间分界点, 将数组一分为二
var mid = Math.floor(arr.length / 2);
var left = arr.slice(0, mid);
var right = arr.slice(mid);
// 合并左右部分
return Merger(mergeSort(left), mergeSort(right));
}
function Merger(leftArr, rightArr) {
var lenL = leftArr && leftArr.length;
var lenR = rightArr && rightArr.length;
var temp = [];
var i = 0, j = 0;
while (i < lenL && j < lenR) {
if (leftArr[i] < rightArr[j])
temp.push(leftArr[i++]);
else
temp.push(rightArr[j++]);
}
while (i < lenL)
temp.push(leftArr[i++]);
while (j < lenR)
temp.push(rightArr[j++]);
console.log("将数组", JSON.stringify(leftArr), '和', JSON.stringify(rightArr), '合并为', JSON.stringify(temp));
return temp;
}
mergeSort([8,4,5,7,1,3,6,2]);

将数组 [8] 和 [4] 合并为 [4,8]
将数组 [5] 和 [7] 合并为 [5,7]
将数组 [4,8] 和 [5,7] 合并为 [4,5,7,8]
将数组 [1] 和 [3] 合并为 [1,3]
将数组 [6] 和 [2] 合并为 [2,6]
将数组 [1,3] 和 [2,6] 合并为 [1,2,3,6]
将数组 [4,5,7,8] 和 [1,2,3,6] 合并为 [1,2,3,4,5,6,7,8]

简单描述一下反向代理解决跨域问题原理?
跨域:跨域是浏览器行为,不是服务器行为。
实际上,请求已经到达服务器了,只不过在回来的时候被浏览器限制了。就像Python他可以进行抓取数据一样,不经过浏览器而发起请求是可以得到数据。
代理:所谓代理就是在我们和真实的服务器之间有一台代理服务器,我们所有的请求都是通过它来进行转接的。
正向代理:我们访问不了Google,但是我在国外有一台vps,它可以访问Google,我访问它,叫它访问Google后,把数据传给我。

反向代理:反向代理隐藏了真实的服务端,当我们请求 www.baidu.com 的时候,就像拨打10086一样,背后可能有成千上万台服务器为我们服务,但具体是哪一台,你不知道,也不需要知道,你只需要知道反向代理服务器是谁就好了,www.baidu.com 就是我们的反向代理服务器,反向代理服务器会帮我们把请求转发到真实的服务器那里去。

总结:正向代理隐藏了真实的客户端。
反向代理隐藏了真实的服务器。
通俗讲解 "快速排序" 算法?
"快速排序"的思想很简单,整个排序过程只需要三步:
(1)在数据集之中,选择一个元素作为"基准"(pivot)。
(2)所有小于"基准"的元素,都移到"基准"的左边;所有大于"基准"的元素,都移到"基准"的右边。
(3)对"基准"左边和右边的两个子集,不断重复第一步和第二步,直到所有子集只剩下一个元素为止。
举例来说,现在有一个数据集{85, 24, 63, 45, 17, 31, 96, 50},怎么对其排序呢?
第一步,选择中间的元素45作为"基准"。(基准值可以任意选择,但是选择中间的值比较容易理解。)

第二步,按照顺序,将每个元素与"基准"进行比较,形成两个子集,一个"小于45",另一个"大于等于45"。

第三步,对两个子集不断重复第一步和第二步,直到所有子集只剩下一个元素为止。




代码实现:
var quickSort = function(arr) {
// 检查数组的元素个数,如果小于等于1,就返回
if (arr.length <= 1) { return arr; }
// 选择"基准"(pivot),并将其与原数组分离
var pivotIndex = Math.floor(arr.length / 2);
var pivot = arr.splice(pivotIndex, 1)[0];
// 再定义两个空数组,用来存放一左一右的两个子集
var left = [];
var right = [];
for (var i = 0; i < arr.length; i++){
if (arr[i] < pivot) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
return quickSort(left).concat([pivot], quickSort(right));
};
如何理解排序算法的稳定性?
稳定性的意思就是对于相同值来说,相对顺序不能改变。
通俗的讲有两个相同的 数 A 和 B,在排序之前 A 在 B 的前面, 而经过排序之后,B 跑到了 A 的 前面,对于这种情况的发生,我们管他叫做排序的不稳定性。
稳定性有什么意义?
个人理解对于前端来说,比如我们熟知框架中的虚拟 DOM 的比较,我们对一个<ul>列表进行渲染,当数据改变后需要比较变化时,不稳定排序或操作将会使本身不需要变化的东西变化,导致重新渲染,带来性能的损耗。
js 实现一个函数,完成超过范围的两个大整数相加功能?
主要思路是通过将数字转换为字符串,然后每个字符串在按位相加。
function add(a,b){
// 保存最终结果
var res='';
// 保存【本次循环】两位相加的结果 和 【上一次循环】进位值
var c=0;
// 字符串转数组b
a = a.split('');
b = b.split('');
while (a.length || b.length || c){
// ~~ 用来把String类型 转为 Number类型
// 把两位相加的结果 和 进位值相加
c += ~~a.pop() + ~~b.pop();
// 取余,把余数拼接到最终结果中
res = c % 10 + res;
// 保存进位,true 或者 false.
// 隐式类型转换的时候,true会转为1,false会转为0
c = c>9; // 将本次进位值,用于和下次计算结果相加
}
return res;
}
计算:
add('11111111111111137','22222222222222288')
// 33333333333333425
js 如何实现数组扁平化?
ES6 增加了扩展运算符,用于取出参数对象的所有可遍历属性,拷贝到当前对象之中:
var arr = [1, [2, [3, 4]]];
console.log([].concat(...arr));
// [1, 2, [3, 4]]
上面的方法只可以扁平一层,但是顺着这个方法一直思考,我们可以写出这样的方法:
var arr = [1, [2, [3, 4]]];
function flatten(arr) {
while (arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr);
}
return arr;
}
console.log(flatten(arr));
// [1, 2, 3, 4]
ES6 如何优雅实现数组去重?
随着 ES6 的到来,去重的方法又有了进展,比如我们可以使用 Set 和 Map 数据结构,以 Set 为例,ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。
var array = [1, 2, 1, 1, '1'];
function unique(array) {
return Array.from(new Set(array));
}
console.log(unique(array)); // [1, 2, "1"]
甚至可以再简化下:
function unique(array) {
return [...new Set(array)];
}
还可以再简化下:
var unique = (a) => [...new Set(a)]
此外,如果用 Map 的话:
function unique (arr) {
const seen = new Map()
return arr.filter((a) => !seen.has(a) && seen.set(a, 1))
}
JS 使用两种方法计算两个整数的 最大公约数
最大公约数:指两个或多个整数共有约数中最大的一个
1、小学时候一般采用质因数分解法,一般使用短除得到结果,下面用一种最初级的方法求最大公约数
function gcd(a,b) {
var result = 1;
for(var i = 1; i <= a && i <= b; i++ ){
if(a%i == 0 && b%i == 0 ){
result = i;
}
}
return result;
}
2、使用欧里几德算法,辗转相除法
计算原理依赖于下面的定理:
定理:两个整数的最大公约数等于其中较小的那个数和两数相除余数的最大公约数。
function gcd(a,b){
if(b == 0){
return a;
}
var r = a % b;
return gcd(b,r);
}
【扩展】求最小公倍数:
最大公约数(gcd)和最小公倍数(lcm)的关系:
gcd(a, b) * lcm(a, b) = ab
function lcm(a, b){
return a * b / gcd(a, b);
}
判断一个字符串是否为回文字符串?
回文:是指正读反读都能读通的句子,它是古今中外都有的一种修辞方式和文字游戏,如 “我为人人,人人为我” 等
function isPalindrome(str) {
// []是定义匹配的字符范围。
// \w 匹配字母或数字或下划线或汉字 等价于 '[^A-Za-z0-9_]'
let reg = /[\w]/g,
// 匹配所有非单词的字符以及下划线
newStr = str.replace(reg, "").toLowerCase(),
// 替换为空字符并将大写字母转 换为小写
reverseStr = newStr.split("").reverse().join("");
// 将字符串反转
return reverseStr === newStr;
}
console.log(isPalindrome('我为人人,人人为我'));
// true
实现一个累加函数的功能比如 sum(1,2,3)(2).valueOf()
function sum(...args) {
let result = 0;
// 计算sum(args)
result = args.reduce(function (pre, item) {
return pre + item;
}, 0);
let add = function (...nextArgs) {
// 计算sum(args)(nextArgs)
result = nextArgs.reduce(function (pre, item) {
return pre + item;
}, result);
return add;
};
add.valueOf = function () {
return result
};
return add;
}
console.log(sum(1,2,3)(2).valueOf())
// 8
forEach, map, filter, some, every方法返回值有什么不同?
forEach循环,循环数组中每一个元素并采取操作, 没有返回值

map函数,遍历数组每个元素,并回调操作,需要返回值,返回值组成新的数组,原数组不变

filter函数, 过滤通过条件的元素组成一个新数组, 原数组不变

some函数,遍历数组中是否有符合条件的元素,返回Boolean

every函数, 遍历数组中是否每个元素都符合条件, 返回Boolean值

设计一个简单的任务队列,要求分别在 1,3,4 秒后打印执行
class Queue {
constructor() {
this.queue = [];
this.time = 0;
}
addTask(task, t) {
this.time += t;
this.queue.push([task, this.time]);
return this;
}
start() {
this.queue.forEach(item => {
console.time(item[0]);
setTimeout(() => {
console.timeEnd(item[0]);
}, item[1]);
})
}
}
const queue = new Queue;
queue.addTask('1秒后打印', 1000);
queue.addTask('3秒后打印', 3000);
queue.addTask('4秒后打印', 4000);
queue.start();
// 1秒后打印: 1009.35791015625ms
// 3秒后打印: 4013.808837890625ms
// 4秒后打印: 8016.027099609375ms
addEventListener('click') 和 onClick 区别?
onclick绑定方式
优点:简洁、处理事件的this关键字指向当前元素
缺点:不能对事件捕获或事件冒泡进行控制,只能使用事件冒泡,无法切换成事件捕获
addEventListener绑定方式
优点:1、可以对事件捕获或事件冒泡进行控制。addEventListener最后一个参数设置为false(默认值,表示事件冒泡)或者true(表示事件捕获)来切换 ;
2、事件处理 this与onclick一样
3、你可以为某个元素绑定多个事件而不会覆盖之前绑定的处理程序 (按照顺序执行)
缺点:IE8以下不支持
验证事件覆盖行为
<input type="button" id="iS_addEventListener" value="addEventListener">
<input type="button" id="iS_onclick" value="onclick">
<script type="text/javascript">
(function(){
document.getElementById("iS_addEventListener").addEventListener("click",function(){
alert("我是addEventListener1");
},false);
document.getElementById("iS_addEventListener").addEventListener("click",function(){
alert("我是addEventListener2");
},false);
//onclick是重新赋值,变量提升
document.getElementById("iS_onclick").onclick = function() {
alert("我是onclick1");
}
document.getElementById("iS_onclick").onclick = function() {
alert("我是onclick2");
}
})();
</script>
验证结果:
onclick只出现一次alert:我是onclick2【很正常第一次click事件会被第二次所覆盖】
addEventListener却可以先后运行,不会被覆盖【正如:它允许给一个事件注册多个监听器】
ES6 Map 实现JS对象深克隆?
递归遍历对象,解决循环引用问题
解决循环引用问题,我们需要一个存储容器存放当前对象和拷贝对象的对应关系(适合用key-value的数据结构进行存储,也就是map),当进行拷贝当前对象的时候,我们先查找存储容器是否已经拷贝过当前对象,如果已经拷贝过,那么直接把返回,没有的话则是继续拷贝。
function deepClone(target) {
const map = new Map()
function clone (target) {
if (isObject(target)) {
let cloneTarget = isArray(target) ? [] : {};
if (map.get(target)) {
return map.get(target)
}
map.set(target,cloneTarget)
for (const key in target) {
cloneTarget[key] = clone(target[key]);
}
return cloneTarget;
} else {
return target;
}
}
return clone(target)
};
扩展:
Map 对象:Map 对象保存键值对。任何值(对象或者原始值) 都可以作为一个键或一个值。
Maps 和 Objects 的区别
一个 Object 的键只能是字符串或者 Symbols,但一个 Map 的键可以是任意值。
Map 中的键值是有序的(FIFO 原则),而添加到对象中的键则不是。
Map 的键值对个数可以从 size 属性获取,而 Object 的键值对个数只能手动计算。
Object 都有自己的原型,原型链上的键名有可能和你自己在对象上的设置的键名产生冲突。
哪些情况不应该使用箭头函数?
1. 对象属性方法不要使用箭头函数
this.animal = "dog";
let obj = {
animal: "cat",
log() {
console.log(this.animal);
}
};
obj.log(); // cat
使用箭头函数后:
this.animal = "dog";
let obj = {
animal: "cat",
log: ()=> {
console.log(this.animal);
}
};
obj.log(); // dog
ecma262规范中明确规定,箭头函数根本没有自身的this绑定;也就是说箭头函数本身没法修改this,所以对this访问永远是它继承外部上下的this
2. 原型方法(prototype method)的定义不要使用箭头函数
function Cat(name) {
this.name = name;
}
Cat.prototype.sayCatName = () => {
console.log(this === window); // => true
return this.name;
};
const cat = new Cat('Mew');
cat.sayCatName(); // => undefined
3. 使用 arguments 时
箭头函数执行前绑定this的时候,传入的thisArgument会被直接忽略
function test() {
return (...args) => {
console.log(...arguments);
// 打印[6, 6, 6],箭头函数没有arguments需要从外部函数获取
console.log(...args);
// 打印[8, 8, 8, 8]
};
}
test([6, 6, 6])([8, 8, 8, 8]);
4. 定义事件回调时不要使用箭头函数
const button = document.getElementById('myButton');
button.addEventListener('click', () => {
// this === window
console.log(this === window); // => true
this.innerHTML = 'Clicked button';
});
普通函数:
const button = document.getElementById('myButton');
button.addEventListener('click', function() {
// this === button
console.log(this === button); // => true
this.innerHTML = 'Clicked button';
});
简述 JavaScript 编译原理
尽管通常将 JavaScript 归类为“动态”或“解释执行”语言,但事实上它是一门编译语言。但与传统的编译语言不同,它不是提前编译的,编译结果也不能在分布式系统中进行移植。尽管如此,JavaScript 引擎进行编译的步骤和传统的编译语言非常相似,在某些环节可能 比预想的要复杂。
在传统编译语言的流程中,程序中的一段源代码在执行之前会经历三个步骤,统称为“编 译”:
1、分词/词法分析(Tokenizing/Lexing)
这个过程会将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代 码块被称为词法单元(token)。例如,考虑程序 var a = 2;。这段程序通常会被分解成 为下面这些词法单元:var、a、=、2 、;。空格是否会被当作词法单元,取决于空格在 这门语言中是否具有意义。
分词(tokenizing)和词法分析(Lexing)之间的区别是非常微妙、晦涩的, 主要差异在于词法单元的识别是通过有状态还是无状态的方式进行的。简 单来说,如果词法单元生成器在判断 a 是一个独立的词法单元还是其他词法 单元的一部分时,调用的是有状态的解析规则,那么这个过程就被称为词法 分析。
2、解析/语法分析(Parsing)
这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法 结构的树。这个树被称为“抽象语法树”(Abstract Syntax Tree,AST)。var a = 2; 的抽象语法树中可能会有一个叫作 VariableDeclaration 的顶级节点,接下 来是一个叫作 Identifier(它的值是 a)的子节点,以及一个叫作 AssignmentExpression 的子节点。AssignmentExpression 节点有一个叫作 NumericLiteral(它的值是 2)的子 节点。
3、代码生成
将 AST 转换为可执行代码的过程称被称为代码生成。这个过程与语言、目标平台等息 息相关。抛开具体细节,简单来说就是有某种方法可以将 var a = 2; 的 AST 转化为一组机器指 令,用来创建一个叫作 a 的变量(包括分配内存等),并将一个值储存在 a 中。
比起那些编译过程只有三个步骤的语言的编译器,JavaScript 引擎要复杂得多。
例如,在 语法分析和代码生成阶段有特定的步骤来对运行性能进行优化,包括对冗余元素进行优化 等。
JavaScript 引擎不会有大量的(像其他语言编译器那么多的)时间用来进行优化,因 为与其他语言不同,JavaScript 的编译过程不是发生在构建之前的。对于 JavaScript 来说,大部分情况下编译发生在代码执行前的几微秒(甚至更短!)的时 间内。JavaScript 引擎用尽了各种办法(比如 JIT,可以延 迟编译甚至实施重编译)来保证性能最佳。
简单地说,任何 JavaScript 代码片段在执行前都要进行编译(通常就在执行前)。因此, JavaScript 编译器首先会对 var a = 2; 这段程序进行编译,然后做好执行它的准备,并且 通常马上就会执行它
JS 变量提升原理?
先看个例子:
a = 2;
var a;
console.log( a ); // 2
很多开发者会认为是 undefined,因为 var a 声明在 a = 2 之后,他们自然而然地认为变量 被重新赋值了,因此会被赋予默认值 undefined。但是,真正的输出结果是 2。
再看一个例子:
console.log( a ); // undefined
var a = 2;
你可能会认为这个代码片 段也会有同样的行为而输出 2。还有人可能会认为,由于变量 a 在使用前没有先进行声明, 因此会抛出 ReferenceError 异常。
输出来的会是 undefined。
原理:
引擎会在解释 JavaScript 代码执行之前首先对其进行编译。编译阶段中的一部分工作就是找到所有的 声明,并用合适的作用域将它们关联起来。这个机制,也正是词法作用域的核心内容。
因此,正确的思考思路是,包括变量和函数在内的所有声明都会在任何代码被执行前首先 被处理。
当你看到 var a = 2; 时,可能会认为这是一个声明。但 JavaScript 实际上会将其看成两个 声明:var a; 和 a = 2;。第一个定义声明是在编译阶段进行的。第二个赋值声明会被留在 原地等待执行阶段。
我们的第一个代码片段会以如下形式进行处理:
var a;
a = 2;
console.log( a );
其中第一部分是编译,而第二部分是执行。
类似地,我们的第二个代码片段实际是按照以下流程处理的:
var a;
console.log( a );
a = 2;
因此,打个比方,这个过程就好像变量和函数声明从它们在代码中出现的位置被“移动” 到了最上面。这个过程就叫作提升。
扩展:
1、函数声明会被提升,但是函数表达式却不会被提升。
foo();
function foo() {
console.log( a ); // undefined
var a = 2;
}
// 执行流程如下:
function foo() {
var a;
console.log( a ); // undefined
a = 2;
}
foo();
// 函数表达式执行流程如下:
foo(); // 不是 ReferenceError, 而是 TypeError!
var foo = function bar() {
// ...
};
这段程序中的变量标识符 foo() 被提升并分配给所在作用域(在这里是全局作用域),因此 foo() 不会导致 ReferenceError。但是 foo 此时并没有赋值(如果它是一个函数声明而不 是函数表达式,那么就会赋值)。foo() 由于对 undefined 值进行函数调用而导致非法操作, 因此抛出 TypeError 异常。
2、函数声明和变量声明都会被提升,但函数会首先被提升,然后才是变量。
foo(); // 1
var foo;
function foo() {
console.log( 1 );
}
foo = function() {
console.log( 2 );
};
会输出 1 而不是 2 !这个代码片段会被引擎理解为如下形式:
function foo() {
console.log( 1 );
}
foo(); // 1
foo = function() {
console.log( 2 );
};
注意,var foo 尽管出现在 function foo()... 的声明之前,但它是重复的声明(因此被忽 略了),因为函数声明会被提升到普通变量之前。
尽管重复的 var 声明会被忽略掉,但出现在后面的函数声明还是可以覆盖前面的。
foo(); // 3
function foo() {
console.log( 1 );
}
var foo = function() {
console.log( 2 );
};
function foo() {
console.log( 3 );
}


2816

被折叠的 条评论
为什么被折叠?



