一、原型链
1、构造函数
原型首先需要明白构造函数、实例原型、和实例之间的关系(不得不承认有点儿乱)
function Person() {
}
// 虽然写在注释里,但是你要注意:
// prototype是函数才会有的属性
Person.prototype.name = 'Kevin';
var person1 = new Person();
var person2 = new Person();
console.log(person1.name) // Kevin
console.log(person2.name) // Kevin
这是构造函数,构造函数有prototype上面的属性直接进行了赋值
创建了实例对象输出,最后输出(一般都没啥问题)
*一个构造函数可以生成多个实例
2、prototype属性
每个函数都有一个prototype属性(函数,函数,函数)
function Person() {
}
// 虽然写在注释里,但是你要注意:
// prototype是函数才会有的属性
Person.prototype.name = 'Kevin';
var person1 = new Person();
var person2 = new Person();
console.log(person1.name) // Kevin
console.log(person2.name) // Kevin
从上面可以看出,protype指向的是通过构造函数调用创建的实例对象,即prototype是person1和2的原型,每一个对象都会从原型中继承属性。
3、__proto__属性
每个javascript对象上都有一个__proto__属性(对象,对象,对象),该属性会指向该对象的原型。(即person1.__proto__ === Person.prototype)
4、constructor属性
每一个原型都有一个constructor属性(原型,原型,原型),该属性指向该原型的构造函数(即Person === Person.prototype.constructor)
*需要注意的是,当person(小写)没有constructor属性时,他会在该对象的原型对象上去找,即person.constructor = Person.prototype.constructor
接下来就是面试中经常会问到或者笔试遇到的一些问题
5、实例与对象
当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,直到找到最顶层为止。
function Person() {
}
Person.prototype.name = 'Kevin';
var person = new Person();
person.name = 'Daisy';
console.log(person.name) // Daisy
delete person.name;
console.log(person.name) // Kevin
第一个输出,打印的就是该对象本身属性;第二个输出,因为自己本身对象上并没有这个属性,就去找该对象的原型的属性,即Person.prototype的name属性,所以打印出来是原型的属性值。
6、原型的原型
原型对象也是对象,所以也可以用最原始的方法去创建他。
var obj = new Object();
obj.name = 'Kevin'
console.log(obj.name) // Kevin
即obj.proto === Objcet.prototype
Objcet.prototype.__proto === null 此处Objcet.prototype的原型指向null,即最初的Objcet.prototype没有原型即为最顶层了已经
7、原型链
本质上来说就是一个链表结构,即有属性(__proto__)值将各个节点(即原型对象,Function.prototype,Object.prototype)连接在了一起。
三个常用和容易混淆的原型链指向顶层(除对象外,都需要先指向自己的本身的原型对象,再指向Object的原型对象,适用于其他number类型,string类型等等)
obj -> Object.prototype -> null 即 obj.__proto__ = Object.protype Object.protype.__proto === null
func -> Function.prototype -> Object.prototype -> null
arr -> Array.prototype -> Object.prototype -> null
8、补充instanceof,判定为true的条件即是在原型链上能找到对象的类型
举个例子:arr instanceof Array 输出为true 因为在arr的原型对象上能找到Array对象
arr instanceof Object 输出也为true,因为Array的原型指向Object,所以arr的原型链上也能找到Object所以为true
9、常见易错面试
上面说到,当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,直到找到最顶层为止。
const obj = {}
obj.x 直接输出肯定是undefined
但是在obj原型上挂一个属性后即Object.prototype.x === 'x'
obj.x 输出为x
最易出错的是
const func = () => {}
Object.prototypr.x === 'x'
func.x 输出为x
get到了么,最后一道常见面试题测试下吧
var foo = {},
F = function() {}
Object.prototype.a = 'value a';
Function.prototype.b = 'value b'
console.log(foo.a); console.log(foo.b); console.log(F.a); console.log(F.b)
答案:value a; undefined; value a; value b
二、继承
1、构造函数继承
将父类构造函数的内容复制给子类的构造函数,唯一一个不涉及到prototype的继承
ParentType.call(ChildType);
优点:和原型链继承完全反着来
缺点:父类的引用属性不能共享;子类构建实例的时候能给父类传值
2、原型链继承
让子类的原型指向父类的实例
ChildType.prototype = new ParentType()
// 所有涉及到原型链继承的继承方式都要修改子类构造函数的指向,否则子类实例的构造函数会指向PerentType。
ChildType.prototype.constructor =ChildType;
优点:父类方法可以复用
缺点:父类引用属性会被所有子类共享;子类构建实例的时候不能给父类传值
3、原型式继承
本质就是一个object方法对一个参数对象的浅复制
优缺点和原型链继承一样,但是性能优于原型链继承
4、组合继承
原型式继承和构造函数继承的组合,兼具了两者的优点;但是会调用两次父类函数
5、寄生式继承
使用原型式继承获取目标对象的浅复制,然后增强复制能力
没什么优缺点
6、寄生组合继承
解决组合继承调用两次父类的问题
完美的继承方案
7、es6的class extends
寄生组合继承结果一样。一个是先创建实例对象this继承,再对其增强;es6的是先将对象的属性方法加到this上面,然后用子类构造函数修改this
三、作用域
1、词法作用域和动态作用域
因为 JavaScript 采用的是词法作用域,函数的作用域在函数定义的时候就决定了。
而与词法作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的。
*javascript的作用域是静态作用域
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar();
从静态作用域分析:输出1,因为在foo中没有找到value,从上一层去找
从动态作用域分析:输出2,因为函数的作用域是在函数调用的时候决定的,所以,foo找不到value,去上一层去找,上一层是bar函数
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope();
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()();
两个都会输出"local scope"
因为javascript是词法作用域,它的作用域基于函数创建的位置,和其他无关,两个函数f都是在checkscope中创建的,所以它的作用域就是这个函数内部
2、执行上下文栈
javascript执行顺序:顺序执行?javascript会去一段一段执行代码,会先去进行一个准备工作,例如变量提升等等。
javascript当执行一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出。
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope();
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()();
上面的两端代码的执行结果一样,但是过程不一样
第一段代码:
ECStack.push(<checkscope> functionContext);
ECStack.push(<f> functionContext);
ECStack.pop();
ECStack.pop();
第二段代码:
ECStack.push(<checkscope> functionContext);
ECStack.pop();
ECStack.push(<f> functionContext);
ECStack.pop();
4、变量对象
当进入执行上下文时,这时候还没有执行代码,
变量对象会包括:
-
函数的所有形参 (如果是函数上下文)
由名称和对应值组成的一个变量对象的属性被创建
没有实参,属性值设为 undefined
-
函数声明
由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建
如果变量对象已经存在相同名称的属性,则完全替换这个属性
-
变量声明
由名称和对应值(undefined)组成一个变量对象的属性被创建;
如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性
简单一句话就是:函数传参 > 函数声明 > 变量声明
5、作用域链
当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
var scope = "global scope";
function checkscope(){
var scope2 = 'local scope';
return scope2;
}
checkscope();
执行过程如下:
1.checkscope 函数被创建,保存作用域链到 内部属性[[scope]]
checkscope.[[scope]] = [
globalContext.VO
];
2.执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈
ECStack = [
checkscopeContext,
globalContext
];
3.checkscope 函数并不立刻执行,开始做准备工作,第一步:复制函数[[scope]]属性创建作用域链
checkscopeContext = {
Scope: checkscope.[[scope]],
}
4.第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: undefined
},
Scope: checkscope.[[scope]],
}
5.第三步:将活动对象压入 checkscope 作用域链顶端
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: undefined
},
Scope: [AO, [[Scope]]]
}
6.准备工作做完,开始执行函数,随着函数的执行,修改 AO 的属性值
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: 'local scope'
},
Scope: [AO, [[Scope]]]
}
7.查找到 scope2 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出
ECStack = [
globalContext
];
四、闭包
以下函数算是闭包:
1、即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
2、在代码中引用了自由变量
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]();
输出都是3
分析:输出的i并没有进行定义,所以i是从函数的作用域链上去拿的值,在拿值之前,全局上下文中的i已经是3
五、变量提升
console.log(v1);
var v1 = 100;
function foo() {
console.log(v1);
var v1 = 200;
console.log(v1);
}
foo();
console.log(v1);
// undefined undefined 200 100
看会这道题就明白了
函数变量声明的优先级高于变量声明的优先级
打印分析:首先是var v1;输出是undefinen;第二,函数声明提升在var v1 = 100之前,函数第一个打印出来是undefined;第三,函数内部的执行输出200;最后的打印,因为函数定义内部随着函数一起被提升,最后赋值的是100,输出100.
补充个小知识点儿:函数声明式写法才会导致变量提升,表达式写法是不会导致变量提升的
六、this指向
七、立即执行函数
作用:创建一个独立作用域;闭包和私有数据
for(var i=0;i<2;i++){
liList[i].onclick=function(){
console.log(i);
}
};
输出都是3;原因就不解释了
for(var i=0;i<2;i++){
(function(ii) {
liList[ii].onclick=function(){
console.log(ii);
}
})(i)
};
输出是0,1,2;因为立即执行函数形成了自己的作用域;es6的话可以用let
八、instanceof原理
原理其实很简单,就是instanceof函数检测的是沿着原型链去查找匹配类型,所以他不准确的原因是,除了object外,array、function等除了指向本身原型链外,最终都会指向object
九、bind实现
十、apply和call
call
call() 方法在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。
var foo = {
value: 1
};
function bar() {
console.log(this.value);
}
bar.call(foo); // 1
call改变了this的指向,此时的this指向全局window
简单模拟实现
function foo() {
value : 1;
bar: function() {
console.log(this.value)
}
}
foo.bar()
// 第一版
Function.prototype.call2 = function(context) {
// 首先要获取调用call的函数,用this可以获取
context.fn = this; // 调用的时候才去讲this进行赋给上下文
context.fn();
delete context.fn;
}
// 测试一下
var foo = {
value: 1
};
function bar() {
console.log(this.value);
}
bar.call2(foo); // 1
call 函数还能给定参数执行函数
// 第二版
思路:从argument中去拿到参数的信息,然后传到调用的方法作用域中去
Function.prototype.call2 = function(context) {
// 首先要获取调用call的函数,用this可以获取
context.fn = this; // 调用的时候才去讲this进行赋给上下文
var arg = []
// 将所有的atguments中的值进行动态的遍历
for(let i = 1; i < arguments.length; i++) {
args.push('arguments['+ i +']')
}
eval('context.fn(' + args +')') // args 会自动调用 Array.toString() 这个方法。 进行拼接
context.fn();
delete context.fn;
}
var foo = {
value: 1
};
function bar(name, age) {
console.log(name)
console.log(age)
console.log(this.value);
}
bar.call2(foo, 'kevin', 18);
// kevin
// 18
// 1
// 第三版
Function.prototype.call2 = function (context) {
var context = context || window; // 当函数空调的时候,传入的值为undefined,此时需要指向window
context.fn = this;
var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
var result = eval('context.fn(' + args +')');
delete context.fn
return result; // 调用完的对象是能够进行返回的
}
// 测试一下
var value = 2;
var obj = {
value: 1
}
function bar(name, age) {
console.log(this.value);
return {
value: this.value,
name: name,
age: age
}
}
bar.call2(null); // 2
console.log(bar.call2(obj, 'kevin', 18));
// 1
// Object {
// value: 1,
// name: 'kevin',
// age: 18
// }
call函数的实现:
Function.prototype.apply = function(context, arr) {
var context = Object(context) || window;
context.fn = this;
var result;
if(!arr) {
result = context.fn();
} else {
var args = []
for(let i =0;i< arr.length; i++) {
args.push('arr['+ i +']')
}
result = eval('context.fn('+args+')')
}
delete context.fn();
return result;
}
十一、柯里化
简单来说就是把接受多个参数变成接受一个参数,举个例子a(x,y) => a(x)(y)
作用:
参数重复利用:在实现第一个函数后可以将第一个函数进行保存,用来穿参执行不同的函数的调用
十二、v8垃圾回收机制
分为副垃圾回收器和主垃圾回收器
副垃圾回收器:会向内存中新生代去分配,假如新生代是from space是工作状态,就放入from space中;经过一段时间的运行内存达到上限;会将根结点无法遍历到的对象进行标记,将未标记的对象进行复制,复制到to space,将form space进行清空,将to space状态转换成from space状态。
主垃圾回收器:主要是有一些未标记的体积太大,放到老生代内存区,会将标记的直接进行清除。
垃圾回收优化策略:并行垃圾回收、增量垃圾回收、并发垃圾回收、空闲时垃圾回收
十三、浮点数精度
经典0.1+0.2不等于0.3
原因:首先十进制的浮点数会先转换成二进制,但是浮点数的二进制是无穷的,而标准的浮点数精度只支持53位,所以计算时会产生误差。
解决办法:tofix截取;转换成整数后处理后再恢复
十四、new
function Person(firtName, lastName) {
this.firtName = firtName;
this.lastName = lastName;
}
Person.prototype.sayName = function() {
console.log(this.firtName)
}
// 函数的new的实例
// const tb = new Person('chen')
// tb.sayName()
// 手写的new函数
func = (obj,...rest) => {
// 用传入的对象原型去创建一个空的对象
const newObj = Object.create(obj.prototype);
// 现在newObj就代表Person,但是this指向没有修改
const res = obj.apply(newObj, rest);
// 判断传入的obj是否是null或者undefined,我们返回的是newObj,否则返回res
return typeof res === 'object' ? res : newObj
}
const tb = func(Person, 'chen')
tb.sayName()
十五、事件循环机制
1、所有任务都能分为同步任务和异步任务,同步任务就是立即执行任务,一般进入到主线程中去执行,异步任务就是异步执行的任务,比如ajax,setTimeout,异步任务会通过任务队列的机制来进行协调,同步任务进入主执行栈,主执行栈执行完,会去 Event Queue 读取对应的任务,推入主线程执行。
2、promise属于微任务,setTimeout属于宏任务,先执行微任务后执行宏任务
十六、promise原理
为什么出现:javascript是单线程,有很多的异步执行,可以通过回调解决,但是太多会进入回调地狱。
原理剖析:
首先清楚,promise的原理还是回调,只不过是封装好了,看起来简洁;内部有三种状态,pending,fulfilled,rejected;只有两种过程,进行中到成功和进行中到失败;有两个回调,resolve和rejtct
//Promise应用
let p = new Promise(resolve => {
setTimeout(() => {
console.log('done')
resolve('5秒')
},5000)
}).then((tip) => {
console.log(tip)
})
基础版本实现:
class Promise = {
callback = [];
constructor(fn) {
fn(this._resolve.bind(this))
}
then(onFulfilled) {
this.callback.push(onFulfilled)
return this // then链式调用
}
_resolve(value) {
this.callback.forEach(fn => fn(value))
}
}
加入延时机制:(上面应用去掉setTimeout后面的then不会执行,因为resolve的状态已经变为成功,状态不可更改)
class Promise = {
callback = []
constructor(fn) {
fn(this._resolve.bind(this))
}
then(onFulfilled) {
this.callback.push(onFulfilled)
return this
}
_resolve(value) {
setTimeout(() => {
this.callback.forEach(fn => fn(value))
})
}
}
// 第二个应用(then3不会执行)
let p = new Promise(resolve => {
console.log('同步执行');
resolve('同步执行');
}).then(tip => {
console.log('then1', tip);
}).then(tip => {
console.log('then2', tip);
});
setTimeout(() => {
p.then(tip => {
console.log('then3', tip);
})
});
// 加入状态机制(基础版本完成)
class Promise = {
callback = []
state = 'pending' // 初始状态
value = null // 保存结果
constructor(fn) {
fn(this._resolve.bind(this))
}
then(onFulfilled) {
if (this.state === 'pending') {
this.callback.push(onFulfilled) // 在resolve之前,和之前逻辑一样
} else { // 在resolve之后直接执行回调
onFulfilled(this.value)
}
return this
}
_resolve(value) {
this.state = 'fulfilled' // 改变状态
this.value = value // 保存结果
this.callback.forEach(fn => fn(value)
}
}
// 真正的链式调用实现
class Promise = {
callback = []
state = 'pending'
value = null;
constructor(fn) {
fn(this._resolve.bind(this))
}
then(onFulfilled) {
return new Promise(resolve => {
this._handle({
onFulfilled: onFulfilled || null
resolve: resolve
})
})
}
_handle(callback) {
if (this.state === 'pending') {
this.callback.push(callback)
return
}
if (!callback.onFulfilled) { // 如果then中没有传递任何东西
callback.resolve(this.value)
return
}
var ret = callback.onFulfilled(this.value)
callback.resolve(ret)
}
_resolve(value) {
if(value && (typeof value === 'object' || typeof value === 'function')) {
var then = value.then
}
this.state = 'fulfilled'
this.value = value
this.callback.forEach(fn => fn(value))
}
}