IIFE
IIFE(立即调用函数表达式)是一个再定义就会立即执行的JavaScript函数
1 | (function (){})() |
这是一个被称为自执行匿名函数的设计模式,主要包含两部分。第一部分是包围再圆括号运算符()里的一个匿名函数,这个匿名函数拥有独立的词法作用域,这不仅避免了外界访问此IIFE中的变量,而且又不会污染全局作用域
在JavaScript里,圆括号不能包含声明。因为这点,当圆括号为了包裹函数碰上了function
关键字,它便知道将它作为一个函数表达式而不是函数声明,
- 当圆括号出现在匿名函数的尾部想要调用函数时,它会默认将函数当成函数声明
- 当圆括号包裹函数时,它会默认将函数作为表达式去解析,而不是函数声明
匿名函数的其他使用方法,可替换为其他的一元运算符
再function
前面加上运算符时,就变成了一个函数表达式,而函数表达式后面又添加了以个()就变成了一个立即执行的函数
1 | +function(){ |
注意:在使用匿名函数时,前面或者后面的代码一定要加;
不然会报错
1 |
|
闭包
答案:
- 是一个能够读取其他函数内部变量的函数,实质上是变量的解析过程(由内而外)
- 可以用来封装私有变量,实现模块化
- 可以保存变量到内存中(内存泄漏)
例子:
假设出现了一个案例:
每点击按钮实现数字加1,这个很简单学过一点JS的人都会
1 | let sum = 0; |
但是这样又污染了全局作用域,多了个全局变量,这时候我们可以使用闭包
1 | function add(){ |
私有方法
1 | var makeCounter = function() { |
经典面试题
1 | <ul> |
这是因为li节点的onclick事件属于异步的,在click被触发的时候,for循环以迅雷不及掩耳盗铃的速度就执行完毕了,此时变量i的值已经是4了
因此在li的click事件函数顺着作用域链从内向外开始找i的时候,发现i的值已经全是4了
解决方法就需要通过闭包,把每次循环的i值都存下来。然后当click事件继续顺着作用域链查找的时候,会先找到被存下来的i,这样每一个li点击都可以找到对应的i值了
1 | <script> |
值类型和应用类型
值类型 | String | Number | Boolean | Undefined | Null |
---|---|---|---|---|---|
引用类型 | Array | Object | Function |
区别
- 存储位置不一样
- 值类型的变量会保存再栈内存中,如果再一个函数中声明一个值类型的变量,那么这个变量当函数执行结束之后就会自动销毁
- 引用类型的变量名会保存再栈内存中,但是变量值会存储再堆内存中,引用类型的不变量不会自动销毁,当没有引用变量应用时,系统的垃圾回收机制会回收
栈内存 | 堆内存 |
---|---|
a = 10,b = 20 | |
arr | [10,20,30,40] |
- 赋值类型不一样
- 值类型的变量直接赋值就是深赋值,如
var a = 10;var b = a
那么a的值就复制给b了,b修改值不会影响a - 引用类型的变量直接赋值实际上是传递引用,只是浅复制
- 值类型的变量直接赋值就是深赋值,如
1 | var arr = [10,20,30]; |
- 值类型无法添加属性和方法
- 添加了不报错但是会显示undefined
1 | let str = "hello"; |
类型比较
- 值类型的比较是值比较,只有当它们的值相等的时候才能相等
- 引用类型比较的是地址比较
Code1
2
3
4
5
6
7
8
9let num = 1;
let numStr = "1";
console.log(num == numStr) // true
console.log(num === numStr) // false 类型不一样
let obj = {}
let obj1 = {}
console.log(obj == obj1) // false
DOM事件流
当在页面上某个元素触发特定事件时,比如点击的目标元素,所在的祖先元素都是会触发该事件,一直到window。
那这样就出现了一个问题,是现在目标元素上触发事件,还是现在祖先元素上触发?这就是事件流的概念
事件流是事件在目标元素和祖先元素间触发顺序,在早期,微软和网景实现了相反的事件里流,网景主张捕获方式,微软主张冒泡方式
- 捕获(Capture):事件由最顶层逐级向下传播,直到目标元素
- 冒泡(Bubble):从下往上。事件由第一个被触发的元素接收,然后逐级向上传播
后来w3c采用泽中 的方式,规定先捕获再冒泡,如此一个事件就被分成了三个阶段
- 捕获阶段:事件从最顶层元素window一直传递到目标元素的父元素
- 目标阶段:事件到达目标元素,如果事件指定不冒泡,那就会再这里中止
- 冒泡阶段:事件从目标元素向上逐级传递知道最顶层元素window及捕获阶段的反方向
在默认的情况下都是以冒泡排序
addEventListener
在DOM2中,事件监听机制提供了一个参数来决定事件是在捕获阶段生效还是在冒泡阶段生效
语法:
1 | target.addEventListener(type, listener[, options]); |
- type:表示监听事件类型的字符串
- listener:当所监听的事件类型触发时的回调,会接收一个事件通知对象
- options:可选,可用的选项如下:
- capture:Boolean,如果是true,表示listener会在捕获阶段触发,默认是false
- once:Boolean,如果是true,表示listener在添加之后最多调用一次
- passive:Boolean,如果是true,表示listener永远不会调用preventDefault()。如果listener任然调用了这个函数,客户端将会忽略它并抛出一个警告
- useCapture:可选。Boolean,同options-capture
相比以前的事件绑定,addEventListener的方式有一下几个优势:
- 可以为同一个事件注册多个回调函数,依次触发,而onclick的方式会覆盖掉
- 使用onclick会覆盖HTML的方式
- 可以通过参数决定监听是在冒泡还是捕获
- onclick注册的监听只会在冒泡阶段生效
三种事件的绑定:
1 | <div id="app" onclick="alert(11)"></div> |
三种事件的解绑:
1 | app.setAttributer('onclick', false); |
三种方式优先级排序 onclick > html > addEventLisnter. onclick 会覆盖 html. 但是 addEventLisnter 不会被覆盖且可以叠加.
BOM
BOM(Browser Object Model) 是指浏览器对象模型,是用于描述这种对象与对象之间层次关系的模型,浏览器对象模型提供了独立于内容的、可以与浏览器窗口进行互动的对象结构。
window对象
- alert(str):向用户弹出消息提示
- confirm(str):向用户弹出确认消息框,返回选择的Boolean
- prompt(str):向用户弹出输入框,返回输入的内容
- print():打印页面
location对象
- hash:保存当前url中的哈希值(url中#号后面的字符)
- host:保存当前url中的域名和端口号
- hostName:保存当前url中的域名
- pathName:保存当前url中路劲或者文件名
- port:保存当前url的端口号
- href:返回完整的url
- protocol:返回当前url的协议
- search:返回从问号开始的url
screen对象
Navigator对象
History对象
原型链
Javascript中所有的对象都是Object的实例,并继承Object.prototype的属性和方法,也就是说,Object.prototype是所有对象的爸爸。
如果发现自己身上没有该属性,则会去原型链找
如果发现自己身上有此属性,则会先用自己的
1 | function Something(){} |
这个例子中可以看出,实例修改了foo
属性,但是并不是修改构造函数里的foo
,而是在one
实例中又添加了一个foo
的属性,构造函数中的foo
属性可以通过原型链来拿到
既然说到原型链就不得不提new
关键字
既然通过new
关键字能把自身的属性给新的实例,那么能不能不通过new
达到相同的效果
答案是可行的,在MDN
上讲解,实例上的__proto__
是等于构造函数的prototype
就拿上面的例子举例
1 | function Some(){} |
这种效果相等于new
1 | function Some(){} |
如果往自身的prototype
中挂载属性则必须通过???.prototype.属性
来获取
1 | function Some(){} |
call apply bind
三者的区别
apply,call和bind都是用来改变this的指向
apply和call会让当前函数立即执行,而bind会返回一个函数,后续需要再调用
apply参数接收的是一个数组,call和bind都是接收一个参数
call
源码实现
1 | Function.prototype.myCall = function(target){ |
例子:
1 | let foo = {value: 1}; |
我们来看看这个函数做了什么:
- 先让target做个兼容性处理
- target.fn指向this,由于this是指向调用它的人,所有这里的this指向的是bar
- 接收call穿过来的参数,平且用args变量来接收
- 这里是设计的最精妙之处,由于前面说过,this的指向是调用它的人target.fn(args)。这里的this指向的是target(foo),而target.fn又是调用这个函数(call)的人,所以bar这时的this指向了foo,改变了this的指向。并且传入参数
- 删除target.fn属性
- 返回函数返回值
bind
源码实现
1 | Function.prototype.myBind = function(target) { |
例子:
1 | var obj = { |
- 他和call的区别在于他不是立即执行的,所以使用闭包是最好的选择
apply
源码实现:
ES6(ES2015)
Proxy
Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。
我们日常会操作一些对象的增删查改,而Proxy能监视他们的操作并给出已经预置好的函数来处理他们的操作
语法:const p = new Proxy(target, handler)
- target:要使用
Proxy
包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理) - handler:一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理
p
的行为。
例子:
1 | let handler = { |
Reflect
数据请求
XMLHttpRequest:
1 | var xhr = new XMLHttpRequest(); |
fetch:
1 | fetch("./20. promise.html") |
fetch的特点:
- 基于Promise实现的接口,避免了ajax的回调地狱
- 以数据流的形式返回数据,数据量大和传输大文件有优势
- Fetch API引入三个新的对象(也是构造函数):Headers,Request和Response。headers用于设置请求头还是比较方便的。浏览器其实是不怎么需要构造请求和处理响应,这个角度看request和response比较鸡肋。但随着Service Worker的部署,以后浏览器也可以向Service Worker发起请求
- cors的支持,fetch的response.type有三种不一样的状态。basic:正常的同域请求,cors:CORS机制下的跨域请求,opaque:非CORS机制下的跨域请求,这时无法读取返回的数据,也无法判断是否请求成功
fetch的缺点:
- 首先是现在fetch的兼容性还不是很好,可能需要配置polyfill
- 其次fetch没办法像XMLHttpRequest一样取消请求的发送
跨域
跨域是指一个域下的文档或脚本试图去请求另一个域下的资源,这是跨域的广义
- 资源跳转:A链接,重定向
- 资源嵌入:
<link> <script> <img> <frame>
等标签,以及CSSbackground:url() @font-face()
等等 - 脚本请求:js发起的ajax请求,dom和js对象的跨域操作等
JSONP
JSONP的原理很简单,就是利用<script>
标签没有跨域限制的”漏洞”。通过<script>
标签指向一个需要访问的地址,并且我们可以通过callback的形式拿到请求的数据
但是JSONP只限于get请求。
CORS
CORS:全称”跨域资源共享”(Cross-origin resource sharing)。
CORS需要浏览器和服务器同时支持,才可以实现跨域请求,目前几乎所有浏览器都支持CORS,IE则不能低于IE10。CORS的整个过程都由浏览器自动完成,前端无需做任何设置,跟平时发送ajax请求并无差异。so,实现CORS的关键在于服务器,只要服务器实现CORS接口,就可以实现跨域通信。
通过设置请求头来进行跨域,nodeJS中设置
1 | res.setHeader('Access-Control-Allow-Origin', '*') |
深浅拷贝
什么是深浅拷贝
首先你要明白什么是值类型和引用类型
值类型:字符串,数字。。。
引用类型:数组,对象。。。
明白这个之后,再明白他们的区别
1 | let aa = 11; |
这个就是值类型,也是深拷贝,拷贝出来就跟原来的数据没有光系了
1 | let aa = [11,22,33]; |
总所周知引用类型的指向的是一个地址,再赋值操作的时候其实是把地址赋值给他,所以修改的是同一个区域,这就是所谓的浅拷贝
浅拷贝
Object.assign()
明白什么是浅合并之前,先明白对象的合并Object.assign(目标对象, 被合并对象)
如果被合并对象有的属性会把目标对象的给覆盖掉
他会修改原目标对象,并且返回这个合并的对象
1 | let form = {name: 'liming', sex: '男'}; |
注意:
- 此方法为浅合并
- undefined参与合并
- 原型属性不参与和合并
深拷贝
直接赋值
for in
1 | let form = {name: 'liming', sex: '男'}; |
扩展运算符
1 | var obj = { |
JSON.parse(JSON.stringify(target))
1 | let aa = {a:11,b:22}; |
for循环
1 | var arr = [1,2,3,4,5] |
slice
1 | var arr = [1,2,3,4,5] |
concat
1 | var arr = [1,2,3,4,5] |
Event
Event Delegation
- 事件代理(
Event Delegation
),又称之为事件委托。是JavaScript
中常用绑定事件的常用技巧。顾名思义,“事件代理”即是把原本需要绑定的事件委托给父元素,让父元素担当事件监听的职务。事件代理的原理是DOM元素的事件冒泡。使用事件代理的好处是可以提高性能 - 可以大量节省内存占用,减少事件注册,比如在
table
上代理所有td
的click
事件就非常棒 - 可以实现当新增子对象时无需再次对其绑定
Event Emitter
Event Hub
EventHub是基于发布订阅模式实现的一个实例,那么要搞清楚EventHub就先要搞清楚什么是发布订阅模式。
Event Loop
防抖与节流
防抖
其实是为了防止一个函数在段时间内疯狂执行。
那么在哪些场景下会发生这种现象
- 搜索框
- 鼠标移动
- 视窗resize
一个简单的例子
1 | function aa() { |
这个例子只有当你的鼠标静止不动的时候才会一直触发
节流
规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。
应用场景:
- 鼠标点击事件不断触发
- 在页面的无限加载场景下,需要用户在滚动页面时,每隔一段时间发一次 ajax 请求,而不是在用户停下滚动页面操作时才去请求数据;
- 监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断
内存泄漏
由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。
意外的全局变量
1 | function my() { |
针对上面类型的内存泄漏我们在平时一定要声明变量,不要有全局直接引用。(在JavaScript文件中添加 'use strict'
,开启严格模式,可以有效地避免上述问题。)
console.log
作为前端平时使用console.log在控制台打出相对应的信息可以说是非常常见。但如果没有去掉console.log可能会存在内存泄漏。因为在代码运行之后需要在开发工具能查看对象信息,所以传递给console.log的对象是不能被垃圾回收
闭包
1 | function my(name) { |
在my()内部创建的sendName()函数是不会被回收的,因为它被全局变量test引用,处于随时被调用的状态。如果向释放内存可以设置test=null;由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多。
DOM泄漏
1 | <div class="main"> |
在我点击了三次增加之后,可以明显的看到节点(绿线)有三次明显的增加,之后我又删除了一个节点,但绿线没有下降,这是为什么呢?,这也就是内存泄漏。原因就是删除的DOM在js中有全局的引用。也就是我删除的test在文中被引用,所以无法释放内存。所以在删除更新等操作后应该将其设置为null
timers
js中常用的定时器setInterval()、setTimeout().他们都是规定延迟一定的时间执行某个代码,而其中setInterval()和链式setTimeout()在使用完之后如果没有手动关闭,会一直存在执行占用内存,所以在不用的时候我们可以通过clearInterval()、clearTimeout() 来关闭其对应的定时器,释放内存。熟悉朋友都知道这类定时器是有误差的,所以游览器给出了专门的API-requestAnimationFrame();大家可以试一下。
requestAnimationFrame
这时HTML5中新加的一个API,主要也是做动画的
requestAnimationFramel()
浏览器每刷新一次,动画代码执行一次,不会造成丢帧现象以及动画卡顿;
1. requestAnimationFrame 会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说,这个频率为每秒60帧。
2、在隐藏或不可见的元素中,requestAnimationFrame将不会进行重绘或回流,这当然就意味着更少的的cpu,gpu和内存使用量。
函数柯里化
柯里化(Currying),把接收多个参数的函数转换册灰姑娘接受一个单一参数的函数
1 | let add = function(x) { |
奇淫技巧
!!
在JS
中判断语句一般在特殊条件下我们都会使用单个感叹号来取反,比如:
1 | const one = ""; |
通过双感叹号我们就可以拿到值现在的布尔值
>>>
1 | -1 >>> 0 // 4294967295 |