个人面试经历吧,觉得重要的分享一下。原生才是重中之重
原型与原型链
原型(prototype):定义了一些公用的属性和方法,利用原型创建出来的新对象实例会共享原型的所有属性和方法
所有函数拥有一个原型属性(prototype),也称显示原型
每个对象(除null)都有__proto__(隐式原型),对于js,可以说一切皆是对象,所以函数也有。对象的__proto__指向创建该对象的构造函数的原型对象(prototype)。
Function.prototype是个特例,它是函数对象,但是没有prototype属性。其他所有函数都有prototype属性
以函数对象为例
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.say = function () {
console.log("公用方法");
};
const p1 = new Person("张三", 18);
const p2 = new Person("李四", 20);
console.log(p1.__proto__, Person.prototype);
console.log(p1.__proto__ === Person.prototype); // 实例的__proto__指向创建该对象的构造函数的原型对象
console.log(p2.__proto__ === Person.prototype);
console.log(p1.__proto__ === p2.__proto__); // true
console.log(p1.say === p1.say); // 实例后的原型对象是共享(同一指针地址),所以定义的方法也是同一
console.log(p1.__proto__.constructor === Person); // 实例上原型对象constructor属性指向构造函数
console.log(p1.__proto__.constructor === Person.constructor); // false
console.log(p1.__proto__.constructor); // Person
console.log(Person.__proto__.constructor); // Function 函数对象的构造函数是Function
console.log(Person.__proto__ === Function.prototype); // true
console.log(Function.prototype.prototype === undefined); // true // Function.prototype是个特例,它是函数对象,但是没有prototype属性。其他所有函数都有prototype属性。
console.log(Function.__proto__ === Function.prototype); // true 内置的Function也是一个函数对象,它是通过自己来创建自己的
console.log(Function.prototype.__proto__ === Object.prototype); // true Function.prototype__proto__指向Object.prototype
console.log(Object.prototype.__proto__); // null
以数据类型为例
const test = "测试";
console.log(test.__proto__ === String.prototype); // true
console.log(String.prototype.__proto__ === Object.prototype); // true
以普通对象为例
const obj = {
name: "张三",
};
console.log(obj.__proto__ === Object.prototype); // true
原型链是基于__proto__形成的,继承是通过prototype实现的
每个对象都有一个__proto__属性,原型链上的对象正是依靠这个__proto__属性连结在一起的! 对于原型链上的一个对象obj,那么访问obj.xxx属性(方法也是属性)的过程是: 如果自身有xxx属性,则访问它;如果没有,就通过__proto__属性找到其原型链的上一级原型对象,看它有没有xxx属性,如此递归查找,直至找到xxx属性或到了原型链顶端Object.prototype对象为止。而Object.prototype.__proto__以null值结束。
bind、call与apply
三者都改变函数执行时的上下文对象,通俗点this的指向
call、apply与bind的差别
- bind方法是事先把fn的this改变为我们要想要的结果,并且把对应的参数值准备好,以后要用到了,直接的执行即可,也就是说bind不会马上的执行(react中this指向问题),而call和apply是立马执行的
call、apply的区别
- 在于参数的区别,call和apply的第一个参数都是要改变上下文的对象,而call从第二个参数开始以参数列表的形式展现,apply则是把除了改变上下文对象的参数放在一个数组里面作为它的第二个参数
let obj = {name: '张三'};
function Child(name){
this.name = name;
}
Child.prototype.showName= function(){
console.log(this.name);
}
var child = new Child('李四');
child.showName(); // 李四
// call,apply,bind使用
child.showName.call(obj);
child.showName.apply(obj);
let bind = child.showName.bind(obj); // 返回一个函数
bind(); // 张三
手写bind
Function.prototype.bind2 = function (context) {
//对context进行深拷贝,防止bind执行后返回函数未执行期间,context被修改
const ctx = JSON.parse(JSON.stringify(context)) || window
ctx.func = this
const args = Array.from(arguments).slice(1)
return function () {
//注意bind方法需要合并两次函数执行的参数 (bind 要声明一次,还要调用一次。一共两次,参数合并)
const Allargs = args.concat(Array.from(arguments))
return Allargs.length > 0 ? ctx.func(...Allargs) : ctx.func()
}
}
//测试
obj = { c: 2 }
function a(x,y,z) { console.log(this, x, y, z) }
a.bind(obj,1,2)(3)//{c:2} 1 2 3
a.bind2(obj,1,2)(3)//{c:2,func:[function a]} 1 2 3
闭包
嵌套的内部函数,引用外部的变量
闭包的含义是函数在调用时能够访问函数在定义时可以访问的作用域,例如在定义函数a的时候,a能够访问变量b。每一个函数都有自己对应的闭包,当函数没有被垃圾回收机制回收时函数对应的闭包也会常驻内存。如果需要清除闭包就要回收不需要的函数,根据JavaScript回收机制,当一个内存空间没有变量指向的时候就会被回收。那么闭包清除的方式就是将不需要的函数名赋值为null。
generator
generator(生成器)是ES6标准引入的新的数据类型。一个generator看上去像一个函数,但可以返回多次。
与函数的不同之处
generator和函数不同的是,generator由function定义(注意多出的号),并且,除了return语句,还可以用yield返回多次
function* generator() {
yield "1";
yield "2";
}
var it = generator();
console.log(it);
console.log(it.next());
console.log(it.next());
console.log(it.next());
yield
Generator函数返回的Iterator运行的过程中,如果碰到了yield, 就会把yield后面的值返回, 此时函数相当于停止了, 下次再执行next()方法的时候, 函数又会从上次退出去的地方重新开始执行;
yield*
yield*这种语句让我们可以在Generator函数里面再套一个Generator
next()
generator函数返回的Iterator执行next()方法以后, 返回值的结构为:
{
value : "value", //value为返回的值
done : false //done的值为一个布尔值, 如果Interator未遍历完毕, 他会返回false, 否则返回true;
}
next方法传参数, 那么这个参数将会作为上一次yield语句的返回值 ,这个特性在异步处理中是非常重要的, 因为在执行异步代码以后, 有时候需要上一个异步的结果, 作为下次异步的参数, 如此循环::
function* foo(x) {
var y = 2 * (yield x + 1);
var z = yield y / 3;
return x + y + z;
}
var a = foo(5);
console.log(a.next()); // Object{value:6, done:false}
console.log(a.next()); // Object{value:NaN, done:false}
console.log(a.next()); // Object{value:NaN, done:true}
var b = foo(5);
console.log(b.next()); // { value:6, done:false } x = 5
console.log(b.next(12)); // { value:8, done:false } y = 24
console.log(b.next(13)); // { value:42, done:true } 5+24+13
模拟generator
function gen(array){
var nextIndex = 0;
return {
next: function(){
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{value: undefined, done: true};
}
};
};
var it = gen(["arr0", "arr1", "arr2", "arr3"]);
console.log( it.next() );
console.log( it.next() );
console.log( it.next() );
console.log( it.next() );
console.log( it.next() );
new 的作用与原理
作用:返回一个对象
- 构造函数中没有return,默认返回this,即实例对象
- 有return ,但是返回的不是对象,继续返回默认的this
- 有return并且是对象,那么返回该对象。无论如何都会返回一个对象
原理
- 创建一个空对象,作为将要返回的实例。
- 将空对象的原型(proto)指向构造函数的prototype属性。
- 将空对象赋值给构造函数中的this。
- 指向构造函数中的代码
function _new(/* 构造函数 */ constructor, /* 构造函数参数 */ params) {
// 将 arguments 对象转为数组
var args = [].slice.call(arguments);
// 取出构造函数
var constructor = args.shift();
// 创建一个空对象,继承构造函数的 prototype 属性
var context = Object.create(constructor.prototype);
// 执行构造函数
var result = constructor.apply(context, args);
// 如果返回结果是对象,就直接返回,否则返回 context 对象
return (typeof result === 'object' && result != null) ? result : context;
}
vue中v-for key的作用
vue实现了一套虚拟DOM,使我们可以不直接操作DOM元素,只操作数据便可以重新渲染页面。而隐藏在背后的原理便是其高效的Diff算法。
当页面的数据发生变化时,Diff算法只会比较同一层级的节点:
如果节点类型不同,直接干掉前面的节点,再创建并插入新的节点,不会再比较这个节点以后的子节点了。
如果节点类型相同,则会重新设置该节点的属性,从而实现节点的更新。
当某一层有很多相同的节点时,也就是列表节点时,Diff算法的更新过程默认情况下也是遵循以上原则。
比如一下这个情况:
我们希望可以在B和C之间加一个F,Diff算法默认执行起来是这样的:
即把C更新成F,D更新成C,E更新成D,最后再插入E,是不是很没有效率?
所以我们需要使用key来给每个节点做一个唯一标识,Diff算法就可以正确的识别此节点,找到正确的位置区插入新的节点。
所以一句话,key的作用主要是为了高效的更新虚拟DOM。另外vue中在使用相同标签名元素的过渡切换时,也会使用到key属性,其目的也是为了让vue可以区分它们。
列表展示的时候,用index无伤大雅;但是涉及到增删的时候,与相应 key 的绑定关系有变化,所以被重新渲染,这会影响性能。还有绑定值的时候与index关联也会出现错乱。
async与await
async、await、promise三者是es6新增的关键字,async-await 是建立在 promise机制之上的,并不能取代其地位。
async: 作为一个关键字放在函数前面,用于表示函数是一个异步函数,因为async就是异步的异步,异步函数也就是意味着这个函数的执行不会阻塞后面代码的执行。
async表示函数异步,定义的函数会返回一个promise对象,可以使用then方法添加回调函数。
async function test() {
const a = 1;
return a;
}
console.log(test().then((res) => console.log(res)));
如果async内部发生错误,使用 throw 抛出,catch捕获
await 等待, 后面可以跟任何的JS 表达式。虽然说 await 可以等很多类型的东西,但是它最主要的意图是用来等待 Promise 对象的状态被 resolved。如果await的是 promise对象会造成异步函数停止执行并且等待 promise 的解决,如果等的是正常的表达式则立即执行。
function demo(){
return new Promise((resolve, reject) => {
resolve('hello promise!');
});
}
(async function exec(){
let res = await demo();
console.log(res); //hello promise!
})();
防抖与节流
圣杯样式实现
<div class="box">
<div class="left"></div>
<div class="right"></div>
<div class="middle"></div>
</div>
- flex: left和right定宽为200px,middle自适应
.box{
height: 200px;
background-color: yellow;
display: flex;
}
.box .middle{
height: 200px;
background: pink;
flex: 1;
}
.box .left{
width: 200px;
height: 200px;
background-color: red;
}
.box .right{
width: 200px;
height: 200px;
background-color: purple;
}
- 将left进行浮动靠左,将right进行浮动靠右
.box{
height: 200px;
background-color: yellow;
}
.box .left{
width: 200px;
height: 200px;
background-color: red;
float: left;
}
.box .right{
width: 200px;
height: 200px;
background-color: purple;
float: right;
}
.box .middle{
height: 200px;
background-color: pink;
}
- left和right宽度设为20%,中间60%自适应
.box {
position: relative;
height: 300px;
}
.box .left {
width: 20%;
height: 100%;
background-color: red;
position: absolute;
top: 0;
left: 0;
}
.box .middle {
height: 100%;
background-color: pink;
position: absolute;
top: 0;
left: 20%;
width: 60%;
}
.box .right {
width: 20%;
height: 100%;
background-color: purple;
position: absolute;
top: 0;
right: 0;
}
vue defineProperty
Object.defineProperty无法监控到数组下标的变化,导致通过数组下标添加元素,不能实时响应;
Object.defineProperty只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果,属性值是对象,还需要深度遍历。