面试题-js
new关键字
new关键字的作用
通过new关键字实例化构造函数,获取对象
new在执行时会做的4件事
1在内存中创建一个新的空对象
2让this指向这个新对象(将构造函数的作用域赋给新对象,就是给这个新对象构造原型链,链接到构造函数的原型对象上,从而新对象就可以访问构造函数中的属性和方法)
3执行构造函数里面的代码,给这个对象添加属性和方法
4返回这个新对象(所以构造函数里面不需要return)
为什么子类的构造函数,一定要调用super()
?
原因就在于 ES6 的继承机制,与 ES5 完全不同。
ES5 的继承机制,是先创造一个独立的子类的实例对象,然后再将父类的方法添加到这个对象上面,即“实例在前,继承在后”。
ES6 的继承机制,则是先将父类的属性和方法,加到一个空的对象上面,然后再将该对象作为子类的实例,即“继承在前,实例在后”。
这就是为什么 ES6 的继承必须先调用super()
方法,因为这一步会生成一个继承父类的this
对象,没有这一步就无法继承父类。
ES5的继承和ES6的继承有什么区别
ES5的继承:通过prototype或构造函数机制来实现。ES5的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this
function Father(uname,age) { this.uname = uname; this.age = age; } Father.prototype.money = function() { console.log(this); } function Son(uname, age, sex) { Father.call(this, uname, age); // 3 实现继承 this.sex = sex;; } // Father的实例对象中有__proto_ ,指向Father的原型对象prototype,Father的prototype中就有Father的方法 Son.prototype = new Father(); // 或者Object.create(person.prototype);
// 如果利用实例对象的形式修改了原型对象,要把constructor 指回构造函数
Son.prototype.constructor = Son;
Son.prototype.exam = function() {
console.log('孩子的方法');
}
ES6的继承:先创建父类的实例对象this(所以必须先调用父类的super()方法),然后再用子类的构造函数修改this。
ES6 规定,子类必须在constructor()
方法中调用super()
,否则就会报错。
这是因为子类自己的this
对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,添加子类自己的实例属性和方法。
如果不调用super()
方法,子类就得不到自己的this
对象。
为什么 JS 阻塞页面加载 ?
由于 JavaScript 是可操纵 DOM 的,如果在修改这些元素属性同时渲染界面(即 JavaScript 线程和 UI 线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。
因此为了防止渲染出现不可预期的结果,浏览器设置 GUI 渲染线程与 JavaScript 引擎为互斥的关系。
什么是单线程,和异步的关系?
”JS是单线程的”指的是JS 引擎线程。
在浏览器环境中,有JS 引擎线程和渲染线程,且两个线程互斥。
Node环境中,只有JS 线程。
单线程 :只有一个线程,只能做一件事
原因 : 避免 DOM 渲染的冲突
浏览器需要渲染 DOM
JS 可以修改 DOM 结构
JS 执行的时候,浏览器 DOM 渲染会暂停
两段 JS 也不能同时执行(都修改 DOM 就冲突了)
webworker 支持多线程,但是不能访问 DOM
解决方案 :异步
css 加载会造成阻塞吗 ?
由浏览器渲染流程我们可以看出 :
DOM 和 CSSOM 通常是并行构建的,所以 CSS 加载不会阻塞 DOM 的解析。
然而,由于 Render Tree 是依赖于 DOM Tree 和 CSSOM Tree 的,
所以他必须等待到 CSSOM Tree 构建完成,也就是 CSS 资源加载完成(或者 CSS 资源加载失败)后,才能开始渲染。
因此,CSS 加载会阻塞 Dom 的渲染。
DOMContentLoaded 与 load 的区别 ?
当 DOMContentLoaded 事件触发时,仅当 DOM 解析完成后,不包括样式表,图片。
当文档中没有脚本时,浏览器解析完文档便能触发 DOMContentLoaded 事件。
如果文档中包含脚本,则脚本会阻塞文档的解析,而脚本需要等 CSSOM 构建完成才能执行。
在任何情况下,DOMContentLoaded 的触发不需要等待图片等其他资源加载完成。
当 onload 事件触发时,页面上所有的 DOM,样式表,脚本,图片等资源已经加载完毕。
DOMContentLoaded -> load。
js 执行机制:事件循环(宏任务微任务)
JavaScript 语言的一大特点就是单线程,同一个时间只能做一件事。
为了协调事件、用户交互、脚本、UI 渲染和网络处理等行为,防止主线程的不阻塞,Event Loop 的方案应用而生
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。
所有任务分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)
在所有同步任务执行完之前,任何的异步任务是不会执行的。
例子:网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。
而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务
导图要表达的内容用文字来表述的话:
- 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。
- 当指定的事情完成时,Event Table会将这个函数移入Event Queue。
- 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
- 上述过程会不断重复,也就是常说的Event Loop(事件循环)。
我们不禁要问了,那怎么知道主线程执行栈为空啊?js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。
事件循环
事件循环的顺序,决定js代码的执行顺序。
进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。
然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。
- macro-task(宏任务):包括整体代码script,setTimeout,setInterval
- micro-task(微任务):Promise,process.nextTick
DOM 事件有哪些阶段?谈谈对事件代理的理解
分为三大阶段:捕获阶段--目标阶段--冒泡阶段
事件代理:事件不直接绑定到某元素上,而是绑定到该元素的父元素上,进行触发事件操作时(例如'click'),再通过条件判断,执行事件触发后的语句(例如'alert(e.target.innerHTML)')
好处:(1)使代码更简洁;(2)节省内存开销
构造函数
一般情况下,公共属性定义到this上,公共方法放到原型对象上
如果方法是放this上的,每生成一个实例就会开辟一个新的内存空间,造成浪费
原型,原型链
每一个构造函数都有一个prototype对象,就是原型对象。在prototype对象上定义方法和属性,所有的实例对象都可以共享,节约内存。
每个实例对象上都有一个__proto__属性,它指向父类构造函数的原型,这个原型的__proto__属性也指向父类的原型,以次类推,直到指向Object对象为止,这就形成了原型链
当访问实例对象上的属性和方法时,就是通过原型链一层一层去查找的
(构造函数的)原型对象 prototype
每一个构造函数都有一个prototype属性,是一个对象,叫原型对象-prototype
作用
共享方法:把方法定义在prototype上,所有实例都可以共享这个方法
节约内存:不需要再开辟内存空间
对象(的)原型 __proto__
每个对象上都有一个属性__proto__ 指向构造函数的原型对象
(对象可以使用构造函数prototype原型对象上的属性和方法,就是因为对象有__proto__存在)
__proto__对象原型和原型对象prototype是等价的
constructor构造函数
对象原型(__proto__)和构造函数的原型对象(prototype)里面都有一个constructor属性,指回构造函数本身
constructor属性主要用来记录该对象引用于哪个构造函数
原型链
在JavaScript中万物都是对象,对象和对象之间的关系是通过prototype对象指向父类对象,直到指向Object对象为止,这样就形成了一个原型指向的链条,专业术语称之为原型链
js的成员查找机制
1 访问一个对象的属性或方法时,先查找对象自身有没有该属性
2 没有找到 就查找他的原型(也就是__proto__指向的prototype原型)
3 还是没有找到,就查找原型对象的原型(object的原型对象)
4 以此类推一直找到Object(null)为止
作用域,作用域链
1 作用域
作用域是在运行时代码中变量和对象的可访问性
作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。
ES5中作用域有:全局作用域、函数作用域。没有块作用域的概念。
ES6中新增了块级作用域。块作用域由 { } 包括,if语句和for语句里面的{ }也属于块作用域。
2 作用域链
一般情况下,变量取值到 创建 这个变量 的函数的作用域中取值。
但是如果在当前作用域中没有查到值,就会向上级作用域去查,直到查到全局作用域,这么一个查找过程形成的链条就叫做作用域链。
this指向
1 函数内this指向 由谁调用就指向谁
btn.onclick = function(){
this.disable = true; // 指向btn
setTimeout(function(){
this.disable = false; // 指向window
},1000)
}
btn.onclick = function(){
this.disable = true;
setTimeout(function(){
this.disable = false;
}.bind(this),1000) // 改变this指向 this指向btn
}
调用方式 | this指向 |
普通函数调用 | window (严格模式下undefined) |
构造函数调用 | 实例对象(原型对象里面的方法也指向实例对象-obj.prototype) |
对象方法调用 | 该方法所属对象 |
事件绑定方法 | 绑定事件对象 btn.onclick = function() {} |
定时器函数 | window |
立即执行函数 | window |
2 使用call() apply() bind()
相同点:改变this指向
不同点:
1) call,apply会调用函数,并且改变this指向
2)传参方式不同 call(指向, 参数1, 参数2) apply(指向,[参数1, 参数2])
3)bind不会调用函数,只改变this指向
应用场景
1)call经常做继承
2)apply经常和数组有关,
3)改变定时器内部的this指向
3 箭头函数-es6的
不绑定this关键字,箭头函数中的this,指向函数定义位置的上下文this,只会从自己的作用域链的上一层继承this。
bind,call,apply只能调用传递参数,不可修改this指向
箭头函数内部没有arguments,不能使用new实例化
function函数也是一个对象,箭头函数不是对象
// 箭头函数
var age = 1;
var obj = {
age: 2,
say: () => {
console.log(this.age); // 对象不能产生作用域 指向window
}
}
obj.say(); // 1
// 普通函数
var age = 1;
var obj = {
age: 2,
say: function() {
console.log(this.age); // 指向调用者
}
}
obj.say(); // 2
var obj = {
age: 2,
init: function () {
document.addEventListener('click', () => {
this.say(); // 箭头函数 向上一级作用域查找 2
})
// 相当于
document.addEventListener('click', function () {
this.say(); // 2
}.bind(this));
//
document.addEventListener('click', function () {
this.say(); // this指向document 报错 say is not a function
})
},
say: function () {
console.log(this.age); // 指向调用者
}
}
obj.init();
高价函数
高阶函数是至少满足下列一个条件的:
- 接受一个或多个函数作为输入
- 输出一个函数
数组自带的map、filter和reduce就是高阶函数,因为它们接受一个函数作为参数
什么是闭包?闭包作用?优缺点
闭包(closure)指有权访问另一个函数作用域中的变量的函数
function fn() { // fn就是闭包函数 var num = 10; return function () { console.log(num) } } var f = fn(); f()
作用:延伸变量的作用范围
优点:
- 避免全局变量的污染
- 可以读取函数内部的变量
缺点:
1 导致变量不会被垃圾回收机制回收,造成内存消耗
2 常驻内存,增加内存使用量
案例1:利用闭包的方式得到当前小li的索引号
var lis = document.querySelector('.nav').querySelectorAll('li') for(var i = 0; i< lis.length; i++) { // 利用for循环创建4个立即执行函数 ( function(i) { lis[i].onclick = function () { // 点击事件是异步的 console.log(i); // 0 1 2 3 } } )(i); }
不用闭包:动态添加属性
var lis = document.querySelector('.nav').querySelectorAll('li') for(var i = 0; i< lis.length; i++) { lis[i].i = i lis[i].onclick = function() { console.log(this.i) } }
案例2:利用闭包 3秒后打印所有li元素的内容
var lis = document.querySelector('.nav').querySelectorAll('li') for(var i = 0; i< lis.length; i++) { // 利用for循环创建4个立即执行函数 ( function(i) { setTimeout(() => { console.log(lis[i].innerHtml); }, 3000); } )(i); }
javascript 的垃圾回收机制
JS规定在一个函数作用域内,程序执行完以后变量就会被销毁,这样可节省内存
(C 语言,低级内存管理,需显式地对操作系统的内存进行分配和释放)
JavaScript 在创建对象(对象、字符串等)时会为它们分配内存,不再使用对时会“自动”释放内存,这个过程称为垃圾回收。
内存生命周期中的每一个阶段:
分配内存 — 内存是由操作系统分配的,它允许您的程序使用它。
在低级语言(例如 C 语言)中,这是一个开发人员需要自己处理的显式执行的操作。然而,在高级语言中,系统会自动为你分配内在。
使用内存 — 这是程序实际使用之前分配的内存,在代码中使用分配的变量时,就会发生读和写操作。
释放内存 — 释放所有不再使用的内存,使之成为自由内存,并可以被重利用。
与分配内存操作一样,这一操作在低级语言中也是需要显式地执行。
四种常见的内存泄漏:全局变量,未清除的定时器,闭包,以及 dom 的引用
- 全局变量 不用 var 声明的变量,相当于挂载到 window 对象上。如:b=1; 解决:使用严格模式
- 被遗忘的定时器和回调函数
- 闭包
- 没有清理的 DOM 元素引用
前端性能优化
减少 HTTP 请求数
减少 DNS 查询
使用 CDN
避免重定向
图片懒加载
减少 DOM 元素数量
减少 DOM 操作
使用外部 JavaScript 和 CSS
压缩 JavaScript、CSS、字体、图片等
优化 CSS Sprite
使用 iconfont
多域名分发划分内容到不同域名
尽量减少 iframe 使用
避免图片 src 为空
把样式表放在 link 中
把 JavaScript 放在页面底部
减少请求数量
减小资源大小
优化网络连接
优化资源加载
减少重绘回流
使用性能更好的API
构建优化
负载均衡
单台服务器共同协作,不让其中某一台或几台超额工作,发挥服务器的最大作用
http 重定向负载均衡:调度者根据策略选择服务器以 302 响应请求,缺点只有第一次有效果,后续操作维持在该服务器 dns 负载均衡:解析域名时,访问多个 ip 服务器中的一个(可监控性较弱)原因 - 避免 DOM 渲染的冲突
反向代理负载均衡:访问统一的服务器,由服务器进行调度访问实际的某个服务器,对统一的服务器要求大,性能受到 服务器群的数量
函数柯里化
函数柯里化其实就是在函数调用时只传递一部分参数进行调用,函数会返回一个新函数去处理剩下的参数
//函数柯里化 function fn(x) { return function (y) { console.log(x + y); }; }; var fn_ = fn(1); fn_(1); //2 fn(1)(1) //2
作用:
参数复用
利用闭包的原理,让我们前面传输过来的参数不要被释放掉
提前确认
这一特性经常是用来对浏览器的兼容性做出一些判断并初始化api,比如说我们目前用来监听事件大部分情况是使用addEventListener来实现的,但是一些较久的浏览器并不支持该方法,所以在使用之前,我们可以先做一次判断,之后便可以省略这个步骤了
延迟运行
js中的bind这个方法,用到的就是柯里化的这个特征
案例
function add() { // 第一次执行时,定义一个数组专门用来存储所有的参数 var _args = Array.prototype.slice.call(arguments); // 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值 var _adder = function() { _args.push(...arguments); return _adder; }; // 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回 _adder.toString = function () { return _args.reduce(function (a, b) { return a + b; }); } return _adder; } add(1)(2)(3) // 6 add(1, 2, 3)(4) // 10 add(1)(2)(3)(4)(5) // 15 add(2, 6)(1) // 9
异步编程的实现方式?
回调函数
优点:简单、容易理解
缺点:不利于维护、代码耦合高
事件监听
优点:容易理解,可以绑定多个事件,每个事件可以指定多个回调函数
缺点:事件驱动型,流程不够清晰
发布/订阅(观察者模式)
类似于事件监听,但是可以通过‘消息中心’,了解现在有多少发布者,多少订阅者
Promise 对象
优点:可以利用 then 方法,进行链式写法;可以书写错误时的回调函数
缺点:编写和理解,相对比较难
Generator 函数
优点:函数体内外的数据交换、错误处理机制
缺点:流程管理不方便
async 函数
优点:内置执行器、更好的语义、更广的适用性、返回的是 Promise、结构清晰
缺点:错误处理机制