JavaScript 常见面试题
1. 闭包
闭包就是一个函数引用另一个函数内部的变量,因为变量被引用着,所以当另外一个函数执行结束,其相应的执行上下文弹出栈时, 变量并不会被回收,因此可以用来封装一个私有变量。这既是优点也是缺点,不必要的闭包只会增加内存消耗, 因为没有使用的变量并不会被及时回收
闭包就是能够读取其他函数内部变量的函数 。例如在 javascript 中,只有函数内部的子函数才能读取 局部变量 ,所以闭包可以理解成“定义在一个 函数 内部的函数“。在本质上, 闭包是将函数内部和函数外部连接起来的桥梁 。
function f1(){
var n=999;
nAdd=function(){
n+=1
}
function f2(){
alert(n);
}
return f2;
}
var result=f1();
result(); // 999
nAdd();
result(); // 1000
在这段代码中,result 实际上就是 闭包 f2 函数 。它一共运行了两次,第一次的值是 999,第二次的值是 1000。这证明了,函数 f1 中的局部变量 n 一直保存在内存中,并没有在 f1 调用后被自动清除。
为什么会这样呢? 原因就在于 f1 是 f2 的父函数,而 f2 被赋给了一个全局变量,这导致 f2 始终在内存中,而 f2 的存在依赖于 f1,因此 f1 也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收 。
这段代码中另一个值得注意的地方,就是" nAdd=function(){n+=1}
"这一行,首先在 nAdd 前面没有使用 var 关键字,因此 nAdd 是一个全局变量,而不是局部变量。其次, nAdd 的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以 nAdd 相当于是一个 setter,可以在函数外部对函数内部的局部变量进行操作 。
使用闭包的注意点
1)由于 闭包会使得函数中的变量都被保存在内存中,内存消耗很大 ,所以不能滥用闭包,否则会造成网页的性能问题,在 IE 中可能 导致内存泄露 。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
2) 闭包会在父函数外部,改变父函数内部变量的值 。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
闭包的用途
- 可以读取函数内部的变量
- 让这些变量的值始终保持在内存中
比较典型是定义模块 ,我们将操作函数暴露给外部,而细节隐藏在模块内部
- 闭包的第一个用途是使我们在函数外部能够访问到函数内部的变量。 通过使用闭包,我们可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来 创建私有变量 。
- 函数的另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。
2. 介绍一下 js
数据类型
基本数据类型有 :
- String
- Number
- Boolean
- Null
- Undefined
- Symbol
- BigInt
引用数据类型有 :
- Object
- Array
- Function
基本数据类型的值 直接保存在 栈 中,而 复杂数据类型的值 保存在 堆 中, 通过使用在栈中保存对应的指针来获取堆中的值 。
3. isFinite & Number.isFinite 区别
都是检测 有限性 的值。两者区别在于, isFinite
函数强制将一个非数值的参数转换成数值 ,如果能转换成数值,然后再去判断是否是 有限的
Number.isFinite()
检测有穷性的值,这个方法 不会强制将一个非数值的参数转换成数值 ,这就意味着, 只有数值类型的值,且是有穷的(finite),才返回 true
。
Number.isFinite(0) // true
Number.isFinite('0') // false
Number.isFinite(Infinity) false
isFinite('0') // true
isFinite('0') // true
isFinite(Infinity) // false
4.isNaN 和 Number.isNaN 函数的区别
isNaN
会 将参数转换成数字 , 任何不能被转换成数值的都返回 true ,所以对于非数字的参数返回 true,会影响NaN
判断Number.isNaN
首先判断是不是数字,是数字在去判断是不是NaN
,这种方法更准确。
// isNaN('sdasd') true
// isNaN('21N') true
// isNaN(NaN) true
// isNaN(123) false
Number.isNaN('1232N') // false
Number.isNaN('1232') // false
Number.isNaN(21312) // false
Number.isNaN('sadas') // false
Number.isNaN(NaN) // true
5.什么是可迭代对象
一个对象必须实现 @@iterator 方法。这意味着对象(或者它原型链上的某个对象)必须有一个键为 @@iterator 的属性,可通过常量 Symbol.iterator
访问该属性
如何判断一个类型是不是可迭代对象
let someString = "hi";
typeof someString[Symbol.iterator]; // "function"
- 常见的可迭代对象,有
Array
,Map
,Set
,String
,TypeArray
,arguments
- 可以通过判断
Symbol.iterator
判断当前变量是否是可迭代对象
6.原型
- 在
js
中,我们 通常会使用构造函数来创建一个对象,每一个构造函数的内部都有一个 prototype 属性,这个属性对应的值是一个对象,这个对象它包含了可以由该构造函数的所有实例都共享的属性和方法 ,我们把它称为原型。 - 原型分为 显示原型 和 隐式原型 ,一般称
prototype
为显示原型,__proto__
称为隐式原型。 - 一般而言,
__proto__
这个指针我们应该获取这个值,但是浏览器中都实现了__proto__
属性来让我们访问这个属性,但是我们最好不要使用这个属性,因为它不是规范中规定的。 - ES5 中新增了一个
Object.getPrototypeOf()
方法,我们可以通过这个方法来获取对象的原型。
举个例子
为什么我们新建的对象可以使用 toString()
方法,这是因为我们访问一个对象的属性时,首先会在这个对象身上找,如果没有的话,我们会通过这个对象的 __proto__
找到该对象的原型,然后在这个原型对象中找,这个原型对象又没有的话,就这样子通过一直找下去,这也就是 原型链概念 。直到找到 原型链的尽头也就是 Object.prototype
。
js 获取原型的方法
假设 Demo 是一个对象,那么有三种方式
Demo.constructor.prototype
- Demo.
__proto__
Object.getPrototypeOf(Demo)
因为设置对象原型的代码:
Object.setPrototypeOf = function (obj, proto) {
obj.__proto__ = proto;
return obj;
}
6.构造函数、原型与实例之间的关系
每创建一个函数,该函数就会自动带有一个 prototype
属性。该属性是个指针,指向了一个对象,我们称之为 原型对象 。
原型对象上默认有一个属性 constructor
,该属性也是一个指针,指向其相关联的构造函数。
通过调用构造函数产生的实例,都有一个内部属性,指向了原型对象。所以实例能够访问原型对象上的所有属性和方法。
所以三者的关系是, (重要) 每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针 ,而 实例都包含一个指向原型对象的内部指针 。通俗点说就是, 实例通过内部指针可以访问到原型对象,原型对象通过 constructor 指针,又可以找到构造函数 。
function Dog (name) {
this.name = name;
this.type = 'Dog';
}
Dog.prototype.speak = function () {
alert('wang');
}
var doggie = new Dog('jiwawa');
doggie.speak(); //wang
以上代码定义了一个构造函数 Dog(), Dog.prototype
指向的原型对象,其自带的属性 construtor
又指回了 Dog,即 Dog.prototype.constructor == Dog.
实例 doggie 由于其内部指针指向了该原型对象,所以可以访问到 speak 方法。
Dog.prototype
只是一个指针,指向的是原型对象,但是这个原型对象并不特别,它也只是一个普通对象 。假设说,这时候,我们让 Dog.protptype
不再指向最初的原型对象,而是另一个类 (Animal)的实例,情况会怎样呢?
如果 Dog 原型对象变成了某一个类的实例 aaa
,这个实例又会指向一个新的原型对象 AAA,那么 doggie 此时就能访问 aaa 的实例属性和 AA A 原型对象上的所有属性和方法了。
同理,新的原型对象 AAA 碰巧又是另外一个对象的实例 bbb,这个实例 bbb 又会指向新的原型对象 BBB,那么 doggie 此时就能访问 bbb 的实例属性和 BBB 原型对象上的所有属性和方法了。
//定义一个 Animal 构造函数,作为 Dog 的父类
function Animal () {
this.superType = 'Animal';
}
Animal.prototype.superSpeak = function () {
alert(this.superType);
}
function Dog (name) {
this.name = name;
this.type = 'Dog';
}
//改变 Dog 的 prototype 指针,指向一个 Animal 实例
//当 doggie 去访问 superSpeak 属性时,js 会先在 doggie 的实例属性中查找,发现找不到
//然后,js 就会去 doggie 的原型对象上去找,doggie 的原型对象已经被我们改成了一个 animal 实例,那就是去 animal 实例上去找。先找 animal 的实例属性,发现还是没有 superSpeack, 最后去 animal 的原型对象上去找
Dog.prototype = new Animal();
Dog.prototype.speak = function () {
alert(this.type);
}
var doggie = new Dog('jiwawa');
doggie.superSpeak(); //Animal
总结来说:就是当重写了 Dog.prototype
指向的原型对象后,实例的内部指针也发生了改变,指向了新的原型对象,然后就能实现类与类之间的继承了。( 但是如果在重写原型对象之前,产生的实例,其内部指针指向的还是最初的原型对象 )。
7. arguments 对象了解吗?
arguments
对象是所有(非箭头)函数中都可用的 局部变量 。此对象包含传递给函数的每个参数,第一个参数在索引 0 处。 arguments
对象不是一个 Array
。它类似于 Array
,但除了 length 属性和索引元素之外没有任何 Array
属性
转换成数组
let args = Array.prototype.slice.call(arguments)
let args1 = Array.from(arguments)
let args2 = [...arguments]
- 非严格模式中的函数 没有 包含剩余参数、默认参数和解构赋值,那么
arguments
对象中的值 会 跟踪参数的值(反之亦然)
function func(a) {
arguments[0] = 99; // 更新了 arguments[0] 同样更新了 a
console.log(a);
}
func(10); // 99
这里 arguments 就会跟踪 a 变量
function func(a) {
a = 99; // 更新了 a 同样更新了 arguments[0]
console.log(arguments[0]);
}
func(10); // 99
- 当非严格模式中的函数 有 包含 剩余参数 、 默认参数 和 解构赋值 ,那么
arguments
对象中的值 不会 跟踪参数的值(反之亦然)。相反,arguments
反映了调用时提供的参数:
把 script 标签放在 body 闭合标签上面的原因
- JavaScript 的执行 会阻塞 HTML 的 解析渲染 ;
- 当使用 script 标签引入外部
js
文件时, Network 线程 会 阻塞 HTML 的解析 , 但 不会阻塞 HTML 的渲染 ;
JavaScript 执行确实会 阻塞 HTML 的解析渲染 , 若是以 嵌入的方式 引入 JavaScript, 不管 script 标签 是放在 head 标签中 或是 body 标签尾部 , 页面都会由于 JavaScript 的执行而持续白屏;
而在 引入外部 js 文件 的情况,由于 Network 线程 下载外部 js 文件仅阻塞 HTML 的解析而不会阻塞 HTML 的渲染, script 标签 置于 body 标签尾部 可以避免由于 js 文件下载时间太长导致的页面持续白屏!
1、 避免用户等待,让页面先展示,然后再去执行脚本 2、 防止过多 js 执行时,变量没有初始化的情况 3、 js 文件比较大,一般流程时先加载页面,然后加载并执行 js
从上图可以看出,网页的渲染流程大致如下:
- Parse HTML 该阶段生成了 DOM Tree 和 CSSOM Tree;
- Layout 将 DOM Tree 结合 CSSOM Tree, 生成 Layout Tree(又称 Render Tree), 计算每个元素的尺寸和位置;
- Update Layout Tree 更新 Layout Tree;
- Paint 生成 PaintLayout Tree 记录元素绘制顺序;
- Composite 合成视图输出到屏幕;
async VS defer
<script src="..." async></script>
此属性告诉浏览器,此脚本加载的时候,不会阻止浏览器渲染页面,只要此脚本下载完毕,就开始执行脚本 。
不过因为异步下载共有的特性, 多个脚本下载完毕的先后顺序,可不一定是按代码的顺序 ,如果两个脚本前后有依赖,使用 async 可能不是一个好主意。异步加载的方式,更适用于类似 require.js 那样的动态加载。
<script src="..." defer></script>
此属性与 async
类似,只是 运行的时机不是脚本下载完毕,而是等着整个文档加载完毕之后执行 ,类似于执行在 jQuery 的 ondomready
事件(其实也就是 DOMContentLoaded
事件, defer 在 DOMContentLoaded
之前执行)。
另外 defer 还有一个特点,它是按代码顺序来执行 的。
讲讲 PWA
PWA 全称 Progressive Web App,即渐进式 WEB 应用
一个 PWA 应用首先是一个网页,可以通过 Web 技术编写出一个网页应用. 随后添加上 App Manifest 和 Service Worker 来实现 PWA 的安装和离线等功能
- 可以 添加至主屏幕 ,点击主屏幕图标可以实现启动动画以及隐藏地址栏
- 实现 离线缓存 功能,即使用户手机没有网络,依然可以使用一些离线功能
- 实现了 消息推送
如何理解 JS 中的 this 关键字
this 表示当前对象,this 的指向是根据调用的上下文来决定的,默认指向 window 对象 。 全局环境:全局环境就是在里面,这里的 this 始终指向的是 window 对象。 局部环境: 1.在全局作用域下直接调用函数,this 指向 window。 2.对象函数调用,哪个对象调用就指向哪个对象。 3.使用 new 实例化对象,在构造函数中的 this 指向实例化对象。 4.使用 call 或 apply 改变 this 的指向。
解释一下严格模式(strict mode)
严格模式用于标准化正常的 JavaScript 语义。严格模式可以嵌入到非严格模式中,关键字 ‘use strict’。使用严格模式后的代码应遵循 JS 严格的语法规则。例如,分号在每个语句声明之后使用。
解释 JavaScript 中的 null 和 undefined
- JavaScript 中有两种底层类型:null 和 undefined。它们代表了不同的含义:
- 尚未初始化 :undefined;
- 空值 :null。
//null 和 undefined 是两个不同的对象
null == null //true
null === null //true
null == undefined //true
null === undefined //flase
字符串去重,并去除掉特殊字符,按照数字在前字母在后的顺序排序字符串
//如下:"1233fddfd&3434fdsaff&454545&4545444rfdsfds&545gdsgs"
var str = "1233fddfd&3434fdsaff&454545&4545444rfdsfds&545gdsgs";
var n = "";
var s="";
for(var i=0;i<str.length;i++){
if((str[i]>=0&&str[i]<=9)&&n.indexOf(str[i])==-1){
n+=str[i];
}else if((str.charCodeAt(i)>=97&&str.charCodeAt(i)<=122)&&s.indexOf(str[i]) == -1){
s+=str[i];
}
}
console.log(n+s); //12345fdsarg
编写一个数组去重的方法
function sort(arr) {
for(var i = 0;i<arr.length;i++){
for(var j = i+1;j<arr.length;j++){
if(arr[i] == arr[j]){
arr.splice(j,1);
j--; //删除一个元素后,后面的元素会依次往前,下标也需要依次往前
}
}
}
return arr
}
DOM 怎样添加、移除、移动、复制、创建和查找节点
- 获取子节点
父节点.children 父节点.childNodes
- 获取父节点 子节点.parentNode 子节点.offsetParent
- 创建 document.createElement(‘标签名’) document.createTextNode(‘文本内容’)
- 添加 父节点.appendChild(子节点) 父节点.insertBefore(newChild,refChild)
- 复制 被复制的节点.cloneNode(true)
- 删除: 节点.remove() 父节点.removeChild(子节点)
- 替换 父节点.replaceChild(newChild,refChild)
如何获取 url 地址中搜索内容?
window.location.search
解释一下事件流?
- 事件捕获阶段 :当事件发生的时候,将事件从 window 依次往子元素传递 确定目标阶段:确定事件目标
- 事件冒泡阶段 :事件目标开始处理事件,处理完以后会将事件依次传递给父元素,一直到 window 事件都是在事件冒泡处理,
ie
只有冒泡
ajax 请求的时候 get 和 post 方式的区别,什么时候用 post
- GET 请求会将参数跟在 URL 后进行传递,而 POST 请求则是作为 HTTP 消息的实体内容发送给 WEB 服务器。当然在 Ajax 请求中,这种区别对用户是不可见的
- GEt 传输数据容量小,不安全,post 传输数据内容大,更加安全; 当向服务器发送一些数据的时候选择 post 比较安全
js 哪些操作会造成内存泄露?
- 意外的全局变量 引起的内存泄露
function leak(){ leak=“xxx”;//leak 成为一个全局变量,不会被回收 }
- 被遗忘的定时器 或者 回调
- 闭包 引起的内存泄漏
$(document).ready() 方法和 window.onload
有什么区别?
window.onload
只能执行一次,执行第二次则第一次被覆盖; ready 可执行多次,不会覆盖;window.onload
等 文档和资源都加载完成 以后调用; ready 只要 文档结构加载完成 以后就会调用;
获取对象属性的方法
Object.keys(testObj)
返回的参数就是一个数组, 数组内包括对象内可枚举属性和方法名- for in 遍历的也可以,不过对于非继承的属性名称也会获取到,通过
hasOwnproperty
判断 Object.getOwnPropertyNames(obj)
返回的参数就是一个数组,数组内包括 自身拥有的枚举 或 不可枚举属性 名称字符串,如果是数组的话,还有可能获取到length
属性
for of 和 for in 区别
for in
- index 获取的是 索引
- 遍历的顺序可能不是按照顺序进行的
- 使用 for in 会 遍历数组所有可枚举属性,包括原型 , 例如上面的 method 和 name 都会遍历
- for in 更适合遍历对象 ,不要使用 for in 去遍历数组
for of
- for of 语法遍历的是数组元素的 值
- for in 遍历的是 索引
- for of 遍历的只是数组内的元素,而 不包括数组的原型属性 method 和 索引 name
小结
- for..of 适用遍历数/数组对象/字符串/map/set 等拥有迭代器对象的集合,不能遍历对象 ,因为没有迭代对象,与
forEach()
不同的是,它可以正确响应 break 、 continue 和 return 语句。 - for in 可以遍历一个普通的对象,这样也是它的本质工作,for in 会遍历原型以及可枚举属性 ,最好的情况下,使用
hasOwnProperty
判断是不是实例属性。
作用域链
作用域 规定了如何查找变量 ,也就是确定当前执行代码对变量的访问权限。当查找变量的时候,会先从 当前上下文的变量对象中 查找,如果没有找到,就会从 父级(词法层面上的父级)执行上下文的变量对象中 查找,一直找到 全局上下文的变量对象 ,也就是全局对象。这样 由多个执行上下文的变量对象构成的链表 就叫做 作用域链 。
函数的作用域在函数创建时就已经确定了。 当函数创建时,会有一个名为 [[scope]]
的内部属性保存所有父变量对象到其中。当函数执行时,会创建一个执行环境,然后通过复制函数的 [[scope]]
属性中的对象构建起执行环境的作用域链,然后,变量对象 VO
被激活生成 AO
并添加到作用域链的前端,完整作用域链创建完成:
Scope = [AO].concat([[Scope]]);
所以闭包,可以说是作用域链的另外一种表示形式。
手写函数防抖和函数节流
节流 throttle
规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。
- 鼠标的点击事件,比如
mousedown
只触发一次 - 监听滚动事件,比如是否滑到底部自动加载更多,用 throttle 判断
- 比如游戏中发射子弹的频率(1 秒发射一颗)
throttle(callback,wait){
let last = Date.now();
return function(...args){
if((Date.now() - last) > wait){
callback.call(this,...args);
last = Date.now();
}
}
}
防抖 (延迟)
在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则重新计时
- search 搜索,用户不断输入值时,用防抖来节约 Ajax 请求,也就是输入框事件 。
- window 触发 resize 时,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次
function debounce(callback, delay){
let timer = null;
return function(...args){
if(timer) clearTimeout(timer);
timer = setTimeout(function(){
callback.apply(this, args);
},delay)
}
}
如果仅需要 _.debounce
和 _.throttle
方法,可以使用 Lodash 的自定义构建工具,生成一个 2KB 的压缩库。使用以下的简单命令即可:
npm i -g lodash-cli
npm i -g lodash-cli include=debounce,throttle
谈一谈你对 requestAnimationFrame(rAF)理解
正好跟节流有点关系,有点相似处
动画帧率可以作为衡量标准,一般来说画面在 60fps 的帧率下效果比较好。
换算一下就是,每一帧要在 16.7ms (16.7 = 1000/60) 内完成渲染。
我们来看看 MDN 对它的解释吧!
window.requestAnimationFrame()
方法 告诉浏览器您希望执行动画并请求浏览器在下一次重绘之前调用指定的函数来更新动画。 该方法使用一个回调函数作为参数,这个回调函数会在浏览器重绘之前调用。 -- MDN
当我们调用这个函数的时候,我们告诉它需要做两件事:
- 我们需要新的一帧;
- 当你渲染新的一帧时需要执行我传给你的回调函数
rAF 与 setTimeout 相比
rAF(requestAnimationFrame) 最大的优势+是 「由系统来决定回调函数的执行时机」 。
具体一点讲就是,系统每次绘制之前会主动调用 rAF 中的回调函数,如果系统绘制率是 60Hz,那么回调函数就每 16.7ms 被执行一次,如果绘制频率是 75Hz,那么这个间隔时间就变成了 1000/75=13.3ms。
换句话说就是,rAF 的执行步伐跟着系统的绘制频率走。它能保证回调函数在屏幕每一次的绘制间隔中只被执行一次(上一个知识点刚刚梳理完 「函数节流」 ),这样就不会引起丢帧现象,也不会导致动画出现卡顿的问题。
另外它可以自动调节频率。如果 callback 工作太多无法在一帧内完成会自动降低为 30fps。虽然降低了,但总比掉帧好。
与 setTimeout 动画对比的话,有以下几点优势
- 当页面隐藏或者最小化时,setTimeout 仍然在后台执行动画,此时页面不可见或者是不可用状态,动画刷新没有意义, 浪费 CPU 。
- rAF 不一样,当页面处理未激活的状态时,该页面的屏幕绘制任务也会被系统暂停,因此跟着系统步伐走的 rAF 也会停止渲染,当页面被激活时,动画就从上次停留的地方继续执行,有效节省了 CPU 开销。
什么时候调用呢
规范中似乎是这么去定义的:
- 在重新渲染前调用。
- 很可能在宏任务之后不去调用
这样子分析的话,似乎很合理嘛,为什么要在重新渲染前去调用呢?因为 rAF 作为官方推荐的一种做流畅动画所应该使用的 API,做动画不可避免的去操作 DOM,而如果是在渲染后去修改 DOM 的话,那就只能等到下一轮渲染机会的时候才能去绘制出来了,这样子似乎不合理。
rAF
在浏览器决定渲染之前给你最后一个机会去改变 DOM 属性,然后很快在接下来的绘制中帮你呈现出来,所以这是做流畅动画的不二选择。
- 根据经验,如果 JavaScript 方法需要绘制或者直接改变属性,我会选择
requestAnimationFrame
,只要涉及到重新计算元素位置,就可以使用它。 - 涉及到 AJAX 请求,添加/移除 class (可以触发 CSS 动画),我会选择
_.debounce
或者_.throttle
,可以设置更低的执行频率
浏览器窗口可视区域大小 clientHeight
- offsetWidth : 元素的宽度值
- offsetHeight : 元素的高度值
- offsetLeft : 元素的水平偏移
- offsetTop : 元素的垂直偏移
- clientX : 元素在当前窗口中的水平偏移量
- clientY : 元素在当前窗口中的垂直偏移量
- clientWidth : 元素的宽度值(不包括边框)
- clientHeight : 元素的高度值(不包括边框)
- pageX : 元素在页面中的水平偏移
- pageY : 元素在页面中的垂直偏移
- pageX : clientX + scrollLeft
- pageY : clientY + scrollTop
- screenX : 元素在屏幕中水平偏移
- screenY : 元素在屏幕中垂直偏移
- scrollLeft : 滚动条的水平偏移距离
- scrollTop : 滚动条的垂直偏移距离
- scrollWidth : 水平滚动条能滚动的距离
- scrollHeight : 垂直滚动条能滚动的距离
获得浏览器窗口的尺寸(浏览器的视口,不包括工具栏和滚动条)的方法:
一、对于 IE9+、Chrome、Firefox、Opera 以及 Safari:
- window.innerHeight - 浏览器窗口的内部高度
- window.innerWidth - 浏览器窗口的内部宽度
二、对于 Internet Explorer 8、7、6、5:
- document.documentElement.clientHeight 表示 HTML 文档所在窗口的当前高度。
- document.documentElement.clientWidth 表示 HTML 文档所在窗口的当前宽度。
或者
Document 对象的 body 属性对应 HTML 文档的 <body>
标签
- document.body.clientHeight
- document.body.clientWidth
在不同浏览器都实用的 JavaScript 方案:
var w = document.documentElement.clientWidth || document.body.clientWidth;
var h = document.documentElement.clientHeight || document.body.clientHeight;
网页尺寸 scrollHeight
scrollHeight 和 scrollWidth,获取网页内容高度和宽度
一、针对 IE、Opera:
scrollHeight 是网页内容实际高度,可以小于 clientHeight。
二、针对 NS、FF:
scrollHeight 是网页内容高度,不过最小值是 clientHeight。也就是说网页内容实际高度小于 clientHeight 时,scrollHeight 返回 clientHeight 。
三、浏览器兼容性
var w=document.documentElement.scrollWidth
|| document.body.scrollWidth;
var h=document.documentElement.scrollHeight
|| document.body.scrollHeight;
注意:区分大小写
scrollHeight 和 scrollWidth 还可获取 Dom 元素中内容实际占用的高度和宽度。
网页尺寸 offsetHeight
offsetHeight 和 offsetWidth,获取网页内容高度和宽度(包括滚动条等边线,会随窗口的显示大小改变)。
一、值
offsetHeight = clientHeight + 滚动条 + 边框。
二、浏览器兼容性
var w= document.documentElement.offsetWidth
|| document.body.offsetWidth;
var h= document.documentElement.offsetHeight
|| document.body.offsetHeight;
网页卷去的距离与偏移量
scrollLeft: 设置或获取位于给定对象左边界与窗口中目前可见内容的最左端之间的距离 ,即左边灰色的内容。
scrollTop: 设置或获取位于对象最顶端与窗口中可见内容的最顶端之间的距离 ,即上边灰色的内容。
offsetLeft: 获取指定对象相对于版面或由 offsetParent 属性指定的父坐标的计算左侧位置 。
offsetTop: 获取指定对象相对于版面或由 offsetParent 属性指定的父坐标的计算顶端位置 。
注意:
1. 区分大小写
2. offsetParent:布局中设置 postion 属性(Relative、Absolute、fixed) 的父容器,从最近的父节点开始,一层层向上找,直到 HTML 的 body。
offsetWidth、clientWidth、scrollTop 的区别?
Element.getBoundingClientRect()
方法返回元素的大小及其相对于视口的位置
1.偏移量
元素的可见大小由其高度、宽度决定,包括所有内边距、滚动条和边框的大小(注意,不包括外边距)
offsetWidth
:当前对象的宽度,占位宽,包含 内容宽 width+左右 padding+左右 border
offsetHeight
:当前对象的高度, 内容高 height+上下 padding+上下 border
offsetLeft
:当前元素 左边框 外边缘 到 最近的已定位父级( offsetParent
) 左边框内边缘的距离。如果父级都没有定位,则分别是到 body 顶部 和左边的距离。不能对其进行赋值.设置对象到其上级层左边的距离请用 style.left
属性
offsetTop
:当前元素 上边框 外边缘 到 最近的已定位父级( offsetParent
) 上边框 内边缘的 距离。如果父级都没有定位,则分别是到 body 顶部 和左边的距离。 不能对其进行赋值.设置对象到上级层顶部边的距离请用 style.top
属性
offsetParent
:当前对象的上级层对象。
2.客户区大小
元素的客户区大小指的是 元素内容及其内边距所占据的空间大小
clientWidth
:获取元素对象人眼可见内容的宽度,包含 内容宽 width+左右 padding
clientHeight
: 获取元素对象人眼可见内容的高度, 内容高 height + 上下 padding ;
clientLeft
: 获取元素对象的 左边框 border 宽度
clientTop
:获取元素对象的 上边框 border 高度
3.滚动大小
scrollWidth
: 获取对象的滚动宽度。即 获取指定标签内容层的真实宽度 ( 可视区域宽度+被隐藏区域宽度 )。
scrollHeight
: 获取对象的滚动高度。即 获取指定标签内容层的真实高度 ( 可视区域高度+被隐藏区域高度 )
scrollLeft
: 设置或获取位于对象实际左边界和对象中目前可见内容的最左端之间的距离(width+padding 为一体
scrollTop
:页面被卷去的高,设置或获取位于对象实际最顶端的边界和对象中可见内容的最顶端边界之间的距离;(height+padding 为一体)
对于不包含滚动条的页面而言,scrollWidth 和 scrollHeight 与 clientWidth 和 clientHeight 之间的关系并不十分清晰,浏览器之间的差异很大。
var scrollTop = document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop
// 持续获取高度的方式
window.addEventListener('scroll', ()=>{
var scrollTop = document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop;
});
实现图片的懒加载
网页可见区域宽: document.body.clientWidth;
网页可见区域高: document.body.clientHeight;
网页可见区域宽: document.body.offsetWidth (包括边线的宽);
网页可见区域高: document.body.offsetHeight (包括边线的宽);
网页正文全文宽: document.body.scrollWidth;
网页正文全文高: document.body.scrollHeight;
网页被卷去的高: document.body.scrollTop;
网页被卷去的左: document.body.scrollLeft;
网页正文部分上: window.screenTop;
网页正文部分左: window.screenLeft;
屏幕分辨率的高: window.screen.height;
屏幕分辨率的宽: window.screen.width;
屏幕可用工作区高度: window.screen.availHeight;
原理思路
- 拿到所以的图片
img dom
- 重点是第二步,判断当前图片是否到了可视区范围内
- 到了可视区的高度以后,就将 img 的 data-src 属性设置给 src
- 绑定 window 的
scroll
事件
当然了,为了用户的体验更加,默认的情况下,设置一个 「占位图」
第一种方式
clientHeight-scrollTop-offsetTop
let Img = document.getElementsByTagName("img"),
len = Img.length,
count = 0;
function lazyLoad () {
let viewH = document.body.clientHeight, //可见区域高度
scrollTop = document.body.scrollTop; //滚动条距离顶部高度
for(let i = count; i < len; i++) {
if(Img[i].offsetTop < scrollTop + viewH ){
if(Img[i].getAttribute('src') === 'default.png'){
Img[i].src = Img[i].getAttribute('data-src')
count++;
}
}
}
}
function throttle(fn, delay) {
let flag = true,
timer = null;
return function (...args) {
let context = this;
if (!flag) return;
flag = false;
clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(context, args);
flag = true;
}, delay);
};
};
window.addEventListener('scroll', throttle(lazyLoad,1000))
lazyLoad(); // 首次加载
第二种方式
使用 element.getBoundingClientRect()
API 直接得到 top 值。
let Img = document.getElementsByTagName("img"),
len = Img.length,
count = 0;
function lazyLoad () {
let viewH = document.body.clientHeight, //可见区域高度
scrollTop = document.body.scrollTop; //滚动条距离顶部高度
for(let i = count; i < len; i++) {
if(Img[i].getBoundingClientRect().top < scrollTop + viewH ){
if(Img[i].getAttribute('src') === 'default.png'){
Img[i].src = Img[i].getAttribute('data-src')
count++;
}
}
}
}
function throttle(fn, delay) {
let flag = true,
timer = null;
return function (...args) {
let context = this;
if (!flag) return;
flag = false;
clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(context, args);
flag = true;
}, delay);
};
};
window.addEventListener('scroll', throttle(lazyLoad,1000))
lazyLoad(); // 首次加载
this
this 永远指向最后调用它的那个对象
- 默认指向,作为 普通函数调用 , 指向 window,严格模式下指向 undefined
- 使用 call/apply/bind 显示改变 this 指向
- new 对象 ,被实例调用,指向的就是实例对象
- 箭头函数: this 指向的是上级作用域中的 this
- class 方法:该 this 指向的就是实例
ECMAScript6 怎么写 class,为什么会出现 class 这种东西
在我看来 ES6 新添加的 class 只是为了补充 js 中缺少的一些面向对象语言的特性,但本质上来说它只是一种语法糖,不是一个新的东西,其背后还是原型继承的思想。通过加入 class 可以有利于我们更好的组织代码。在 class 中添加的方法,其实是添加在类的原型上的。
哪些操作会造成内存泄漏
- 意外的全局变量
- 被遗忘的计时器或回调函数
- 脱离 DOM 的引用
- 闭包
- 第一种情况是我们由于 使用未声明的变量 ,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。
- 第二种情况是我们 设置了 setInterval 定时器,而忘记取消它 ,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。
- 第三种情况是我们 获取一个 DOM 元素的引用,而后面这个元素被删除 ,由于我们一直保留了对这个元素的引用,所以它也无法被回收。
- 第四种情况是 不合理的使用闭包,从而导致某些变量一直被留在内存当中 。
Object.is() 使用过吗?跟 === 和 == 区别
- 两等号判等,会在比较时进行类型转换。
- 三等号判等(判断严格),比较时不进行隐式类型转换,(类型不同则会返回 false)
- 使用 Object.is 来进行相等判断时, 一般情况下和三等号的判断相同 ,它处理了一些特殊的情况,比如 -0 和 +0 不再相等,两个
NaN
认定为是相等的。
JS 事件循环机制
- 因为 js 是单线程运行的,在代码执行的时候,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行。
- 在执行同步代码的时候,如果遇到了异步事件,js 引擎并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。
- 当异步事件执行完毕后,再将异步事件对应的回调加入到与当前执行栈中不同的另一个任务队列中等待执行。
- 任务队列可以分为宏任务对列和微任务对列,当前执行栈中的事件执行完毕后,js 引擎首先会判断微任务对列中是否有任务可以执行,如果有就将微任务队首的事件压入栈中执行。
- 当微任务对列中的任务都执行完成后再去判断宏任务对列中的任务。
微任务包括了 promise 的回调、node 中的 process.nextTick 、对 Dom 变化监听的 MutationObserver
。
宏任务包括了 script 脚本的执行、setTimeout ,setInterval ,setImmediate 一类的定时事件,还有如 I/O 操作 、 UI 渲染 等。
立即执行函数是什么
声明一个函数,并马上调用这个匿名函数就叫做立即执行函数 ;也可以说立即执行函数是一种语法,让你的函数在定义以后立即执行;
作用:
- 不必为函数命名, 避免了污染全局变量
- 立即执行函数内部形成了一个单独的作用域, 可以封装一些外部无法读取的私有变量
- 封装变量
发布订阅者模式
class EventEmitter {
constructor(){
this.list = {} //list 比喻成微信平台
}
on(key,fn){ // key 比喻成公众号、fn 为订阅者
if(!this.list[key]){
this.list[key] = []
}
this.list[key].push(fn)
return this
}
once(key,fn) {
if(!this.list[key]){
this.list[key] = []
}
this.list[key].push(fn)
this.list[key].flag = this.list[key].length;
return this
}
emit(key, args){
let that = this;
let fns = this.list[key]
if(!fns || fns.length === 0) return false
for(let i = 0; i < fns.length; i++) {
fns[i].apply(this, args)
if(fns.flag === i){
that.off(key,fns[i-1])
}
}
}
off(key,fn) {
let fns = this.list[key];
let len = fns.length,
k = -1;
for(let i = 0; i < len; i++) {
if(fns[i].name === fn.name){ // 删除
k = i;
break;
}
}
if(k !== -1) {
this.list[key].splice(k,1)
}
}
allOff(key) {
if(key === undefined){
this.list = {}
}else{
this.list[key] = []
}
}
}
var emitter = new EventEmitter();
function handleOne(a, b, c) {
console.log('第一个监听函数', a, b, c)
}
function handleSecond(a, b, c) {
console.log('第二个监听函数', a, b, c)
}
function handleThird(a, b, c) {
console.log('第三个监听函数', a, b, c)
}
emitter.on("demo", handleOne)
.once("demo", handleSecond)
.on("demo", handleThird);
emitter.emit('demo', [1, 2, 3]);
// => 第一个监听函数 1 2 3
// => 第二个监听函数 1 2 3
// => 第三个监听函数 1 2 3
emitter.off('demo', handleThird);
emitter.emit('demo', [1, 2, 3]);
// => 第一个监听函数 1 2 3
emitter.allOff();
emitter.emit('demo', [1, 2, 3]);
// nothing
模块化
模块化好处
- 避免了命名冲突(减少了命名空间污染)
- 更好的分离,按需加载
- 更高复用性
- 高维护性
CommomJS
CommonJS 定义了两个主要概念:
require
函数,用于导入模块
module.exports
变量,用于导出模块
require
的第一步是 解析路径获取到模块内容 :
- 如果是核心模块,比如
fs
,就直接返回模块 - 如果是带有路径的如
/
或者./
等等,则拼接出一个绝对路径,然后 先读取缓存require.cache
再读取文件。如果没有加后缀,则自动加后缀然后一一识别。
.js
解析为 JavaScript 文本文件.json
解析 JSON 对象.node
解析为二进制插件模块
- 首次加载后的模块会缓存在
require.cache
之中,所以多次加载require
,得到的对象是同一个 。
ES6 模块与 CommonJS 的区别
CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用
CommonJS
模块输出的是 值的拷贝 ,也就是说, 一旦输出一个值,模块内部的变化就影响不到这个值 。- ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令
import
,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import
有点像 Unix 系统的“符号连接”,原始值变了,import
加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
CommonJS 模块是运行时加载,ES6 模块是编译时输出接口
- 运行时加载:
CommonJS
模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。 - 编译时加载: ES6 模块不是对象,而是通过
export
命令显式指定输出的代码,import
时采用静态命令的形式。即在import
时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”
模块化开发怎么做?
- 我对模块的理解是, 一个模块是实现一个特定功能的一组方法 。在最开始的时候,js 只实现一些简单的功能,所以并没有模块的概念,但随着程序越来越复杂,代码的模块化开发变得越来越重要。
- 由于 函数具有独立作用域的特点 ,最原始的写法是使用函数来作为模块,几个函数作为一个模块,但是这种方式容易造成全局变量的污染,并且模块间没有联系。
- 后面提出了对象写法,通过将函数作为一个对象的方法来实现,这样解决了直接使用函数作为模块的一些缺点,但是这种办法会暴露所有的所有的模块成员,外部代码可以修改内部属性的值。
- 现在 最常用的是立即执行函数的写法,通过利用闭包来实现模块私有作用域的建立,同时不会对全局作用域造成污染。
Attribute 与 Property
attribute :是 HTML 标签上的某个属性 ,如 id、class、value 等以及自定义属性
property :是 js 获取的 DOM 对象上的属性值 ,比如 a,你可以将它看作为一个基本的 js 对象。
let demo11 = oDiv.getAttribute('class');
let demo2 = oDiv.setAttribute('data-name','new-value')
路由规则
可以在不刷新页面的前提下动态改变浏览器地址栏中的 URL 地址,动态修改页面上所显示资源。
window.history 的方法和属性
back()` `forward()` `go()
HTML5 新方法:添加和替换历史记录的条目
pushState()
history.pushState(state, title, url); 添加一条历史记录,不刷新页面
state
: 一个于指定网址相关的状态对象, popstate
事件触发时,该对象会传入回调函数中。如果不需要这个对象,此处可以填 null。
title
: 新页面的标题,但是所有浏览器目前都忽略这个值,因此这里可以填 null。
url
: 新的网址,必须与前页面处在同一个域。浏览器的地址栏将显示这个网址。
replaceState
history.replaceState(state, title, url); 替换当前的历史记录,不刷新页面
- 这两个 API 的相同之处是都会操作浏览器的历史记录,而不会引起页面的刷新。
- 不同之处在于,pushState 会增加一条新的历史记录,replaceState 则会替换当前的历史记录。
- 这两个 api,加上 state 改变触发的
popstate
事件,提供了单页应该的另一种路由方式。popstate
事件:历史记录发生改变时触发
基于 hash(location.hash+hashchange 事件)
我们知道 location.hash 的值就是 url 中 #
后面的内容 ,如 http://www.163.com#something
。
此网址中, location.hash='#something' 。
hash 满足以下几个特性,才使得其可以实现前端路由:
- url 中 hash 值的变化并不会重新加载页面,因为 hash 是用来指导浏览器行为的,对服务端是无用的,所以不会包括在 http 请求中。
- hash 值的改变,都会在浏览器的访问历史中增加一个记录 ,也就是能通过浏览器的回退、前进按钮控制 hash 的切换
- 我们可以通过 hashchange 事件,监听到 hash 值的变化 ,从而响应不同路径的逻辑处理。
window.addEventListener("hashchange", funcRef, false)
如此一来,我们就可以在 hashchange 事件里,根据 hash 值来更新对应的视图,但不会去重新请求页面,同时呢,也在 history 里增加了一条访问记录,用户也仍然可以通过前进后退键实现 UI 的切换。
触发 hash 值的变化有 2 种方法
- 一种是 通过 a 标签,设置 href 属性 ,当标签点击之后,地址栏会改变,同时会触发 hashchange 事件
<a href="#TianTianUp">to somewhere</a>
- 另一种是通过 js 直接赋值给 location.hash ,也会改变 url,触发 hashchange 事件 。
location.hash="#somewhere"
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: 如何实现扫码登录
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论