lodash 插件 cloneDeep

浅拷贝

数组:slice()/concat()/Array.from()/扩展运算符
对象:Object.assign()/扩展运算符

深拷贝

  • 通过 JSON.parse(JSON.stringify(obj))

这种方法只能复制 JSON 格式支持的属性名和值,不支持的属性名和值会直接忽略:会忽略 undefined、symbol,不能序列化函数,不能解决循环引用的对象 参考MDN

JSON.parse(JSON.stringify({
[Symbol('a')]: 'abc',
b: function() {},
c: undefined,
d: Infinity,
e: NaN,
}))
// 返回 {d: null, e: null}
  • 实现简单深拷贝
function deepCopy(source) {
if (Array.isArray(source)) {
const target = []
for (const [index, value] of source.entries()) {
target[index] = deepCopy(value)
}
return target

// 简化 => return source.map(elem => deepCopy(elem))
} else if (typeof source === 'object' && source !== null) {
const target = {}
for (const [key, value] of Object.entries(source)) {
target[key] = deepCopy(value)
}
return target

// 简化 => return Object.fromEntries(Object.entries(source).map(([key, val]) => [key, deepCopy(val)]))
} else {
// 基础类型无需拷贝
return source
}
}

Object.fromEntries() 方法把键值对列表转换为一个对象,是 Object.entries 的反转

循环引用(环)

解决思路: 通过一个WeakMap来存储拷贝过的对象

let hash = new WeakMap()
if (hash.has(source)) {
return hash.get(source)
}

hash.set(source, target)

特殊对象的拷贝

// 拷贝 Function
target = new Function(`return ${source.toString()}`)()
// 拷贝 Date
target = new Date(source.getTime())

// 拷贝 RegExp
target = new RegExp(source)

防抖和节流都是为了解决短时间内大量触发某函数而导致的性能问题,比如触发频率过高导致的响应速度跟不上触发频率,出现延迟,假死或卡顿的现象

防抖(debounce)

在事件被触发 n 秒后再执行回调函数,如果在这 n 秒内又被触发,则重新计时(短时间内连续触发的事件 只有效执行一次)

应用场景

  • 用户在输入框中连续输入一串字符后,只会在输入完后去执行最后一次的查询请求,这样可以有效减少请求次数,节约请求资源

  • window 的 resize、scroll 事件,不断地调整浏览器的窗口大小、或者滚动时会触发对应事件,防抖让其只触发一次

节流(throttle)

规定一个单位时间 n,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内事件被触发多次,只有一次能生效(每 n 秒触发一次)

应用场景

  • 鼠标连续不断地触发某事件(如点击),n 秒内只触发一次

  • 监听滚动事件,比如是否滑到底部自动加载更多

区别

防抖的作用是将多个连续的debounced调用合并为一次callback调用。防抖是基于最近次 debounced 调用来重置 waitTime,如果debounced事件触发间隔小于 waitTimecallback就不会执行;

节流的作用是限制callback调用的频率(每waitTime调用一次)。是基于上次 callback 调用来计算 waitTime 的,不管callback 事件触发有多频繁,只要距离上次 callback 调用超过了 waitTime,就一定会进行下次 callback 调用。

– 原理:

防抖是 debounced 维护了一个计时器,规定在 waitTime 时间后触发 callback,但是在 waitTime 时间内再次触发 debounced 的话,会清除当前的 timer 然后重新计时,这样一来,只有最后一次debounced 操作才能触发 callback

节流是通过判断是否到达一定时间 (waitTime) 来再次触发 callbackfuncwaitTime 时间内不能被再次触发。

实现

throttle-debounce 插件

简单实现

// 节流
function throttle(delay, func) {
let flag = true
return function() {
const context = this
const arg = arguments
if (flag) {
flag = false
setTimeout(() => {
flag = true
func.apply(context, arg)
}, delay)
}
}
}

// 防抖
function debounce(delay, func) {
let timer = null
return function() {
const context = this
const args = arguments

clearTimeout(timer)
timer = setTimeout(function() {
func.apply(context, args)
}, delay)
}
}

获取元素的方法

  • document.getElementById()
  • getElementsByClassName()
  • getElementsByTagName()
  • getElementsByName()
  • querySelector()
  • querySelectorAll()

注意事项

  • getElementById() 只能由 document 调用,如果通过其他元素调用会报错,其他方法可由已经获取到的 dom 对象调用

  • querySelectorgetElementById如果获取不到元素会返回 null;getElementsByClassNamegetElementsByTagNamegetElementsByNamequerySelectorAll 如果没有获取到元素也会返回一个伪数组,只不过伪数组长度为 0

事件

  • focus : 获得焦点
  • blur : 失去焦点
  • click : 单击
  • dblclick :双击
  • mouseover/mouseout : 进入/离开元素
  • mouseenter/mouseleave : 进入/离开元素
  • mousedown :按下
  • mouseup :抬起/释放
  • mousemove :移动
  • keydown/keypress :按下
  • keyup: 释放

属性操作

标签的自定义属性

我们之前讨论的属性,都是 HTML 规范中,标签本来就有的属性,对于标签自定义的一些属性,比较特殊

在 html 页面中,定义一个自定义属性

<div id="box" aa="bb"></div>

在对应的 DOM 对象中是不存在的,在 DOM 对象中只会存在固定的那些属性

var box = document.getElementById('box')
console.log(box.aa) // undefined

attribute 方法

attribute 系列方法用于设置标签的属性,不管是自定义的还是固有的属性

// 获取标签的属性
box.getAttribute(属性名)

// 设置标签的属性
box.setAttribute(属性名, 属性值)

// 移除标签的属性
box.removeAttribute(属性名)

区别 :

// <div a="1" id="box"></div>

// 1. 直接给标签里只能添加固有的属性 title 等 标签+对象里都有显示

// 2. 给标签添加自定义属性--标签上显示
console.log(box.a) // undefined => 对象中不显示
console.log(box.getAttribute('a')) // 1 =>

// 3. 给对象添加自定义属性 -- 标签中不显示
box.b = 2
console.log(box.b) // 2 => 对象中显示
console.log(box.getAttribute('b')) // null

// 4. Attribute方法
box.setAttribute('c', 3) // 标签中显示
console.dir(box)
console.log(box.c) // undefined =>对象中不显示
console.log(box.getAttribute('c')) // 3

console.log(box.attributes) // {0: a, 1: id, 2: c, a: a, id: id, c: c, length: 3}
console.log(box.attributes.c) // c='3'

tab 栏案例 (重点)

[案例:获取当前元素的索引]

// 方式1:
// 存
btns[i].setAttribute('index', i)
// 取
console.log(this.getAttribute('index'))

// 方式2:
// 存
btns[i].index = i
// 取
console.log(this.index)

// 区别在于: 第一个显示在标签内 第二个不显示在标签内 推荐第二种

【案例:tab 栏切换】

标签的内容属性

innerText 和 innerHTML

  • 共同点 : 都是用来获取和设置标签的内容的
  • 区别:
    • innerHTML 能够识别标签,标签能够生效
    • innerText 只识别文本,标签会被转义
var div = document.getElementById('div')
// 获取内容
// 获取标签内容的时候,只会获取文本,标签扔掉了
console.log(div.innerText) // 哈哈
// 获取标签内容的时候,不管标签还是文本,都能获取到
console.log(div.innerHTML) // <h1>哈哈</h1>

// 设置内容
// 设置标签内容的时候,覆盖原来内容,对标签进行转义
div.innerText = '<h1>嘿嘿</h1>'
// 设置内容的时候,覆盖原来内容,标签也能生效,浏览器能解析这个标签
div.innerHTML = '<h1>嘿嘿</h1>'

浏览器兼容性:指网页在各种浏览器上的显示效果不一致。或者是一些属性和方法在低版本的浏览器中不支持

  • innerText 是 IE 提出来的属性,因此低版本的火狐浏览器不支持这个属性。
  • 火狐有一个 textContent 属性,效果跟 innerText 一样,但是 IE678 不支持这个属性

书写 innerText 的兼容性代码

function getInnerText(element) {
if (typeof element.innerText === 'string') {
return element.innerText
} else {
return element.textContent
}
}

行内样式操作(style 属性)

标签不仅可以通过 class 属性操作样式 (嵌套样式),还可以通过 style 属性操作样式 (行内样式)。

同样的 DOM 对象可以通过 className 操作样式 (嵌套样式),也可以通过 style 属性操作样 (行内样式)。

css : 嵌套样式 => js : 类名 div.className = ‘red’

css : 行内样式 => js : style 对象 div.style.color = ‘red’

样式属性

  • style 属性是一个对象,里面存储了所有行内样式的键值对
  • style 属性只能获取和设置行内样式,嵌套样式通过 style 获取不到
  • 如果样式的名字带 - ,比如 background-color ,在 style 对象中使用 backgroundColor => (因为 - 在 js 中不是一个合法的标识符)
  • style 设置的样式是行内样式,优先级要高于通过 className 设置的样式
<div style="color:red;background-color:blue;">哈哈</div>
var div = document.querySelector('div')
// 获取样式
console.log(div.style)
console.log(div.style.color) // red

// 设置样式
div.style.width = '200px'
div.style.height = '200px'
div.style.fontSize = '100px'
div.className = 'box'

getComputedStyle 获取元素计算后的样式

语法: window.getComputedStyle( 获取的元素, 伪类)

​ 伪类 ==> ::after ::before,如果写上了伪类,表示要获取元素的伪类的样式,如果不需要获取的话,该参数写 null

​ 返回值: 返回一个样式对象

var div = document.querySelector('div')
// 获取元素自身的
var ret = window.getComputedStyle(div, null).fontSize
var ret1 = window.getComputedStyle(div, null).backgroundColor
// 推荐:对于复合样式,需要获取什么样式,写具体的样式名,这样能更好的兼容更多浏览器

// 获取伪类的
var ret2 = window.getComputedStyle(div, '::after').width

关于 body 的样式操作

var bd = document.querySelector('body')
console.log(bd) // 通过 querySelector 获取 body 元素
console.log(document.body) // 直接获取 body 元素

document.documentElement // 可以获取 html 元素
document.head // 直接获取 head 元素
document.title // 获取的是 title 中的文本

【案例:开关灯案例】

【案例:随机背景颜色案例】

【案例:百度换肤】

关于 cssText (了解)

使用 cssText 可以设置 style 的属性值

<div style="width: 100px; height: 100px">哈哈哈</div>
<script>
// 优点:可以一次性设置多个值
// 缺点:会覆盖整个style属性且不利于阅读
var div = document.querySelector('div')
div.style.cssText = 'background:red;color:yellow'
</script>

节点操作 (超级重要)

节点属性 (了解)

节点分类:

​ 元素节点、文本节点、属性节点、注释节点

节点常用的属性

  • childNodes : 获取所有的子节点

  • nodeType:  节点类型:元素节点 = 1 属性-2(过时) 注释-8 文本-3

    nodeType 链接-MDN

  • nodeName: 节点名称

  • nodeValue: 节点值

节点查找 (重点)

孩子节点

<ul>
<!-- 下面是li -->
<li>导航1</li>
<li>导航2</li>
<li>导航3</li>
<li>导航4</li>
</ul>
<script>
var ul = document.querySelector('ul')
// 获取ul所有的子节点(包括了元素节点和其他很多类型的节点,基本不常用)
console.log(ul.childNodes)
// 获取第一个子节点 (不常用)
console.log(ul.firstChild)
// 获取最后一个子节点 (不常用)
console.log(ul.lastChild)

// 获取所有的子元素,兼容性:IE678会把注释节点算上
console.log(ul.children)
// 获取第一个子元素 有兼容性问题(IE678)
console.log(ul.firstElementChild)
// 获取最后一个子元素 有兼容性问题(IE678)
console.log(ul.lastElementChild)
// 获取第n个子元素 有兼容性问题(IE678)
console.log(ul.children[n])
</script>

兄弟节点

  • nextSibling : 下一个兄弟节点 (基本不常用)
  • nextElementSibling : 下一个兄弟元素(IE678 不兼容)
  • previousSibling : 上一个兄弟节点 (基本不常用)
  • previousElementSibling : 上一个兄弟元素 有兼容性问题 可以封装一个兼容性方法
<p>导航1</p>
<p class="p">导航2</p>
<p>导航3</p>
<script>
var p2 = document.querySelector('.p')
console.log(p2)
console.log(p2.nextSibling)
console.log(p2.nextElementSibling)
console.log(p2.previousSibling)
console.log(p2.previousElementSibling)
</script>

【案例 1:表单校验】

父亲节点

  • parentNode : 父节点(没有兼容性问题)
  • parentElement : 父元素
div.parentNode // 获取父节点

添加节点 (重点)

appendChild()

// 作用:在子元素的最后添加一个元素
// 语法:parent.appendChild(newChild)
// 父元素.appendChild(新子元素)

注意 : 如果 newChild 已经存在于 DOM 树中,则它会被从原始位置删除

insertBefore()

// 作用:在某个子元素之前添加一个元素
// 语法:parent.insertBefore(newChild, refChild)
  1. 必须要父节点来调用,newChild 为需要添加的那个节点,refChild 为添加到哪一个节点的前面
  2. 没有 insertAfter()
// 将元素添加到最后
div.appendChild(p) // (常用)
div.insertBefore(p, null)

// 将元素添加到 s1 之前(常用)
div.insertBefore(p, s1)

// 将元素添加到 s1 之后
// 没有 insertAfter
div.insertBefore(p, s1.nextElementSibling)

// 将元素添加到最前
div.insertBefore(p, div.children[0]) // (常用)
div.insertBefore(p, div.firstElementChild)

克隆节点 (重点)

// 语法:var newNode = 节点.cloneNode([isDeep])
// isDeep参数:false / true
// false:默认值:是浅复制,只会复制标签节点本身,不会复制节点的孩子
// true: 深度复制,会复制标签,还会复制标签的所有内容
  1. 克隆出来的节点跟原来的节点没有关系了,修改了也不会相互影响
  2. 如果克隆的节点带了 id,我们需要给 id 重新设置一个值,不让 id 冲突
var newNode = div.cloneNode(true)
console.log(newNode)

创建节点(3 种方式) (重点)

document.write(基本不用)

可以生成新的节点,但是不推荐使用。如果页面已经加载完成了,再用 document.write 写内容的话,会把之前的页面给覆盖掉

原理:页面从上往下加载的时候,会开启一个文档流,当页面加载完,文档流就会关闭。document.write 的本意就是在文档流上写入内容。如果页面没加载完成,文档流还是开着的,document.write 直接在这个文档流上写东西,如果页面加载完成了,还是用 document.write 写东西,会重新开启一个新的文档流,往新的文档流上写东西,旧的文档流就被新的文档流覆盖了。

window.onload = function() {
document.write('呵呵')
}

innerHTML (偶尔用)

innerHTML 也可以创建节点

使用 innerHTML 创建节点时,如果原来有内容的话,会把原先的内容覆盖

慎用:很容易出现效率问题

div.innerHTML = '<h1>哈哈</h1>'

createElement (常用)

// 语法:var element = document.createElement('tagName')
// 返回:一个元素
// 需要配合 appendChild 和 innerText 来使用
var div = document.querySelector('div')
var h1 = document.createElement('h1')
console.log(h1)
h1.style.background = 'red'
h1.innerText = '哈'
div.appendChild(h1)

删除节点 (重点)

// 语法:parent.removeChild(child)
// 解析:父元素.removeChild(子元素)
// 功能:由父元素调用,删除里面的一个子元素

div.removeChild(p)
p.parentNode.removeChild(p)

【案例 : 节点操作-删除节点】

1. 使用 children 和 TagName  =>  需要配合 i--
// 原因 :动态计算 , 每删除一个,都会重新分配一次下标
2. 使用 querySelectorAll('li') => ok的
// 原因 : 静态计算

[案例 : 许愿墙案例]

功能1: 克隆10个tip, 并且随机分布
功能2: 点击提高层级
功能3: 点击x, 删除当前tip
功能4: 双击tip头部, 删除当前tip

替换节点

// 语法:
// newChild 为用来替换 oldChild 的新节点
parentNode.replaceChild(newChild, oldChild)

注意 : 如果 newChild 已经存在于 DOM 树中,则它会被从原始位置删除

节点操作综合案例

【动态生成表格】

BOM

BOM(Browser Object Model):浏览器对象模型,提供了一套操作浏览器功能的工具

重点 :定时器、 offset 系列

window

  • window 对象是一个全局对象,也可以说是 JavaScript 中的顶级对象
  • 所有定义在全局作用域中的变量、函数都会变成 window 对象的属性和方法
  • 像 document、alert()、console.log() 这些都是 window 的属性,其实 BOM 中基本所有的属性和方法都是 window 的
  • window 对象下的属性和方法调用的时候可以省略 window

.onload(掌握)

window.onload 事件会在 窗体加载完成 后执行,通常我们称之为入口函数。

window.onload = function() {
//代码会在窗体加载完成后执行。
//窗体加载完成 包括文档树(DOM html)的加载、还有图片、文件的加载完成。
}

如果有图片加载,那么代码一定要写到 window.onload 里面,否则会出现图片没有加载完成,获取到的宽度和高度不对的情况。

浏览器会对页面的加载做优化,在加载图片的时候,图片的引入会延迟。

<img src="./01.png" alt="">
window.onload = function () {
var img = document.querySelector('img')
console.log(img.width)
console.log(img.height)
}

.open() 与 .close() (了解)

  • window.open() 打开一个窗口
// 语法:window.open(url, [name], [features])
// 参数1:需要载入的 url 地址
// 参数2:新窗口的名称或者 targt 属性
// _blank:如果指定为 _blank,表示在新的窗口打开
// 参数3:窗口的属性,指定窗口的大小
// 返回值:会返回刚刚创建的那个窗口,在 window.close() 时使用
// 示例:
var newWin = window.open('http://www.baidu.com', '_blank', 'width=300,height=300')

// 参数配置:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/open
  • window.close() 关闭窗口
newWin.close() // newWin 是刚刚创建的那个窗口
window.close() // 把当前窗口给关闭

延时器与定时器 (重点)

setTimeout 延时器

可以在延迟一定时间后执行指定的代码

设置延时器

// 语法: setTimeOut(callback,time)
// 参数1: 回调函数, 时间到了就会执行
// 参数2: 延时的时间 毫秒为单位 1s = 1000毫秒
// 返回 : 延时器的id,用于清除
var timer = setTimeOut(function() {
//1秒后将执行一次
}, 1000)

清除延时器

// 语法 : clearTimeOut(timerId)
// 参数 : 延时器id
// 示例 :
clearTimeOut(timer) // 清除上面定义的延时器

setInterval 定时器

setInterval 方法重复调用一个函数或执行一个代码段,在每次调用之间具有固定的时间间隔。 (每隔一段时间执行一段代码)

定时器除非清除,否则会一直执行下去。

设置定时器

// 语法 :var timerId = setInterval(func,dealy)
// 参数1 : 重复执行的函数
// 参数2 : 每次间隔的毫秒数
// 返回 : 定时器的id, 用于清除
// 示例 :
var timer = setInterval(function() {
// 1s 之后开始重复执行
}, 1000)

清除定时器

// 语法 : clearInterval(intervalId)
// 参数 : 定时器id
// 示例 :
clearInterval(timerId) // 清除上面的定时器

案例

【短信验证码案例.html】

location 对象

location 对象也是 window 的一个属性

location 其实对应的就是浏览器中的地址栏

常用属性和方法

  • location.href : 控制地址栏的地址,实现页面跳转
document.onclick = function() {
location.href = 'http://www.baidu.com' // 点击页面跳转到百度首页
}

[案例 : 注册成功,3 秒后跳转]

页面跳转:

location.href = 'url地址'

location.assign('url地址')

location.replace('url地址') (不记录历史)

  • location.reload() 让页面重新加载
document.onclick = function() {
location.reload() // 重新刷新
}
  • location 的其他值

    http://www.bbb.com:8080/index.html?id=666&psd=123#xxx

    • location.hash //哈希值 其实就是锚点 ==> #xxx
    • location.host //服务器 服务器名+端口号 => www.bbb.com:8080
    • location.hostname //服务器名 => www.bbb.com
    • location.pathname //路径名 => index.html
    • location.port //端口 => 8080
    • location.protocol //协议 => http
    • location.search //参数 => ?id=666&psd=123

其他对象

  • history 对象表示页面的历史
// 随便打开一个网页 可以演示
// 后退:
history.back()
history.go(-1)
// 前进:
history.forward()
history.go(1)
  • screen 对象
console.log(screen.width) // 屏幕的宽度
console.log(screen.height) // 屏幕的高度
console.log(screen.availWidth) // 浏览器可占用的宽度
console.log(screen.availHeight) // 浏览器可占用的高度

缓动动画

缓动动画初体验

动画公式 :

var step = (target - current) / 10
current += step

[案例演示 : ]

1. 三步走
- 获取当前位置
- 累加小碎步
- 重复赋值回去
2. 定时器

缺点 : 打开控制台, 查看盒子的结构行内样式 left, 发现并没有跑到 400px, 只能跑到 396.4/395.5

原因 : offsetLeft 获取值的时候, 只会获取整数 , (对小数部分会四舍五入,整数有时候往上取整,有时候往下取整); 可以在获取的 offset 地方打印查看

缓动动画 - 移动 400 位置

动画公式 :

var step = (target - current) / 10
step = Math.ceil(step) // 往上取整
current += step

// 为什么往上取整 :
// 1. 如果不取整,赋值为小数的话,下次取值还是会取个 整数回来,这就是之前的缺点
// 2. 往上取整的额原因是:(400-395)/10 = 0.5 如果往下取整为0,那就不会走了,所以
// 为了保证可以走,往上取整 取 1 步数 为 1

案例演示 注意点 :

1.查看位置 : left有时候为 395.5/ 396.4
2.打印: offsetLeft => 395 / 396
3.step为整数 往上取整

缓动动画 - 回到 0 点位置

动画公式 :

var step = (target - current) / 10
step = Math.floor(step) //往下取整
current += step

// 为什么往下取整 :
// 1. 如果不取整,赋值为小数的话,下次取值还是会取个整数回来,这就是之前的缺点
// 2. 往上取整的额原因是 : (0-5)/10 = -0.5 如果往上取整为0 那就不会走了,所以
// 为了保证可以走,往下取整 : 取 -1 步数 为 -1

案例演示注意点 :

1. 先把盒子 设置 left : 400px 位置;  回到0位置
2. 查看位置 : left有时候为 4.5
3. 打印: offsetLeft => 5
4. step为整数 往下取整

缓动动画 - 封装函数

function animate(element, target) {
if (element.timerId) {
clearInterval(element.timerId)
}
element.timerId = setInterval(function() {
// 1. 获取当前位置
var current = element.offsetLeft
// 2. 累加小碎步
var step = (target - current) / 10
// 往上取整 ? 为什么,因为 0.5 如果网下取整也是0 ,不会走
step = step > 0 ? Math.ceil(step) : Math.floor(step)
current += step // 1
// 3. 重新赋值
element.style.left = current + 'px' //400
if (current == target) {
clearInterval(element.timerId)
}
}, 15)
}

[案例 : 筋斗云]

[案例:开机提示关闭]

事件对象

事件对象的概述

触发某个事件的时候,都会产生一个事件对象 Event,这个对象中包含所有与事件相关的一些信息,包括触发事件的元素,事件的类型以及其他与事件相关的信息

鼠标事件触发时,事件对象中会包含鼠标的位置信息

键盘事件触发时,事件对象中会包含按下的键相关的信息

获取事件对象

现代浏览器获取 : (掌握)

// 给一个形参即可
btn.onclick = function(e) {
// e 就是事件对象,里面包含了事件触发时的一些信息
console.log(e)
}

低版本浏览器 (ie678): (了解)

btn.onclick = function() {
// IE678 通过 window.event 获取事件对象
console.log(window.event)
}

兼容性 :

btn.onclick = function(e) {
// 只要用到了事件对象,就要记得处理浏览器兼容性
// 低版本IE event 不存在为undefined
e = e || window.event
}

事件对象的常用属性

事件对象中有很多很多的属性,但是很多属性并不常用。我们经常用到的是鼠标位置信息键盘码相关的信息

鼠标位置信息

  • clientXclientY : 相对于浏览器可视区左上角的位置(不随滚动条滚动而改变)
  • pageXpageY :相对于网页内容(文档 document)左上角的位置
  • screenXscreenY :相对于屏幕左上角的位置
  • offsetXoffsetY :鼠标相对于事件源左上角的位置
document.onmousemove = function(e) {
console.log(e.clientX, e.clientY)
console.log(e.pageX, e.pageY)
console.log(e.screenX, e.screenY)
}

[案例 : 拖拽案例]

键盘码

// 键盘按下的那个键的键盘码
e.keyCode

注册事件的两种方式

on + 事件名称

onclick、onmouseover 这种 on+事件名称的方式注册事件几乎所有的浏览器都支持

// 注册事件
box.onclick = function() {
// 事件处理程序
}
// 移除事件
box.onclick = null

on+事件名称注册事件的缺点:同一个元素同一类型的事件,只能注册一个,如果注册了多个,会出现覆盖问题

addEventListener

现代浏览器支持的注册事件的新方式,这种方式注册的事件不会出现覆盖问题

addEventListener 的语法

// type:事件的类型:click mouseover  字符串类型,不带 on
// fn:函数,每次点击,执行这个函数
// useCapture: 可选,true:事件在捕获阶段执行,false: 事件在冒泡阶段执行(默认)
element.addEventListener(type, fn, useCapture)
btn.addEventListener('click', function() {
console.log('哈哈')
})

removeEventListen 的语法

// type:事件的类型
// fn:要移除的那个函数
element.removeEventListener(type, fn)
btn.removeEventListener('click', fn)
// 注意 : 如果想让注册的事件能移除,不能用匿名函数

低版本浏览器兼容问题: (了解)

IE678 不支持 addEventListener 与 removeEventListen 两个方法,但是支持 attachEvent 与 detachEvnet

attachEvent 的用法:

// type: 事件类型,需要加上on
// fn: 需要执行的那个事件
attachEvent(type, fn)
btn.attachEvent('onclick', function() {
alert('哈哈')
})

detachEvent 的用法:

detachEvent(type, fn)

兼容性封装(了解)

// 添加事件
function addEvent(element, type, fn) {
// 能力检测
if (element.addEventListener) {
element.addEventListener(type, fn)
} else if (element.attachEvent) {
element.attachEvent('on' + type, fn)
} else {
// 如果都不行,那就用on方式
element['on' + type] = fn
}
}

// 移除事件
function removeEvent(element, type, fn) {
if (element.removeEventListener) {
element.removeEventListener(type, fn, false)
} else if (element.detachEvent) {
element.detachEvent('on' + type, fn)
} else {
element['on' + type] = null
}
}

事件流

事件冒泡

当一个元素的事件被触发时,同样的事件将会在该元素的所有祖先元素中依次被触发。这一过程被称为冒泡

说白了就是:当我们触发了子元素的某个事件后,父元素对应的事件也会触发

on 创建的事件默认为冒泡,无法修改

[案例 : 弹窗案例]

阻止事件冒泡 e.stopPropagation()

box.onclick = function(e) {
e.stopPropagation()
}

// 参数1:true => 捕获, false => 冒泡(默认)
box.addEventListener('click', function() {}, 参数1)

event.stopImmediatePropagation

阻止事件冒泡并且阻止相同事件的其他侦听器被调用
如果有多个相同类型事件的事件监听函数绑定到同一个元素,当该类型的事件触发时,它们会按照被添加的顺序执行。如果其中某个监听函数执行了 event.stopImmediatePropagation() 方法,则当前元素剩下的监听函数将不会被执行

阻止事件冒泡的兼容性封装

function stopPropagation(e) {
if (e && e.stopPropagation) {
e.stopPropagation() // 标准浏览器
} else {
window.event.cancelBubble = true // 兼容 IE
}
}

事件捕获

事件冒泡是 ie 提出来的

路径 : 触发事件的目标元素(son) > you > father > body > document

事件捕获是火狐提出来的

路径 : document > body > father > you > 触发事件的目标元素 (son)

解析 : 事件的处理将从 DOM 层次的根开始,而不是从触发事件的目标元素开始,事件被从目标元素的所有祖先元素依次往下传递

// 当 addEventListener 第三个参数为 true 时,表示事件捕获
arr[i].addEventListener(
'click',
function() {
//
},
true
)

事件流的三个阶段

  1. 事件的捕获阶段
  2. 事件的目标阶段(触发自己的事件)
  3. 事件的冒泡阶段

事件有三个阶段 :

  • 捕获事件和冒泡事件都存在的话,首先发生的是捕获阶段,然后是目标阶段,最后才是冒泡阶段
  • addEventListener 第三个参数为是否捕获
  • 如果为 true 时,表示该事件在捕获阶段发生
  • 如果为 false 时,表示该事件在冒泡阶段发生
  • 某一个事件只会执行一次

三大系列

offset 系列 (重要)

offset 系列用于用于获取元素自身的大小和位置,在 webapi 中有广泛应用
offset 系列主要有:offsetHeight、offsetWidth、offsetParent、offsetLeft、offsetTop

offsetHeight 与 offsetWidth

  • 获取元素真实的高度和宽度 (内容大小 + border + padding)(关注盒子本身,不关注盒子内部内容)
  • 获取到的是数值类型,方便计算
  • offsetHeight 与 offsetWidth 是只读属性,不能设置

style.height 与 style.width

  • 只能获取和设置行内样式
  • 不包括内边距、边框和外边距
  • 获取到的是字符串类型,需要转换

offsetParent

  • 获取离当前元素最近的定位父元素(absolute、relative),如果没有,那就找 body

parentNode : 父节点(没有兼容性问题)

parentElement : 父元素

offsetLeft 与 offsetTop

  • 获取元素自身与 offsetParent 真实的距离
  • 获取到的是数值类型,方便计算
  • 只读属性,只能获取,不能设置

style.left 与 style.top

  • 只能获取和设置行内样式
  • 获取到的是字符串,需要转换

scroll 系列

scroll 系列是用来获取盒子内容的大小和位置

scroll 系列主要有 : scrollWidth、scrollHeight、scrollLeft、scrollTop

scrollWidth 与 scrollHeight

  • scrollWidth 与 scrollHeight 是盒子内容的宽度和高度。与盒子大小无关,仅仅与盒子的内容有关系(padding + 内容)
  • 如果内容没有溢出,scrollHeight 就是盒子高度 (scrollWidth 同理) => 与 clientHeight、clientWidth 相同
  • 如果内容超过盒子,scrollHieght 就是内容高度 (scrollWidth 同理)

scrollTop

  • scrollTop 用于获取内容垂直滚动的像素数。如果没有滚动条,那么 scrollTop 值是 0
  • 内容超过盒子,盒子设置 overflow: scroll 就可出现滚动条
  • 此属性是可读写的

scrollLeft

  • scrollLeft 用于获取内容水平滚动的像素数
  • 此属性是可读写的
  • 浏览器切忌这种出现水平滚动条,用户体验极差,避免

scrollX 与 scrollY

  • 只读属性

  • scrollY:文档在垂直方向已滚动的像素值
    pageYOffset 属性是 scrollY 属性的别名
    为了跨浏览器兼容,请使用 window.pageYOffset 代替 window.scrollY

onscroll 事件

对于有滚动条的盒子,可以使用 onscroll 注册滚动事件,每滚动一像素,就会触发该事件

var div = doucment.getElementById('div')
div.onscroll = function() {
console.log(div.scrollLeft)
console.log(div.scrollTop)
}

场景 : 获取页面被卷去的高度和宽度

通常来说,scroll 家族用的最多的地方就是用来获取页面 被卷去的高度,非常的常用

  • 对于老式的浏览器,需要获取 html 或者 body 的 scrollTop
  • 对于现在浏览器,使用 window.pageYOffset 进行获取

页面被卷去的高度和宽度的兼容性封装

// 给整个页面注册滚动事件
document.onscroll = function() {
var scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0
var scrollLeft = window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft || 0
console.log(scrollLeft, scrollTop)
}

[案例 : 固定导航案例]

client 家族

clien t 家族用于获取盒子可视区的大小 (内容 + padding)

client 家族有 clientWidth、clientHeight、clientLeft、clientTop

clientWidth、clientHeight 可视区宽高

clientTopclientLeft 完全没有用,他们就是 borderTop 与 borderLeft

onresize 事件:onresize 事件会在窗口被调整大小的时候发生。

window.onresize = function() {
// 事件处理程序
}

场景 : client 系列一般用来获取页面的可视区宽高

低版本浏览器 : 获取的 html 和 body

高版本的浏览器 : window.innerWidth (掌握)(只读属性)

// 因为求的是窗口大小所以用 window
window.onresize = function() {
var W = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth
console.log(W)
var H = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight
console.log(H)
}

let 与 const

[知乎]我用了两个月的时间才理解 let

[MDN]变量提升

let 的使用

  • let 声明的变量只有在当前作用域(块作用域)有效
{
var a = 1
let b = 2
}

console.log(a) // 1
console.log(b) // ReferenceError: b is not defined
  • 不允许重复声明
var a = 1
let a = 2 // SyntaxError: Identifier 'a' has already been declared
let b = 3
const b = 4 // SyntaxError: Identifier 'b' has already been declared
  • 使用 let 声明的全局变量,不会成为 window 的属性
let c = 1
console.log(window.c) // undefined
console.log(c) // 1
  • 存在变量提升
let a = 1
{
a = 2
let a
}
// 如果 let 不会提升,那么 a = 2 就会将外面的 a 由 1 变成 2
// 但运行发现 a = 2 报错:Uncaught ReferenceError: Cannot access 'a' before initialization
a = 1; let a  // Uncaught ReferenceError: Cannot access 'a' before initialization

总结:

  • let/const 声明的「创建」过程被提升了,但是「初始化」没有提升,var 声明的「创建」和「初始化」都被提升了,但「赋值」没被提升,function 声明的「创建」、「初始化」和「赋值」都被提升了
  • let 声明会提升到块顶部,从块顶部到该变量的初始化语句,这块区域叫做 TDZ(暂时死区),所谓暂时死区,就是不能在初始化之前,使用变量
  • 如果你在 TDZ 内使用该变量,JS 就会报错

如果 let x 的初始化过程失败了,那么

  • x 变量就将永远处于 created 状态
  • 你无法再次对 x 进行初始化(初始化只有一次机会,而那次机会你失败了)
  • 由于 x 无法被初始化,所以 x 永远处在暂时死区

const 的使用

const 声明一个常量。常量:代码执行的过程中,不可以修改常量里面的值

  • const 声明的量不可以改变
const PI = 3.1415
PI = 3 // TypeError: Assignment to constant variable
  • const 声明的变量必须赋值
const num
// SyntaxError: Missing initializer in const declaration
  • 如果 const 声明了一个对象,仅仅保证地址不变,可以修改对象的属性
const obj = { name: 'zs' }
obj.age = 18 // 正确
obj = {} // TypeError: Assignment to constant variable
  • 其他用法和 let 一样

模板字符串

// 定义一个字符串
let str = `hello world`

// 内部允许换行
let str = `
hello
world
`

// 内部可以使用表达式
let str = `你好,我是${name}`

箭头函数

特点

  • 不存在 prototype 这个属性
let a = () => {}
console.log(a.prototype) // undefined
  • 没有自己的 this,arguments

箭头函数的 this、arguments 都是在定义函数时绑定外层的 this 和 arguments,而不是在执行过程中绑定的,所以不会因为调用者不同而发生变化。
可以使用剩余参数(Rest 参数)表示法获得的自身入参列表

因为箭头函数没有 this,因此箭头函数不能作为构造函数

不能用 call()、apply()、bind() 这些方法改变 this 的指向

fn = function(){
let arrow = (...args) => {
console.log(arguments) // 外层的入参列表 -> Arguments(3) [1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ]
console.log(args) // 使用剩余参数表示法获得的自身入参列表 -> (3) [4, 5, 6]
}
arrow(4, 5, 6)
console.log(arrow.length) // 0
}
fn(1, 2, 3)
  • 如果函数体只有一行语句,并且需要返回这个值,那么可以省略 {} 和 return
let fn = (n1, n2) => n1 + n2
  • Rest 参数和 arguments 对象的区别:

rest 参数只包括那些没有给出名称的参数,arguments 包含所有参数

rest 参数之后不能再有其他参数,否则会报错

函数的 length 属性,不包括 rest 参数

arguments 对象不是真正的数组,而 rest 参数是数组实例,可以直接使用数组的方法

对象简化语法

// 当属性的 key 和变量的名相同时可以简写
let person = { name: name } ==> let person = { name }

// 声明函数
let cal = {
add: function () {
return 1
},
// 可以省略 `:function`
add(){
return 1
}
}

属性名表达式

  • ES6 允许字面量定义对象时,用表达式作为对象的属性名,即把表达式放在方括号内。
let propKey = 'foo'
let methodKey = 'bar'

let obj = {
[propKey]: true, // foo: true
['a' + 'bc']: 123, // abc: 123
[methodKey]() {
return 'hi'
}
}

class 关键字

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Classes

ES5 中通过 构造函数 + 原型 的方式来实现面向对象

// 构造函数
function Person() {
this.name = 'jack'
this.age = 18
}

// 在原型中添加实例方法
Person.prototype.say = function() {
console.log(this.name, this.age)
}

// 创建实例
const p = new Person()
p.say()

ES6 中出现了 class 关键字,用来实现面向对象

class 声明不允许再次声明已经存在的类,否则将会抛出一个类型错误
class 声明不可以提升
class 仅仅是一个语法结构(语法糖),本质还是函数,实现继承本质上还是通过构造函数 + 原型的方式

class Person {}
Person instanceof Function // true
Person.prototype.constructor === Person // true

类声明

// 创建 Person 类
class Person {
// 类的构造函数 constructor 固定名称
constructor(name, age) {
this.name = name
this.age = age
}

// 添加实例方法
say() {
console.log(this.name, this.age)
}
}

// 创建实例
const p = new Person('tom', 18)
console.log(p) // Person {name: 'tom', age: 18}
p.say() // tom 18

类的内部所有定义的方法,都是不可枚举的

Object.keys(Person.prototype) // []
Object.getOwnPropertyNames(Person.prototype) //

类表达式

赋予一个命名类表达式的名称是类的主体的本地名称

// 匿名类
let Person = class {}
new Person() // Person {}

// 命名类
let Person = class A {}
new Person() // A {}
new A() // Uncaught ReferenceError: A is not defined
console.log(Person) // class A {}
console.log(A) // Uncaught ReferenceError: A is not defined

类表达式也不存在提升

static 关键字用来定义一个类的静态方法。调用静态方法不需要实例化该类,但不能通过一个类实例调用静态方法

class Point {
constructor(x, y) {
this.x = x
this.y = y
}

static distance(a, b) {
const dx = a.x - b.x
const dy = a.y - b.y

return Math.hypot(dx, dy)
}
}

const p1 = new Point(5, 5)
const p2 = new Point(10, 10)

console.log(Point.distance(p1, p2))

继承:要实现至少需要两个 class(子类 和 父类),子类继承自父类,继承后,子类就可以使用父类中的属性或方法

// 继承

// 父类
class Person {
constructor(name, age) {
this.name = name
}

say() {
console.log('父类中的 say 方法', this.name)
}
}

// 子类
class Chinese extends Person {
constructor(name, age) {
// 子类中使用 constructor 必须手动调用 super
// super 表示父类的构造函数
// 先调用 super() 再使用 this
super()
this.name = name
this.age = age
}
}

// 创建实例
const c = new Chinese('zs', 18)
console.log(c)
c.say() // 父类中的方法

静态方法 static

静态方法不会被实例继承,而是直接通过类来调用

class Person {
static play() {
return 'play'
}
}

Person.play() // 'play'

var person = new Person()
person.play() // err person.play is not a function

静态方法可以与非静态方法重名

class Person {
static play() {
return 'static play'
}
play() {
return 'play'
}
}


Person.play() // 'static play'

var person = new Person()
person.play() // 'play'

父类的静态方法,可以被子类继承, 静态方法也是可以从super对象上调用

class Person {
static play() {
return 'static play'
}
}

class Child extends Person {
static childPlay() {
return 'Child ' + super.play()
}
}

Child.play() // 'static play'
Child.childPlay() // 'Child static play'

解构赋值

ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)

// 1. 对象解构
var { a, b } = { a: 10, b: 20 }

// 同
;({ a, b } = { a: 10, b: 20 }) // 使用没有声明的赋值,数组解构类似
console.log(a, b) // 10 20

// 提取变量并赋值
var { a: p, b: q } = { a: 10, b: 20 }
console.log(p, q) // 10 20

// 将剩余属性赋值给一个变量
var { a, b, ...rest } = { a: 10, b: 20, c: 30, d: 40 }
console.log(a, b, rest) // 10 20 {c: 30, d: 40}

// 提供默认值
var { a = 1, b = 1 } = { a: 10 }
console.log(a, b) // 10 1

// 赋值并提供默认值
var { a: aa = 10, b: bb = 1 } = { a: 10 }
console.log(aa, bb) // 10 1

// 2. 数组解构
var [a, b] = [1, 2]
console.log(a, b) // 1 2

// 将剩余数组赋值给一个变量
var [a, b, ...rest] = [1, 2, 3, 4]
console.log(a, b, rest) // 1 2 [3, 4]
// ==> var a = arr[0]; var b = arr[1]

// 提供默认值
var [c = 2, d = 2] = [10]
console.log(c, d) // 10 2

// 忽略某些值
var [a = 2, , b = 2] = [10, 20, 30]
console.log(a, b) // 10 30

// 3. 函数参数的解构赋值
function foo({ x }) {
console.log(x) // 1
}
foo({ x: 1, y: 2 })

// 函数参数默认值
function foo({ x = 10 }) {
console.log(x) // 10
}
foo()

// 4. 解构的特殊应用
// 交换变量
var a = 1
var b = 3
;[a, b] = [b, a]
console.log(a) // 3
console.log(b) // 1

// 字符串解构
var str = 'love'
var [a, b, c, d] = str
console.log(a, b, c, d) // l o v e

扩展运算符

扩展运算符(spread)是三个点(…)。作用:将一个数组转为用逗号分隔的参数序列

var arr = ['a', 'b', 'c']
console.log(...arr) // a b c

应用

// 数组深拷贝
var arr = [1, 2, 3]
var arr1 = [...arr]
console.log(arr === arr1) // false, 说明arr1和arr指向不同数组

// 把一个数组插入另一个数组字面量
var arr2 = [...arr, 4, 5, 6]
console.log(arr2) // [1, 2, 3, 4, 5, 6]

// 字符串转数组
var str = 'love'
var arr3 = [...str]
console.log(arr3) // [ 'l', 'o', 'v', 'e' ]

对象展开

let defaults = { name: 'zs', age: 18 }
let search = { ...defaults, age: 12 } // { name: 'zs', age: 12 } 后面的属性会覆盖前面的属性

对象展开仅包含对象自身的可枚举属性

class C {
p = 12
m() {}
}
let c = new C()
let clone = { ...c }
clone.p // ok
clone.m() // error!

ES6 模块化

闭包(closure)的概念

闭包是函数和声明该函数的词法环境的组合

在 js 中,在函数中可以(嵌套)定义另一个函数时,如果内部的函数引用了外部的函数的变量,产生闭包

产生闭包的条件:有两个函数,是嵌套关系,内部函数引用了外部函数的变量

闭包的作用:

  • 私有变量,保护数据安全
  • 持久化数据
// 闭包的基本模型
function outer() {
var num = 10
function inner () {
num++
console.log(num)
}
return inner // 把inner函数给返回出去,让外部能够调用inner函数
}

// 并不一定是有返回函数才算是产生了闭包
var f3
function f1() {
var a = 2
f3 = function() {
console.log(a)
}
}
f1()
f3() // 2

闭包的应用

计数器

需求:统计一个函数的调用次数

var count = 0
function fn() {
count++
console.log('我被调用了,调用次数是' + count)
}
fn()
fn()
fn()
// 缺点:count是全局变量,不安全

使用闭包解决这个问题

function outer() {
var count = 0 // 私有变量, 将 count 保护起来了
function add() {
count++
console.log('当前count' + count)
}
return add
}
var result = outer()
result()

缓存的私有化

计算斐波那契数列,会有很大的性能问题,因为重复的计算了很多次,因此我们可以使用缓存来解决这个性能问题。

缺点:既然使用缓存,就需要保证缓存的数据的安全,不能被别人修改,因此,需要使用闭包来实现缓存的私有化。

function outer() {
// 缓存
var arr = []

var fbi = function(n) {
if (n == 1 || n == 2) {
return 1
}
if (arr[n]) {
return arr[n]
} else {
var temp = fbi(n - 1) + fbi(n - 2)
arr[n] = temp //存入缓存
return temp
}
}
return fbi
}
var fbi = outer()
console.log(fbi(40))

闭包存在的问题

正常情况下:函数在调用的时候,去开辟一块内存空间用来执行内部的代码,当函数调用结束的时候,要销毁开辟的空间,节省内存
闭包占用的内存是不会被释放的,因此,如果滥用闭包,会造成内存泄漏的问题。闭包很强大,但是只有在必须使用闭包的时候才使用

js 的垃圾回收机制(了解)

  • 内存:计算机中所有程序的运行都是在内存 中进行的,因此内存的性能对计算机的影响非常大,运行程序需要消耗内存,当程序结束时,内存会得到释放。
  • javascript 分配内存:当我们定义变量,javascript 自动分配内存存储数据。无论是值类型或者是引用类型,都需要存储在内存中。
  • 垃圾回收:当代码执行结束,分配的内存已经不需要了,这时候需要将内存进行回收,在 javascript 语言中,垃圾回收机器会帮我们回收不再需要使用的内存。
引用记数法清除

引用记数垃圾收集:如果没有引用指向某个对象(或者是函数作用域),那么这个对象或者函数作用域就会被垃圾回收机制回收。

var o = {
name: 'zs'
}
// 对象被 o 变量引用,引用记数 1
var obj = o // 变量被 o 和 obj 引用,引用记数 2
o = 1 // o 不在引用对象了,引用记数 1
obj = null // obj 不在引用对象了,引用记数 0,可以被垃圾回收了

引用计数法无法解决循环引用导致的内存泄露

function fn() {
var obj1 = {} // 引用计数为 2
var obj2 = {} // 引用计数为 2
obj1.a = obj2
obj2.b = obj1
}
fn() // 销毁fn调用开辟的空间, 但是由于引用计数考虑到两个对象都不是零引用的对象,就不能够被垃圾回收机制给回收掉
标记清除法清除

使用引用计数法进行垃圾回收的时候,会出现循环引用导致内存泄漏的问题。因此现代的浏览器都采用标记清除法来进行垃圾回收。

这个算法假定设置一个叫做根(root)的对象(在 Javascript 里,根是全局对象 Window)。定期的,垃圾回收器将从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和所有不能获得的对象。

从 2012 年起,所有现代浏览器都使用了标记 - 清除垃圾回收算法。

闭包占用内存释放

当闭包的功能不在需要使用了,将这个变量指向 null, 这样闭包占用的内存就可以被回收掉了

function outer() {
var count = 0
function fn() {
count++
console.log('执行次数' + count)
}
return fn
}
var result = outer()
result()
result = null // 当函数 fn 没有被变量引用了,那么函数 fn 就会被回收,函数 fn 一旦被回收,那么 outer调用形成的作用域也就得到了释放

语法

parseInt() 函数解析一个字符串参数,指定该字符串为指定基数的进制值,并返回一个 10 进制的整数,如果被解析参数的第一个字符无法被转化成数值类型,则返回 NaN

参考 parseInt

parseInt(string, radix)

string 要被解析的值,如果参数不是一个字符串,则将其转换为字符串

radix 基数,表示进制,介于 2 和 36 之间的整数,参数 radix 的值为undefined、0 或没有设置该参数时,parseInt() 会根据 string 来判断数字的基数。如果输入的 string 以 “0x”或 “0x”(一个0,后面是小写或大写的X)开头,那么radix被假定为16,字符串的其余部分被解析为十六进制数。如果输入的 string以 “0”(0)开头, radix被假定为10(十进制)。如果输入的 string 以任何其他值开头, radix 是 10 (十进制)。

parseInt('123', 5) // 将 '123' 看作 5 进制数,返回十进制数 38
parseInt('4215213', 5) // 4 * 5^2 + 2 * 5^1 + 1 * 5^0 = 111 返回 111
parseInt('0x123') // 291
[1, 2, 3].map(parseInt) // [1, NaN, NaN]
// [1, 2, 3].map(parseInt(item, index))

parseFloat() 函数解析一个字符串参数并返回一个浮点数,如果给定值不能被转换成数值,则会返回 NaN

parseFloat('3.14') // 3.14
parseFloat('0x123') // 0
// [1, 2, 3].map(parseFloat) // [1, 2, 3]

问题

parseInt(0.0000005) === 5
// 如果 parseInt 第一个参数不是字符串,会将其转换成字符串
// 小于 10-6 的浮点数以指数表示
// parseint 从 float 的指数法中提取整数
String(0.0000005) // => '5e-7'
parseInt(5e-7) // => 5
parseInt('5e-7') // => 5

parseInt(1111111111111111111111) // => 1
parseInt(999999999999999999999) // => 1

parseInt((5e-7).toFixed()) // => 0

parseFloat(9999999999999999) // 10000000000000000

typeof // 只能查看基本数据类型的类型
instanceof // 判断对象的具体类型
constructor.name // 获取对象的具体类型 适用于任何类型的检测
Object.prototype.toString.call('str') // '[object String]' 适用于任何类型的检测

typeof

用于查看基本数据的数据类型, number string boolean undefined

null 比较特殊,结果是 object

如果查看复杂数据类型,返回的都是 object 类型

函数的结果是 function

// typeof 判断
// 简单类型
typeof 12 // 'number'
typeof 'abc' // 'string'
typeof true // 'boolean'
typeof undefined // 'underfined'
typeof null // 'object'

// 复杂类型 (引用类型)
typeof function() {} // 'function'
typeof [] // 'object'
typeof {} // 'object'

instanceof 判断

// 语法
object instanceof constructor

用来检测 constructor.prototype 是否存在于参数 object 的原型链中

不能用于类型识别

// instanceof 判断
var simpleStr = 'This is a simple string'
var myString = new String()
var newStr = new String('String created with constructor')
var myObj = {}
var myNonObj = Object.create(null)
var myArr = []
var myFn = function() {}

simpleStr instanceof String // 返回 false, 检查原型链会找到 undefined
myString instanceof String // 返回 true
newStr instanceof String // 返回 true
myString instanceof Object // 返回 true

myObj instanceof Object // 返回 true, 尽管原型没有定义
;({} instanceof Object) // 返回 true, 同上
myNonObj instanceof Object // 返回 false, 一种创建对象的方法,这种方法创建的对象不

myArr instanceof Array // true
myArr instanceof Object // true
myFn instanceof Object // true
myFn instanceof Function // true

constructor.name

Undefined/Null 没有 constructor 属性

var myArr = []
var myFn = function() {}
var myObj = {}
let myDate = new Date()

// 原型的构造函数
myArr.constructor.name // Array
myFn.constructor.name // Object
myObj.constructor.name // Function
myDate.constructor.name // Date

// 自定义构造函数
function Teacher(name, age) {
this.name = name
this.age = age
}
var tea = new Teacher('zs', 18)
tea.constructor.name // Teacher

Object.prototype.toString

适用于任何类型的检测,不能识别自定义对象类型

Object.prototype.toString.call('str').slice(8, -1) // String
// 正则 => RegExp
// 时间对象 => Date
// 字符串 => String
// 对象 => Object
// 数组 => Array

// 自定义构造函数
function Teacher(name, age) {
this.name = name
this.age = age
}
var tea = new Teacher('zs', 18)
Object.prototype.toString.call(tea) // '[object Object]'

原生 js 中 for 语句

可使用 continue 跳出当前循环, break 跳出整个循环

如果 for 语句在函数中,使用 return 可以结束 for 循环,同时也会结束函数后续代码的执行

for (var i = 0; i < arr.length; i++) {
console.log(arr[i])
}

原生 js 中数组的 forEach 方法

遍历数组

不能用 break continue 语句跳出整个循环
不支持 return 操作输出,return 只用于跳出当前循环

arr.forEach(function(item, index, arr) {
console.log(item)
console.log(this)
})
// 返回值: undefined

原生 js 中 for…in 语句

遍历对象

支持 break, continue 跳出循环

会枚举原型链中的属性

for (var key in obj) {
console.log(key) // 键
console.log(obj[key]) // 值
}

如果使用 for in 遍历数组,会产生一些问题

var arr = ['a', 'b', 'c']
a.name = 'd'
for (var index in arr) {
console.log(index) // '0', '1', '2', 'name'
}
  1. 数组的索引值 index 是 String 类型
  2. 会将 expando 属性也遍历出来
  3. 在某些情况下,在遍历数组元素时顺序是任意的

es6 for…of 方法

遍历类数组集合(Array, Map, Set, String, Arguments)

支持 break, continuethrow

let arr = [1, 2, 3, 4]
for (const item of arr) {
console.log(item)
}

for…of 与 for…in 的区别

参考 MDN

无论是 for…in 还是 for…of 语句都是迭代一些东西。它们之间的主要区别在于它们的迭代方式。

for…in 语句以任意顺序迭代对象的可枚举属性。

for…of 语句遍历可迭代对象定义要迭代的数据。

jquery 中的 each 方法

遍历 jQuery 对象集合,为每个匹配的元素执行一个函数

$(selector).each(function(index, element) {
// index 表示当前元素在所有匹配元素中的索引号
// element 表示当前元素 dom 对象
// this 在函数内部,this指向了element
})

$('li').each(function(index, ele) {
// $(ele).css("backgroundColor", arr[index])
$(this).css('backgroundColor', arr[index])
})

php 中 foreach 语句

用来遍历数组(关联数组和索引数组均可)。

foreach($arr as $key => $value) {
// $arr: 要遍历的数组
// $key: 键,可以是任意变量名
// $value: 值,可以是任意变量名
}
foreach($arr as $value) {

}
// 遍历关联数组
$arr = array(
"name"=>"zs",
"age"=>18,
"sex"=>20
);
foreach($arr as $k => $v) {
echo $k . "=" . $v . "<br>";
}

作用域

作用域:变量起作用的区域,也就是说:变量定义后,可以在哪个范围内使用该变量

全局作用域 :在 script 标签内,函数外的区域就是全局作用域,在全局作用内声明的变量叫做全局变量 。全局变量可以在任意地方访问。(if/while/for 语句中声明的变量也是全局变量)

函数作用域 :在函数内的区域叫做函数作用域,在函数作用域内声明的变量叫做局部变量 ,局部变量只有在当前函数内才能访问到。

自由变量:对于一个函数来说,函数内部没有声明该变量,但在函数内部有访问该变量。对于这个函数来说, 该变量就是一个自由变量。

隐式全局变量:没有使用 var 定义的变量也是全局变量,叫做隐式全局变量。

var num = 11
function fn() {
var num1 = 22
num2 = 33
num = 33
console.log(num1)
}
fn()
console.log(num)
// console.log(num1)
console.log(num2)

变量的查找规则:

  • 函数内部可以使用函数外部的变量
  • 有局部变量就用局部变量,没有局部变量就用全局变量。

函数作用域是在函数定义的时候作用域就确定下来了,和函数在哪调用无关

var num = 123
function f1() {
console.log(num)
}

function f2() {
var num = 456
f1()
}
f2() // 123

作用域链

作用域链:只要是函数,就会形成一个作用域,如果这个函数被嵌套在其他函数中,那么外部函数也有自己的作用域,这个一直往上到全局环境,就形成了一个作用域链

变量的搜索原则:从当前作用域开始查找,一直查询到全局作用域,如果存在,就返回。如果在全局中也没有找到该变量会报错

// 1.
var num = 10
fn1()
function fn1() {
console.log(num) // undefined
var num = 20
console.log(num) // 20
}
console.log(num) // 10

// 2
var num = 10
fn1()
function fn1() {
console.log(num) // 10
num = 20
console.log(num) // 20
}
console.log(num) // 20

// 3
var num = 123
function f1(num) {
console.log(num) // 456 undefined
}
function f2() {
var num = 456
f1(num)
f1()
}
f2()

// 4
var num1 = 10
var num2 = 20
function fn(num1) {
num1 = 100
num2 = 200
num3 = 300
console.log(num1) // 100
console.log(num2) // 200
console.log(num3) // 300
var num3
}
fn()
console.log(num1) // 10
console.log(num2) // 200
console.log(num3) // error

预解析

预解析过程:js 解析器在执行代码前,会把所有变量的声明和函数的声明提升到当前作用域的顶部。例如var a = 11其实会分为var aa = 11两部分,其中var a会被提升

预解析规则:

  1. var 声明的变量:只提升声明,不会提升赋值
  2. 函数声明:整体提升
  3. 先提升 var 声明的变量,后提升函数声明
  4. 遇到重名的 var 声明, var 声明会被忽略,值会保留
  5. 遇到重名的函数声明,后者会覆盖前者
  6. 如果 var 声明和函数声明同名,函数声明会把 var 声明覆盖
// 函数预解析
// 1.
function fn() {
console.log(a) // undefined
}
fn()
var a = 1

// 2.
var n = 45
function fn5() {
console.log(n) // undefined
n = 20
console.log(n) // 20
var n = 0
console.log(n) // 0
}
fn5()
console.log(n) // 45

// 3.
console.log(b) // 函数体
var b = 23
function b() {
console.log(b)
}
console.log(b) // 23
// b() // 报错

// 4.
console.log(c) // 函数体
c() // 嘿嘿
var c = function() {
comsole.log('哈哈')
}

function c() {
console.log('嘿嘿')
}

// 5.
console.log(fn1) // 函数体
fn1()
function fn1() {
console.log('哈哈') // 哈哈
}
console.log(fn2) // undefined
fn2() // 报错
var fn2 = function() {
console.log('嘿嘿')
}
// 对于函数表达式,函数的调用必须在表达式声明之后
fn2() // 嘿嘿

// 6.
// 只有用 var 声明的变量才会预解析
console.log(d) // 报错
d = 5

// 7.
console.log(e)
console.log(f) // 报错 f is not defined
var e = (f = 10)
console.log(f) // 10

// 8.
if ('a' in window) {
var a = 'abc'
}
console.log(a) // abc

不要在一个作用域内重复的声明相同的变量和函数

函数的参数

arguments 对象里保存了所有的实参,是一个伪数组

定义函数的三种方式

  • 函数声明
fn() // 函数声明可以先调用,在声明
function fn() {
console.log('这是函数声明')
}
  • 函数表达式
var fn = function() {
console.log('这是函数表达式')
}
fn() // 函数表达式必须先声明,再调用
  • 构造函数 Function
// 函数也是对象,可以使用 Function 构造函数 new 出来
// 相当于var fn = function () {}
var fn = new Function()

// 语法:new Function(arg1,arg2,arg3..,body)
// 所有的参数都是字符串类型
// 前面可以定义任意多个形参,最后一个参数是代码体
var fn = new Function('alert(1)')
fn()

var fn1 = new Function('a1', 'a2', 'alert(a1 + a2)')
fn1(1, 2)

Function 属性

  • length:获取形参的长度
  • name:获取函数的名字,此属性不允许修改
Function.length // 1
Function.prototype.length // 0
(function() {}).length // 0
(function(a) {}).length // 1
(function(...args) {}).length // 0
(function(a, b = 1, c) {}).length // 1

Function.prototype 成员

  • arguments:已废弃,获取函数的实参,现在推荐的做法是使用函数内部可用的  arguments 对象来访问函数的实参
  • caller: 已废弃,用于获取当前函数是在哪个函数中调用的
  • constructor:指向当前构造函数,Function
  • call:调用函数,重新指定 this
  • apply:调用函数,重新指定 this
  • bind:重新指向 this,返回一个新的函数,不调用
  • toString : 得到函数的字符串格式
function a() {}
a.toString() // 'function a() {}'

// 获取数据类型
return Object.prototype.toString.call(obj).slice(8, -1) // '[object 构造函数]'

函数的四种调用模式

分析 this 指向问题

  1. 任何函数都有属于自己的 this
  2. this 是动态的,this 在函数声明的时候是确定不了的,只有当函数被调用了才能够确定 this 的指向,this 的指向和函数在哪被调用没有关系

分析 this 的问题的思路: 1. this 是属于哪个函数 2. 这个函数是何种调用模式

函数调用模式

如果一个函数不是一个对象的属性时,就是被当做一个函数来进行调用的。此时 this 指向了 window

function fn() {
console.log(this) // 指向 window
}
fn()

方法调用模式

当一个函数被保存为对象的一个属性时,我们称之为一个方法。当一个方法被调用时,this 被绑定到当前对象
通过点语法或者中括号语法来访问方法,都是属于方法调用模式

var f = function() {
console.log(this)
}
var obj = { fn: f }
var arr = [f]

obj.fn() // obj
obj['fn']() // obj
arr[0]() // arr 也是方法调用模式

构造函数调用模式

如果函数是通过 new 关键字进行调用的,此时 this 被绑定到创建出来的新对象上

function Person() {
console.log(this)
}
var p = new Person() // this 指向 p

总结:分析 this 的问题,主要就是区分函数的调用模式,看函数是怎么被调用的

// 1.
var age = 38
var obj = {
age: 18,
getAge: function() {
console.log(this.age)
}
}
var f = obj.getAge
f() // window ==> 38

// 2.
var age = 38
var obj = {
age: 18,
getAge: function() {
console.log(this.age) // obj ==> 18
function foo() {
console.log(this.age) // window ==> 38
}
foo()
}
}
obj.getAge()

// 3.
var length = 10
var age = 18
function fn() {
console.log(this.length)
}
var arr = [fn, '222']
fn() // 10
arr[0]() // 2

// 4.
var length = 10
function fn() {
console.log(this.length)
}
var obj = {
length: 5,
method: function(fn) {
fn() // window ==> 10
arguments[0]() // argument ==> 3
}
}
obj.method(fn, 10, 5)

// 5.
let len = 10
function fn() {
console.log(this.len)
}
fn() // window ==> undefined

let Person = {
len: 5,
say: function() {
fn() // window ==> undefined
arguments[0]() // arguments ==> undefined
}
}
Person.say(fn)

// 6.
var obj = {
bar: function() {
var x = () => this
return x
}
}

// 作为obj对象的一个方法来调用bar,把它的this绑定到obj。
// 将返回的函数的引用赋值给fn。
var fn = obj.bar()

// 直接调用fn而不设置this,
// 通常(即不使用箭头函数的情况)默认为全局对象
// 若在严格模式则为undefined
console.log(fn() === obj) // true

// 但是注意,如果你只是引用obj的方法,而没有调用它
var fn2 = obj.bar
// 那么调用箭头函数后,this指向window,因为它从 bar 继承了this。
console.log(fn2()() == window) // true

方法借用模式

上下文调用模式也叫方法借用模式,分为 apply,call ,bind
任何函数都可以调用 apply,call ,bind 这三个方法

call 方法

call 方法可以调用一个函数,并且可以指定这个函数的 this 指向
call 方法也可以和 () 一样,进行函数调用
第一个参数:指定函数的 this,如果不传,则 this 指向 window;其余参数:和函数的参数列表一模一样

// 调用函数
function fn() {
console.log(1)
}
fn.call() // 1

// 改变 this 指向
function fn() {
console.log(this)
}
fn.call({ name: 'zs' }) // { name: 'zs' }

apply 方法

apply()方法的作用和 call()方法类似,只有一个区别,就是apply()方法接受的是一个包含多个参数的数组。而call()方法接受的是若干个参数

function fn(n1, n2) {
console.log(this)
console.log(n1 + n2)
}
fn.apply({ name: 'zs' }, [10, 20]) // {name: 'zs'}, 30
// apply 的特性:平铺性,把数组中的每一项取出来作为函数的实参

bind 方法

bind() 方法创建一个新的函数、可以绑定新的函数的 this 指向

返回值:新的函数(不会被调用)
参数:新函数的 this 指向,绑定后,无论使用何种调用模式,this 都不会改变

var fn = function() {
console.log(this)
}

var newFn = fn.bind([1, 2, 3])
// newFn 是 bind 创建并返回出来的
console.log(newFn)
newFn() // this ==> [1,2,3]

如果对一个函数进行多次 bind,那么上下文会是什么呢

let a = {}
let fn = function() {
console.log(this)
}
fn.bind().bind(a)() // => ?

如果你认为输出结果是 a,那么你就错了,其实我们可以把上述代码转换成另一种形式

// fn.bind().bind(a) 等于
let fn2 = function fn1() {
return function() {
return fn.apply()
}.apply(a)
}
fn2()

可以从上述代码中发现,不管我们给函数 bind 几次,fn 中的 this 永远由第一次 bind 决定,所以结果永远是 window

特殊的 this 指向

  • 定时器中的 this 指向了 window,因为定时器的 function 最终是由 window 来调用的
  • 事件中的 this 指向的是当前的元素,在事件触发的时候,浏览器让当前元素调用了 function
  • call apply bind 第一个参数表示要绑定的 this,不传、传 null 或者 undefined,this 均指向 window,但在严格模式下,不传指向 undefined,传 null 指向 null,传 undefined 指向 undefined

递归函数

递归的要求:1. 自己调用自己 2. 要有结束条件(出口)

// 计算斐波那契数列
function fn(n) {
if (n == 1 || n == 2) {
return 1
}
return fn(n - 1) + fn(n - 2)
}
console.log(fn(12))