JavaScript 前端面试题
常见的浏览器内核有哪些 ?
Trident 内核:IE, 360,搜狗浏览器 MaxThon、TT、The World,等。[又称 MSHTML] Gecko 内核:火狐,FF,MozillaSuite / SeaMonkey 等 Presto 内核:Opera7 及以上。[Opera 内核原为:Presto,现为:Blink] Webkit 内核:Safari,Chrome 等。 [ Chrome 的:Blink(WebKit 的分支)]
介绍一下你对浏览器内核的理解 ?
内核主要分成两部分:渲染引擎(layout engineer 或 Rendering Engine) 和 JS 引擎。
渲染引擎
负责取得网页的内容(HTML、XML、图像等等)、整理讯息(例如加入 CSS 等),以及计算网页的显示方式,然后会输出至显示器或打印机。 浏览器的内核的不同对于网页的语法解释会有不同,所以渲染的效果也不相同。 所有网页浏览器、电子邮件客户端以及其它需要编辑、显示网络内容的应用程序都需要内核。
JS 引擎
解析和执行 javascript 来实现网页的动态效果。
最开始渲染引擎和 JS 引擎并没有区分的很明确,后来 JS 引擎越来越独立,内核就倾向于只指渲染引擎。
哪些常见操作会造成内存泄漏 ?
内存泄漏指任何对象在您不再拥有或需要它之后仍然存在。
垃圾回收器定期扫描对象,并计算引用了每个对象的其他对象的数量。如果一个对象的引用数量为 0(没有其他对象引用过该对象),或对该对象的惟一引用是循环的,那么该对象的内存即可回收。
- setTimeout 的第一个参数使用字符串而非函数的话,会引发内存泄漏。
- 闭包、控制台日志、循环(在两个对象彼此引用且彼此保留时,就会产生一个循环)
线程与进程的区别 ?
- 一个程序至少有一个进程,一个进程至少有一个线程。
- 线程的划分尺度小于进程,使得多线程程序的并发性高。
- 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
线程在执行过程中与进程还是有区别的。
- 每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
- 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。
- 但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。
eval() 函数有什么用 ?
eval() 函数可计算某个字符串,并执行其中的的 JavaScript 代码。
函数柯里化
实现一个 add 方法,使计算结果能够满足如下预期:
- add(1)(2)(3) = 6;
- add(1, 2, 3)(4) = 10;
- add(1)(2)(3)(4)(5) = 15;
function add() {
let result = 0;
const addFn = function () {
return (
(result += Array.prototype.slice
.call(arguments)
.reduce((a, b) => a + b)) && addFn
);
};
addFn.toString = () => result;
return addFn(...arguments);
}
console.log(add(1)(2)(3)); // 6
console.log(add(1, 2, 3)(4)); // 10
console.log(add(1)(2)(3)(4)(5)); // 15
Currying 有哪些好处呢
参数复用
// 正常正则验证字符串 reg.test(txt)
// 函数封装后
function check(reg, txt) {
return reg.test(txt);
}
check(/\d+/g, "test"); //false
check(/[a-z]+/g, "test"); //true
// Currying后
function curryingCheck(reg) {
return function (txt) {
return reg.test(txt);
};
}
var hasNumber = curryingCheck(/\d+/g);
var hasLetter = curryingCheck(/[a-z]+/g);
hasNumber("test1"); // true
hasNumber("testtest"); // false
hasLetter("21212"); // false
正常来说直接调用 check 函数就可以了,但是如果我有很多地方都要校验是否有数字,其实就是需要将第一个参数 reg 进行复用,这样别的地方就能够直接调用 hasNumber,hasLetter 等函数,让参数能够复用,调用起来也更方便。
斐波那契数列
斐波那契数列前两项都是 1,从第 3 项开始,每一项都等于前两项之和,斐波纳契数列以如下被以递推的方法定义:
F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n>=3,n∈N*)
const fibonacci = (n) => {
let a = 1,
b = 1,
c = 2;
if (n < 2) {
return c;
}
for (let i = 2; i < n; i++) {
[a, b, c] = [b, c, a + b];
}
return c;
};
fibonacci(20); // 6765, [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765]
mouseenter 和 mouseover 的区别
不论鼠标指针穿过被选元素或其子元素,都会触发 mouseover 事件,对应 mouseout。 只有在鼠标指针穿过被选元素时,才会触发 mouseenter 事件,对应 mouseleave。
js 字符串两边截取空白的 trim 的原型方法的实现
// 删除左右两端的空格
function trim(str) {
return str.replace(/(^\s*)|(\s*$)/g, "");
}
下面的输出结果是?
var out = 25,
inner = {
out: 20,
func: function () {
var out = 30;
return this.out;
},
};
console.log((inner.func, inner.func)());
console.log(inner.func());
console.log(inner.func());
console.log((inner.func = inner.func)());
结果:25,20,20,25
代码解析:这道题的考点分两个
- 作用域
- 运算符(赋值预算,逗号运算)
先看第一个输出:25,因为 ( inner.func, inner.func ) 是进行逗号运算符,逗号运算符就是运算前面的 ”,“ 返回最后一个,举个栗子
var i = 0,
j = 1,
k = 2;
console.log((i++, j++, k)); // 返回的是 k 的值 2 ,如果写成 k++ 的话 这里返回的就是 3
console.log(i); // 1
console.log(j); // 2
console.log(k); // 2
回到原题 ( inner.func, inner.func )
就是返回 inner.func
,而 inner.func
只是一个匿名函数
function () {
var out = 30;
return this.out;
}
而且这个匿名函数是属于 window
的,则变成了
(function () {
var out = 30;
return this.out;
})();
此刻的 this => window
所以 out 是 25。
第二和第三个 console.log 的作用域都是 inner,也就是他们执行的其实是 inner.func()
;
inner 作用域中是有 out 变量的,所以结果是 20。
第四个 console.log 考查的是一个等号运算 inner.func = inner.func
,其实返回的是运算的结果,
举个栗子
var a = 2,
b = 3;
console.log((a = b)); // 输出的是 3
所以 inner.func = inner.func
返回的也是一个匿名函数
function () {
var out = 30;
return this.out;
}
此刻,道理就和第一个 console.log 一样了,输出的结果是 25。
下面程序输出的结果是 ?
if (!("a" in window)) {
// false, ("a" in window) === true, 因为 var a = 1; 变量提升;
var a = 1;
}
alert(a);
实际结果是 “undefined”,代码等价于:
var a;
if (!("a" in window)) {
a = 1;
}
alert(a);
下面程序输出的结果是 ?
function a(x) {
return x * 2;
}
var a;
alert(a);
尽管变量声明在下面定义,但是变量 value 依然是 function,也就是说这种情况下,函数声明的优先级高于变量声明的优先级
// 函数声明会提升,函数声明的优先级高于变量声明的优先级
function a(x) {
return x * 2;
}
// 变量声明,但没有赋值
var a;
下面程序输出的结果是 ? arguments
function b(i, j, k) {
arguments[2] = 10;
alert(k); // 10
}
b(1, 2, 3);
const i = 1,
j = 2,
k = 3;
const c = [i, j, k];
c[2] = 10;
console.log(k); // 3
数组 c[2]
和 变量k
, 指向不同的内存空间,c = [i,j,k]
, 向当于把 k 的值赋给了 c[2]
所指向的的内存空间,在函数中,arguments[2] 和 参数 k,指向同一内存空间
JavaScript 6 种基本数据类型
一、数据类型
undefiend 没有定义数据类型 number 数值数据类型,例如 10 或者 1 或者 5.5,包括 NaN string 字符串数据类型用来描述文本,例如 "你的姓名" boolean 布尔类型 true | false ,不是正就是反 object 对象类型,复杂的一组描述信息的集合,包括 null function 函数类型
异步过程的构成要素有哪些?和异步过程是怎样的 ?
总结一下,一个异步过程通常是这样的:
主线程发起一个异步请求,相应的工作线程接收请求并告知主线程已收到(异步函数返回); 主线程可以继续执行后面的代码,同时工作线程执行异步任务; 工作线程完成工作后,通知主线程; 主线程收到通知后,执行一定的动作(调用回调函数)。
编写一个方法,求一个字符串的字节长度
假设:一个英文字符占用一个字节,一个中文字符占用两个字节
function getBytes(str) {
var len = str.length;
var bytes = len;
for (var i = 0; i < len; i++) {
if (str.charCodeAt(i) > 255) bytes++;
}
return bytes;
}
alert(getBytes("你好, as"));
JSON 的了解 ?
- JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。
- 它是基于 JavaScript 的一个子集。
- 数据格式简单,易于读写,占用带宽小。
- 格式:采用键值对。例如:{ “age‟: ‟12‟, ”name‟: ‟back‟ }
合并数组
如果你需要合并两个数组的话,可以使用 Array.concat(), 也可以用 Array.push.apply(arr1, arr2)
来代替创建新的数组,它可以把第二个数组合并到第一个中,从而较少内存消耗
var array1 = [1, 2, 3];
var array2 = [4, 5, 6];
console.log(array1.concat(array2)); // [1,2,3,4,5,6];
Array.push.apply(arr1, arr2);
把节点列表 (NodeList) 转换为数组
如果你运行 document.querySelectorAll("p") 方法,它可能会返回一个 DOM 元素的数组 — 节点列表对象。
但这个对象并不具有数组的全部方法,如 sort(),reduce(), map(),filter()。为了使用数组的那些方法,你需要把它转换为数组。
var elements = document.querySelectorAll("p"); // NodeList
var arrayElements = [].slice.call(elements); // 现在 NodeList 是一个数组
var arrayElements = Array.from(elements); // 这是另一种转换 NodeList 到 Array 的方法
说说堆和栈的区别 ?
堆栈空间分配区别
- 栈(操作系统):由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈;
- 堆(操作系统):一般由程序员分配释放, 若程序员不释放,程序结束时可能由 OS 回收,分配方式倒是类似于链表。
堆栈缓存方式区别
- 栈使用的是一级缓存, 他们通常都是被调用时处于存储空间中,调用完毕立即释放;
- 堆是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些。
堆栈数据结构区别
- 堆(数据结构):堆可以被看成是一棵树,如:堆排序;
- 栈(数据结构):一种先进后出的数据结构。
ES6 声明变量的六种方法
ES5 只有两种声明变量的方法:var 和 function 。 ES6 除了添加 let 和 const 命令。 还有两种声明变量的方法:import 命令和 class 命令。
手写 new
new 关键字会进行如下的操作:
- 创建一个空的简单 JavaScript 对象(即
{}
); - 为步骤 1 新创建的对象添加属性
__proto__
,将该属性链接至构造函数的原型对象 ; - 将步骤 1 新创建的对象作为 this 的上下文 ;
- 如果该函数没有返回对象,则返回 this。
function create(fn, ...arg) {
// 基于 fn 的原型创建一个新的对象
const obj = Object.create(fn.prototype);
// 将 fn 的 this 指向 obj, 并获取 fn 函数执行的结果, 作用是把 this.name = xx 绑定 obj 后,变为 obj.name = xx
const res = fn.apply(obj, arg);
// 当函数返回值类型为对象时,则返回该对象, 当函数返回值类型不为对象时,返回该构造函数的实例化对象
return typeof res === "object" ? res : obj;
}
手写 Promise
Promise 基本特征
- promise 有三个状态:pending, fulfilled, rejected
- new promise 时, 需要传递一个
executor()
执行器函数,执行器函数立即执行 - executor 接受两个参数,分别是 resolve 和 reject
- promise 的默认状态是 pending,promise 只能从 pending 到 rejected 或者 fulfilled,状态一旦确认,就不会再改变
- promise 必须有一个 then 方法,then 接收两个参数,成功的回调 onFulfilled, 和失败的回调 onRejected
- then 方法的执行结果也会返回一个 Promise 对象。因此我们可以进行 then 的链式执行,这也是解决回调地狱的主要方式。
- 如果调用 then 时,promise 已经成功,则执行 onFulfilled,参数是 promise 的 value
- 如果调用 then 时,promise 已经失败,那么执行 onRejected, 参数是 promise 的 reason
- 如果 then 中抛出了异常,那么就会把这个异常作为参数,传递给下一个 then 的失败的回调 onRejected
Promise 的使用方法:
function myAsyncFunction(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.onload = () => resolve(xhr.responseText);
xhr.onerror = () => reject(xhr.statusText);
xhr.send();
});
}
myAsyncFunction("/api").then(
(data) => {
console.log("responseText", data);
},
(error) => {
console.log("statusText", error);
}
);
Promise 实现:
class myPromise {
constructor(initfn) {
this.status = "pending";
this.value = undefined;
this.error = undefined;
this.onResolvedCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = (value) => {
this.status = "fulfilled";
this.value = value;
this.onResolvedCallbacks.forEach((cb) => cb(value));
};
const reject = (error) => {
this.status = "rejected";
this.error = error;
this.onRejectedCallbacks.forEach((cb) => cb(error));
};
try {
initfn(resolve, reject);
} catch (error) {
this.status = "rejected";
this.error = error;
this.onRejectedCallbacks.forEach((cb) => cb(error));
}
}
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === "function" ? onFulfilled : (v) => v;
onRejected =
typeof onRejected === "function"
? onRejected
: (err) => {
throw err;
};
let promise2 = new myPromise((resolved, rejected) => {
if (this.status === "pending") {
this.onResolvedCallbacks.push((value) => {
try {
resolved(onFulfilled(this.value));
} catch (error) {
this.error = error;
rejected(this.error);
}
});
this.onRejectedCallbacks.push((error) => {
rejected(onRejected(this.error));
});
}
if (this.status === "fulfilled") {
try {
resolved(onFulfilled(this.value));
} catch (error) {
this.error = error;
rejected(this.error);
}
}
if (this.status === "rejected") {
rejected(onRejected(this.error));
}
});
return promise2;
}
}
前端 mvvm(react、vue,)框架基本都是由以下几部分组成
- 虚拟 domdiff 算法(本质是如何找到一个对象树的差异并更新,当然为了避免一股脑 diff 可能造成页面卡顿,可以设计成时间切片的形式)
- 如何设计组件化(函数组件、类组件)
- 数据更新机制(vue 是数据劫持、react 是调用 setState)
- 全局状态管理(vuex、redux...)
- 路由设计逻辑复用机制(hooks、Function based )
- 模板语法的选择(jsx or template)实现模板解析
综上,再复杂的框架都是由各个小的技术点累计而成的,那么将以上每一个技术点都能钻研透彻,并能够将其灵活的组合起来的那么你离完成一个框架就不远了。
命令式和声明式开发的区别
计算机系统是分层的,也就是下层做一些支持的工作,暴露接口给上层用。注意:语言的本质是一种接口。
计算机的最下层是 CPU 指令,其本质就是用“变量定义+顺序执行+分支判断+循环”所表达的逻辑过程。
计算机应用的最上层是实现人类社会的某种功能。所以所有计算机编码的过程,就是用逻辑表达现实的过程。层与层之间定义的借口,越接近现实的表达就叫越“声明式”(declarative),越接近计算机的执行过程就叫越“命令式”(imperative)。注意这不是绝对的概念,而是相对的概念。
当接口越是在表达“要什么”,就是越声明式;越是在表达“要怎样”,就是越命令式。
SQL 就是在表达要什么(数据),而不是表达怎么弄出我要的数据,所以它就很“声明式”。C++就比 C 更声明式,因为面向对象本身就是一种声明式的体现。HTML 也很声明式,它只描述我要一张什么样的表,并不表达怎么弄出一张表,而 DOM 操作就是命令式的。
简单的说,接口的表述方式越接近人类语言——词汇的串行连接(一个词汇实际上是一个概念)——就越“声明式”;越接近计算机语言——“顺序+分支+循环”的操作流程——就越“命令式”。
越是声明式,意味着下层要做更多的东西,或者说能力越强。也意味着效率的损失。越是命令式,意味着上层对下层有更多的操作空间,可以按照自己特定的需求要求下层按照某种方式来处理。实际上,这对概念应该叫做“声明式接口”和“命令式接口”。可能是因为它大部分时候是在谈论“语言”这种接口方式时才会用到,所以会叫做“声明式编程”和“命令式编程”。当然,你也可以把它当成一种编程思想,也就是说,在构建自己的代码时,为了结构的清晰可读,把代码分层,层之间的接口尽量声明式。这样你的代码自然在一层上主要描述从人的角度需要什么;另一层上用计算机逻辑实现人的需要。另外,这组概念总让人迷惑,可能一个原因是翻译问题。如果翻译成”说明式“和”指令式“应该容易理解的多。
- 命令式编程:命令“机器”如何去做事情(how),这样不管你想要的是什么(what),它都会按照你的命令实现。
- 声明式编程:告诉“机器”你想要的是什么(what),让机器想出如何去做(how)。
HOC、Render props、Hooks
HOC(高阶组件)高阶组件是参数为组件,返回值为新组件的函数。
- HOC 优点
- 不会影响内层组件的状态, 降低了耦合度
- HOC 可以做到很轻松地外部协议化注入功能到一个基础 Component 中,所以可以用来做插件。
- HoC 的方式可以天然地进行组件的分层以及组合,并且这种分层基本都可以描述为状态注入以及 props mapping 的过程。
- HOC 的缺点
- 固定的 props 可能会被覆盖
- 调试不方便,当多个 HOC 嵌套时,props 究竟是来自于哪里,如果有问题、问题又出自于哪里。
Render props 将一个组件内的 state 作为 props 传递给调用者, 调用者可以动态的决定如何渲染.
- 接收一个外部传递进来的 props 属性
- 将内部的 state 作为参数传递给调用组件的 props 属性方法.
class Mouse extends React.Component {
constructor(props) {
super(props);
this.state = { x: 0, y: 0 };
}
render() {
return (
<div style={{ height: "100%" }}>{this.props.render(this.state)}</div>
);
}
}
// 调用方式:
<Mouse
render={(mouse) => (
<p>
鼠标的位置是 {mouse.x},{mouse.y}
</p>
)}
/>;
- 缺点
- 无法在 return 语句外访问数据
- 很容易导致嵌套地狱
Hook
const { x, y } = useMouse();
const { x: pageX, y: pageY } = usePage();
useEffect(() => {}, [pageX, pageY]);
- hook 可以重命名参数
- 如果 2 个 hook 暴露的参数一样,我们可以简单地进行重命名.
- hook 会清晰地标注来源
- 从上面的例子可以简单地看到, x 和 y 来源于 useMouse. 下面的 x, y 来源于 usePage
- hook 可以让你在 return 之外使用数据
- hook 不会嵌套
- 简单易懂, 对比 hoc 和 render props 两种方式, 它非常直观, 也更容易理解
DomToJson
function domToJson(node) {
return node && node.tagName
? {
tag: node.tagName,
childs: Array.from(node.childNodes).map((n) => domToJson(n)),
}
: { tag: "TEXT" };
}
console.log(domToJson(document.getElementById("mydom")));