该面试题只是为了记录我自己的面试笔记,大多数摘自行内有关大佬总结,本人只是搬运工,有关链接已放置相关笔记的下面
JS
获取一个页面的所有元素标签数量
// document.querySelectorAll() 是 HTML5中引入的新方法,返回文档中匹配的CSS选择器的所有元素节点列表
let dom = [...document.querySelectorAll("*")].map(n=>n.tagName).reduce((pre, cur)=>{
pre[cur] = (pre[cur] || 0) + 1;
return pre;
}, {})
获取所有元素标签
let domArr = new Set([...document.querySelectorAll("*")].map(n=>n.tagName))
Maps 和 Objects 的区别
Map | Object | |
意外的键 | Map默认情况不包含任何键。只包含显式插入的键。 | 一个 Object 有一个原型,原型链上的键名有可能和你自己在对象上的设置的键名产生冲突。备注:虽然从 ES5 开始可以用 Object.create(null) 来创建一个没有原型的对象,但是这种用法不太常见。 |
键的类型 | 一个Map的键可以是任意值,包括函数、对象或任意基本类型。 | 一个Object的键必须是一个String或是Symbol |
键的顺序 | Map 中的键是有序的。因此,当迭代的时候,一个 Map 对象以插入的顺序返回键值。 | 虽然 Object 的键目前是有序的,但并不总是这样,而且这个顺序是复杂的。 |
Size | Map的键值对个数可以轻易地通过Map.prototype.size属性获取。 | Object的键值对个数只能手动计算. |
迭代 | Map是可迭代的,所以可以直接被迭代。 | Object 没有实现 迭代协议,所以使用 JavaSctipt 的 for...of 表达式并不能直接迭代对象。备注:1、对象可以实现迭代协议,或者你可以使用Object.keys或Object.entries2、for...in表达式允许你迭代一个对象的可枚举属性。 |
性能 | 在频繁增删键值对的场景下表现更好。 | 在频繁添加和删除键值对的场景下未作出优化。 |
序列化和解析 | 没有元素的序列化和解析的支持。 | 原生的由 Object 到 JSON 的序列化支持,使用 JSON.stringify()。原生的由 JSON 到 Object 的解析支持,使用 JSON.parse()。 |
for-in 仅包含了以字符串为键的属性;Object.keys 仅包含了对象自身的、可枚举的、以字符串为键的属性;Object.getOwnPropertyNames 包含了所有以字符串为键的属性,即使是不可枚举的;Object.getOwnPropertySymbols 与前者类似,但其包含的是以 Symbol 为键的属性,等等。
javascript如何做seo优化_前台javascript速度优化总结
两个基本点
1. 择重避轻,有所取舍。
l 核心优先
通常来讲,系统是都是庞大的,不要太完美主义,先抓住重点,理解那些是我们的核心页面,那些页面对我们来说是最重要的,那些页面访问量最高,核心优先。
l 主要问题在那,抓住瓶颈点。
治病要医本。优化前,需要进行细致的分析,抓住主要瓶颈点,对症下药。优化那么多的方子,别全采用,通常几个就能达到效果。
2. 简单有效才是硬道理
越是简单的东西越容易控制,越不容易出错,尽量避免将系统设计的过于庞大,过于复杂,记住,这是在做产品,而不是在搞研发。很多看似很蠢的方法,往往越是有效。
新技术,新方法的引用是具备一定的风险的,要评估,要慎重。
Js处理
1. 尽量放到页面尾部
Js的加载时阻塞页面的,没下载完毕后面的内容不会出来,所以尽量避免把JS放到页面头部,按照经验估计,整个页面中所用的JS逻辑,90%都是可以放到页面尾部。
2. 延迟加载(按需加载)
很多的业务逻辑并非每次都使用也不是要立即使用,首次加载过程中仅仅加载那些必须的,只有当必要的条件触发,才去加载请求必要的JS.比如说权限验证通过,加载管理模块。点击发表文章按钮,加载与发表文章有关的验证和处理函数。
如果写过C++的肯定会接触过动态库和静态库,这个与之类似,什么时候需要什么时候再加载,首次打开页面肯定会清净了许多,而且业务逻辑也由此分离开来,管理和维护也会方便很多,毕竟减少了那么多的耦合。
按照BBS项目经验估计,普通页面的所有业务逻辑中需要在首次请求中加载的不到50%,我们的JS又由此砍掉了一半。
3. 合并JS,减少请求
请求多个小文件的效率远小于请求一个大文件的效率,因为需要多次DNS解析,多次连接,浏览器和server端也需要进行多次开启进程、权限验证和预处理,以及 http请求在数据包传递上的一些问题。
所以尽量避免在页面中加载一堆的js 文件,需要先讲需要的小的JS合并成一个大的JS文件统一输出,页面因此被卡住的时间肯定会减少很多。
为了提高开发效率,合并建议不要每次都手动来进行,导致之后维护成本很大,相信些个XML配置文件,确定合并规则以及依赖关系后,用程序自动合并效率会高很多,后面有我附上的一个配置示例,仅作参考形式不重要
4. JS压缩
此手段属前端特有,毕竟流量意味着速度,意味着金钱。是在降低代码的可读性为前提。但事物的两面性告诉我们,可读性差也意味着安全,而且可读性可以通过保存压缩前的源文件来解决。
所谓的压缩,就是把场的变量名换成短的变量名,去掉没用的空格和换行符,从而节省我们JS程序的长度,不过目前这种处理已经很成熟,通过搜索可以搜出很多相关的工具。不再细说
经验值,能压缩50%以上,视程序与压缩工具而论。
5. 尽量少用第三方库
在我的印象中,很多框架都是很庞大20K以上,虽然很强大很方便,但如果不是做企业级应用,不要用,因为我们也许只可能用到其中很少的一部分功能却加载了整个框架。
不过框架中的很多方法是可以提取出来滴,或者精简成一个轻量级的框架,比如说trimPath,完全可以精简到4k.
6. 合并ajax请求
Ajax请求的数据,如果涉及请求多种数据,尽量考虑到将其合并。
7. 合理的使用缓存
缓存视乎是server端的事,但是js中也是经常用的。
一种是缓存在一个全局变量中,一些很复杂的计算和查找操作可以这样做。如果大家在使用模板类trimPath经常是需要对模板进行预处理,这种预处理的结果是可以被缓存的。这种缓存的缺点是页面刷新后数据就会失效。
另外一种是缓存在window.name或cookie里面,经常用来缓存一些AJAX调用的结果,避免反复请求server端,比如一些用户的权限验证信息,就没必要总是调用server端接口,缓存了也就减少了请求,提高了性能,但cookie大家要慎用,存于一些数据比较小的还行,每次http请求他是占用上行带宽的。
还有一种缓存的实现是借助于flash或其他的第三方组件,特点是可以缓存超大的数据,但是适应场景优先,需要特殊的平台支持,不过FLASH目前已经很通用了。
8. 能静态化输出,尽量少用JS渲染输出
页面制作
1. 素材合并
尽量把页面中的图片合并在一起,利用css sprite切割。这样减少了请求的次数。通常合并成3长大的图片,一张是有固定宽高的(比如说按钮),另外两张是分别横向或纵向平铺的1像素的小图,用来做背景用。
2. CSS压缩处理
道理同JS压缩,也是有很多工具实用的可用。
3. 图片背景切割与平铺
切图是很有讲究的,很多区域能切成用1像素平铺,尽量用1像素小图平铺,尽量用一个较大的图片设置成背静。
4. 少用iframe和frameset
首先一点frame会阻塞页面,第二,产生额外的请求,第三,如果涉及交互,增加开发维护成本,第四对搜索引擎优化不好
5. CSS尽量放到页面头部
浏览器只有等CSS下载完毕后,才会真正的显示页面,所以为了让页面尽快有所输出,把CSS放到头部,而且浏览器对CSS的处理时并行的,不会像JS那样会阻塞页面。
Server处理
- 启用gzip压缩,约能压缩70%~80%
2. js,css,图片添加过期头,让浏览器能缓存。能减少1/3以上的请求。
3. 静态页面、js、css等静态文件单独迁移
第一, 可以针对静态文件做专门优化,比如说squid反向代理,nginx代替apache做静态server。
第二, 便于管理和维护,以后迁移和拓展方便。
4. js、css、图片等静态文件与当前应用放到不同的域名下。
不再传递那些无必要的cookie,减少传输。
5. 图片服务器分多域名。
浏览器对同一域名的只允许使用2个并发,如果页面图片过多,会由于并发排队从而阻塞页面。但域名也不能太多,会消耗DNS解析的时间,建议4个为佳。
附加
1. JS合并配置文件示例
2. 常见分析与调试工具
IE Httpwatch
Firefox firebug
参考链接:cloud.tencent.com/developer/n…
Js 同步异步
同步任务:浏览器按顺序执行的任务
异步任务:
JavaScript中的异步机制可以分为以下几种:
- 回调函数 的方式,使用回调函数的方式有一个缺点是,多个回调函数嵌套的时候会造成回调函数地狱,上下两层的回调函数间的代码耦合度太高,不利于代码的可维护。
- Promise 的方式,使用 Promise 的方式可以将嵌套的回调函数作为链式调用。但是使用这种方法,有时会造成多个 then 的链式调用,可能会造成代码的语义不够明确。
- generator 的方式,它可以在函数的执行过程中,将函数的执行权转移出去,在函数外部还可以将执行权转移回来。当遇到异步函数执行的时候,将函数执行权转移出去,当异步函数执行完毕时再将执行权给转移回来。因此在 generator 内部对于异步操作的方式,可以以同步的顺序来书写。使用这种方式需要考虑的问题是何时将函数的控制权转移回来,因此需要有一个自动执行 generator 的机制,比如说 co 模块等方式来实现 generator 的自动执行。
- async 函数 的方式,async 函数是 generator 和 promise 实现的一个自动执行的语法糖,它内部自带执行器,当函数内部执行到一个 await 语句的时候,如果语句返回一个 promise 对象,那么函数将会等待 promise 对象的状态变为 resolve 后再继续向下执行。因此可以将异步逻辑,转化为同步的顺序来书写,并且这个函数可以自动执行。
虚拟节点
从本质上来说,Virtual Dom是一个JavaScript对象,通过对象的方式来表示DOM结构。将页面的状态抽象为JS对象的形式,配合不同的渲染工具,使跨平台渲染成为可能。通过事务处理机制,将多次DOM修改的结果一次性的更新到页面上,从而有效的减少页面渲染的次数,减少修改DOM的重绘重排次数,提高渲染性能。
虚拟DOM是对DOM的抽象,这个对象是更加轻量级的对 DOM的描述。它设计的最初目的,就是更好的跨平台,比如Node.js就没有DOM,如果想实现SSR,那么一个方式就是借助虚拟DOM,因为虚拟DOM本身是js对象。 在代码渲染到页面之前,vue会把代码转换成一个对象(虚拟 DOM)。以对象的形式来描述真实DOM结构,最终渲染到页面。在每次数据发生变化前,虚拟DOM都会缓存一份,变化之时,现在的虚拟DOM会与缓存的虚拟DOM进行比较。在vue内部封装了diff算法,通过这个算法来进行比较,渲染时修改改变的变化,原先没有发生改变的通过原先的数据进行渲染。
另外现代前端框架的一个基本要求就是无须手动操作DOM,一方面是因为手动操作DOM无法保证程序性能,多人协作的项目中如果review不严格,可能会有开发者写出性能较低的代码,另一方面更重要的是省略手动DOM操作可以大大提高开发效率。
JSBridge的原理
-
JavaScript 是运行在一个单独的 JS Context 中(例如,WebView 的 Webkit 引擎、JSCore)。由于这些 Context 与原生运行环境的天然隔离,我们可以将这种情况与 RPC(Remote Procedure Call,远程过程调用)通信进行类比,将 Native 与 JavaScript 的每次互相调用看做一次 RPC 调用。
- 在 JSBridge 的设计中,可以把前端看做 RPC 的客户端,把 Native 端看做 RPC 的服务器端,从而 JSBridge 要实现的主要逻辑就出现了:通信调用(Native 与 JS 通信) 和 句柄解析调用。(如果你是个前端,而且并不熟悉 RPC 的话,你也可以把这个流程类比成 JSONP 的流程)
-
JavaScript 调用 Native 的方式,主要有两种:注入 API 和 拦截 URL SCHEME。
-
注入API
- 注入 API 方式的主要原理是,通过 WebView 提供的接口,向 JavaScript 的 Context(window)中注入对象或者方法,让 JavaScript 调用时,直接执行相应的 Native 代码逻辑,达到 JavaScript 调用 Native 的目的。
-
拦截 URL SCHEME
- 拦截 URL SCHEME 的主要流程是:Web 端通过某种方式(例如 iframe.src)发送 URL Scheme 请求,之后 Native 拦截到请求并根据 URL SCHEME(包括所带的参数)进行相关操作。
-
参考链接:JSBridge的原理 - 掘金
TS
TS类,接口
类是面向对象程序设计(OOP,Object-Oriented Programming)实现信息封装的基础class xxx
面试官:说说你对 TypeScript 中类的理解?应用场景? | web前端面试 - 面试官系列
接口是一系列抽象方法的声明,是一些方法特征的集合,这些方法都应该是抽象的,需要由具体的类去实现,然后第三方就可以通过这组抽象方法调用,让具体的类执行具体的方法 interface xxxx
面试官:说说你对 TypeScript 中接口的理解?应用场景? | web前端面试 - 面试官系列
any,unknown,never
any和unkonwn在TS类型中属于最顶层的Top Type,即所有的类型都是它俩的子类型。
而never则相反,它作为Bottom Type是所有类型的子类型
Ts 箭头函数与 es6 的箭头函数区别,Ts this 指向和 js this指向
- ts this指向class只有new实例才会实例
- js 常规this常规指向当前上下文,js中箭头函数this和ts中this是一致的
ECMAScript
var,let,const
- var定义的变量,没有块的概念,可以跨块访问, 不能跨函数访问。
- let定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问。
- const用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,而且不能修改。
- const仅保证指针不发生改变,修改对象的属性不会改变对象的指针,所以是被允许的。也就是说const定义的引用类型只要指针不发生改变,其他的不论如何改变都是允许的。
箭头函数和普通函数的区别
-
箭头函数比普通函数更加简洁
- 如果没有参数,就直接写一个空括号即可
- 如果只有一个参数,可以省去参数的括号
- 如果有多个参数,用逗号分割
- 如果函数体的返回值只有一句,可以省略大括号
- 如果函数体不需要返回值,且只有一句话,可以给这个语句前面加一个void关键字。最常见的就是调用一个函数:
let fn = () => void doesNotReturn();
- 箭头函数没有自己的this
箭头函数不会创建自己的this, 所以它没有自己的this,它只会在自己作用域的上一层继承this。所以箭头函数中this的指向在它在定义时已经确定了,之后不会改变。
- 箭头函数继承来的this指向永远不会改变
var id = 'GLOBAL';
var obj = {
id: 'OBJ',
a: function(){
console.log(this.id);
},
b: () => {
console.log(this.id);
}
};
obj.a(); // 'OBJ'
obj.b(); // 'GLOBAL'
new obj.a() // undefined
new obj.b() // Uncaught TypeError: obj.b is not a constructor
对象obj的方法b是使用箭头函数定义的,这个函数中的this就永远指向它定义时所处的全局执行环境中的this,即便这个函数是作为对象obj的方法调用,this依旧指向Window对象。需要注意,定义对象的大括号{}是无法形成一个单独的执行环境的,它依旧是处于全局执行环境中。
- call()、apply()、bind()等方法不能改变箭头函数中this的指向
var id = 'Global';
let fun1 = () => {
console.log(this.id)
};
fun1(); // 'Global'
fun1.call({id: 'Obj'}); // 'Global'
fun1.apply({id: 'Obj'}); // 'Global'
fun1.bind({id: 'Obj'})(); // 'Global'
- 箭头函数不能作为构造函数使用
构造函数在new的步骤在上面已经说过了,实际上第二步就是将函数中的this指向该对象。 但是由于箭头函数时没有自己的this的,且this指向外层的执行环境,且不能改变指向,所以不能当做构造函数使用。
- 箭头函数没有自己的arguments箭头函数没有自己的arguments对象。在箭头函数中访问arguments实际上获得的是它外层函数的arguments值。
- 箭头函数没有prototype
- 箭头函数不能用作Generator函数,不能使用yeild关键字
ES5 和 ES6 的继承
ES6 与 ES5 中的继承有 2 个区别,第一个是,ES6 中子类会继承父类的属性,第二个区别是,super() 与 A.call(this) 是不同的,在继承原生构造函数的情况下,体现得很明显,ES6 中的子类实例可以继承原生构造函数实例的内部属性,而在 ES5 中做不到。
ES5 继承
- 原型链继承(父类的实例赋值给子类)
以原型链的方式来实现继承,但是这种实现方式存在的缺点是,在包含有引用类型的数据时,会被所有的实例对象所共享,容易造成修改的混乱。还有就是在创建子类型的时候不能向超类型传递参数。
function Parent() {
this.name = 'parent';
this.play = [1, 2, 3]
}
function Child() {
this.type = 'child';
}
Child.prototype = new Parent();
console.log(new Child())
- 构造函数继承
借用构造函数的方式,这种方式是通过在子类型的函数中调用超类型的构造函数来实现的,这一种方法解决了不能向超类型传递参数的缺点,但是它存在的一个问题就是无法实现函数方法的复用,并且超类型原型定义的方法子类型也没有办法访问到。
function Parent(){
this.name = 'parent1';
}
Parent.prototype.getName = function () {
return this.name;
}
function Child1(){
Parent1.call(this);
this.type = 'child1'
}
let child1 = new Child1();
console.log(child1); // 没问题
console.log(child1.getName()); // 会报错
- 组合继承
这种方式看起来就没什么问题,方式一和方式二的问题都解决了,但是从上面代码我们也可以看到
Parent3
执行了两次,造成了多构造一次的性能开销
function Parent3 () {
this.name = 'parent3';
this.play = [1, 2, 3];
}
Parent3.prototype.getName = function () {
return this.name;
}
function Child3() {
// 第二次调用 Parent3()
Parent3.call(this);
this.type = 'child3';
}
// 第一次调用 Parent3()
Child3.prototype = new Parent3();
// 手动挂上构造器,指向自己的构造函数
Child3.prototype.constructor = Child3;
var s3 = new Child3();
var s4 = new Child3();
s3.play.push(4);
console.log(s3.play, s4.play); // 不互相影响
console.log(s3.getName()); // 正常输出'parent3'
console.log(s4.getName()); // 正常输出'parent3'
- 原型式继承(Object.create)
这种继承方式的缺点也很明显,因为
Object.create
方法实现的是浅拷贝,多个实例的引用类型属性指向相同的内存,存在篡改的可能
let parent4 = {
name: "parent4",
friends: ["p1", "p2", "p3"],
getName: function() {
return this.name;
}
};
let person4 = Object.create(parent4);
person4.name = "tom";
person4.friends.push("jerry");
let person5 = Object.create(parent4);
person5.friends.push("lucy");
console.log(person4.name); // tom
console.log(person4.name === person4.getName()); // true
console.log(person5.name); // parent4
console.log(person4.friends); // ["p1", "p2", "p3","jerry","lucy"]
console.log(person5.friends); // ["p1", "p2", "p3","jerry","lucy"]
- 寄生式继承
这种继承方式的缺点也很明显,因为
Object.create
方法实现的是浅拷贝,多个实例的引用类型属性指向相同的内存,存在篡改的可能
let parent5 = {
name: "parent5",
friends: ["p1", "p2", "p3"],
getName: function() {
return this.name;
}
};
function clone(original) {
let clone = Object.create(original);
clone.getFriends = function() {
return this.friends;
};
return clone;
}
let person5 = clone(parent5);
console.log(person5.getName()); // parent5
console.log(person5.getFriends()); // ["p1", "p2", "p3"]
- 寄生组合式继承
function clone (parent, child) {
// 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
child.prototype = Object.create(parent.prototype);
child.prototype.constructor = child;
}
function Parent6() {
this.name = 'parent6';
this.play = [1, 2, 3];
}
Parent6.prototype.getName = function () {
return this.name;
}
function Child6() {
Parent6.call(this);
this.friends = 'child5';
}
clone(Parent6, Child6);
Child6.prototype.getFriends = function () {
return this.friends;
}
let person6 = new Child6();
console.log(person6); //{friends:"child5",name:"child5",play:[1,2,3],__proto__:Parent6}
console.log(person6.getName()); // parent6
console.log(person6.getFriends()); // child5
ES6 继承
class Parent {
constructor(name) {
this.name = name
}
eat() {
console.log(this.name + ' is eating')
}
}
class Child extends Parent {
constructor(name, age) {
super(name)
this.age = age
}
}
// 测试
let xm = new Child('xiaoming', 12)
console.log(xm.name) // xiaoming
console.log(xm.age) // 12
xm.eat() // xiaoming is eating