原型链
每一个构造函数都会有一个原型对象,可以使用prototype来找到,通过构造函数进行实例化对象的时候,这个实例化对象就会有一个__proto__的属性在里面,这个属性指向的就是原型对象。
原型对象不能直接找到实例化对象,但是可以通过constructor找到构造函数,每一个原型对象都有一个constructor指向关联的构造函数。
原型链:当读取一个实例化对象的属性时,如果他自身没有这个属性,就会向他的上一级原型对象上找,如果他的构造函数的原型对象也没有,就继续往构造函数的构造函数的原型属性去找,一级一级找到最顶层为止。最顶层就是Object.prototype,它指向的时null。
整个过程有点像继承,但是js不会复制对象的属性,所以不应该叫做继承。
继承
原型链继承
用法:改变子函数的prototype指向父函数。
优点:通过子函数实例化出来的对象,如果修改了数据,这些数据会通过原型链被其他实例化对象共享。
缺点:不能向父级Parent传参。
function Parent(){
this.names = ['a','b'];
}
function Child(){
}
Child.prototype = new Parent();//使用原型链继承
var child1 = new Child();
child1.names.push('c')
console.log(child1.names);//['a','b','c']
var child2 = new Child();
console.log(child2.names);//['a','b','c']
经典继承(借用构造函数)
用法,在子函数中使用call改变this指向父函数
优点:数据不会被共享,可以向Parent传参
缺点:方法都定义在构造函数中,每一次实例化都会创建一遍方法,复用差,浪费性能
//不传参的例子
function Parent(){
this.names = ['a','b'];
}
function Child(){
Parent.call(this);//改变this指向
}
var child1 = new Child();//使用构造函数创建实例化对象
var child2 = new Child();
child1.names.push('c')
console.log(child1.names)//['a','b','c']
console.log(child2.names)//['a','b']
//传参的做法
function Parent(name){
this.name='a';
}
function Child(name){
Parent.call(this,name);//改变this指向,并传参
}
var child1 = new Child('a');
var child2 = new Child('b');
console.log(child1)//a
console.log(child2)//b
组合继承(1和2结合)(常用)
优点:结合了两种方式的优点,可传参,复用性高
缺点:调用两次父类的构造函数(消耗内存)
function Parent(name){
this.name = name
this.num = [1,2,3]
}
function Child(name){
Parent.call(this,name)
}
Child.prototypr = new Parent();//调用一次Parent构造函数
Child.prototype.contructor = Child;
var child1 = new Child('nike')//又调用一次Parent构造函数
var child2 = new Child('like')
child1.num.push(4)
console.log(child1.name)//nike
console.log(child1.num)//[1,2,3,4]
console.log(child2.name)//like
console.log(child1.num)//[1,2,3]
原型式继承(ES5 Object.create)
将传入的对象作为创建对象的原型,这也是ES5中的Object.create的模拟实现
function creatrObj(o){
function F(){}
F.prototype = o;
return new F();
}
缺点:1.与原型链继承一样,会共享数据。
2.无法实现复用。(新实例属性都是后面添加的)
例子如下:
function create(o){
function F(){}
F.prototype = o;
return new F();
}
var Parent ={
name = 'nike';
num = [1,2,3];
}
var child1 = create(Parent);
var child2 = create(Parent);
child1.name = 'like'
child1.num.push(4)
console.log(child1.name);//like
console.log(child1.num);//[1,2,3,4]
console.log(child2.name);//nike
console.log(child2.num);//[1,2,3,4]
//child1和child2的name值不同是因为,child1.name = 'like'这句代码会给child1添加一个name属性,而child2用的是Parent上的属性
寄生式继承
实现:其实就是在原型式继承的基础上再套一个函数
function create(o){
function F(){}
F.prototype = o;
return new F();
}
function createObj(o){//把原型式继承再用一个function包起来
var clone = new create(o);
clone.name = 'nike';
return clone;
}
var Parent = {
name = 'like';
num = [1,2,3];
}
var child = new createObj(Parent);
console.log(child.name);//nike
console.log(child.num);//[1,2,3]
寄生组合继承(最理想)
其实就是通过使用第三方函数,让这个第三方函数的实例对象指向父类的实例对象,再把子类的实例对象指向第三方的实例上。
优点:只调用了一次 Parent 构造函数,原型链保持不变
function Parent(name){
this.name = name;
this.num = [1,2,3];
}
function Child(name,age){
Parent.call(this,name);
this.age = age;
}
var F = function(){}
F.prototype = Parent.prototype
Child.prototype = new F();
var child1 = new Child('nike','18');
console.log(child1);
上述代码经过封装后就会变成:
function create(o){
function F(){}
F.prototype = o;
return new F();
}
function prototype(child,parent){
var prototype = create(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
}
//调用时
prototype(Child,Parent)
作用域与作用域链
在js中没有块级作用域,es6才新增了块级作用域,但是在以下情况会形成块级作用域 {} if{} for{},这些作用域也是局部作用域。每定义一个函数都会生成一个局部作用域,在没有函数包围的最外层有一个全局作用域window。
优先级:局部作用域>全局作用域
在查找对象时,优先在局部作用域中查找,在当前函数作用域中找不到时,向上级函数作用域查找,直到最终找到window全局对象为止,这个链条就被成为作用域链。
在查找的过程中有些地方需要注意:js中有变量提升的概念,还要注意匿名函数也会有作用域,举个例子说明:
var i = "外部全局变量";
function a() {
console.log(i);//函数作用域中找不到,向上查找,打印全局变量
}
function b() {
console.log(i);//虽然写在下面,但是变量提升,相当于在函数第一句写了var i
//所以不会获取全局变量
var i = "变量提升";
}
function c() {
var i = "局部变量";
console.log(i);//有局部变量,优先在自己函数作用域找到并输出
}
a();
b();
c();
闭包
官方的定义是:指那些可以访问自由变量的函数
换成最简单的话来说,闭包其实就是函数嵌套函数。
优点:可以获取函数作用域中的变量,并且使它不会被垃圾回收机制回收
缺点:因为变量不会被回收,造成内存浪费
实现:
function a(){
var x=1;
function b(){
x++
return x
}
return b;
}
a();
立即执行函数
顾名思义,不需要手动调用就会自动执行的函数,写法很简单,就是两个小括号()();或者(()),他可以是具名函数也可以是匿名函数
(function(){
alert('自执行函数')
})();
(function(){
alert('自执行函数')}())
typeof和instanceof
typeof是用来判断变量的数据类型的,它能够帮助我们判断一下几种数据类型:number,string,boolean,object,function,undefine,symbol,它的返回值是字符串,告诉我们是什么类型的,但是针对null,array这种它是判断不出来的,都会返回object。
instanceof主要是用来判断变量是否属于一个对象的实例,返回的是布尔值
这里补充一下,还有一个判断类型的 方法,Object.prototype.toString.call(变量),这个方法可以返回[object 变量类型]
顺带提一下,数据类型有两种:
基础数据类型:
number、string、boolean、null、undefined。
引用数据类型:
object(Array、function、json、date)不属于基础数据类型的变量,类型都是object。
bind实现
bind是用来绑定上下文的,强制的把函数的执行环境绑定到目标作用域中,其作用于call、apply相似,与之不同的是,不会立即执行,且会返回函数
arguments是一个类数组对象,在所有的函数中,除了箭头函数,都存在这个对象,可以通过它获取到函数的参数。当然他也可以被转化为真正的数组:提供四种方法参考:
1. var args = Array.prototype.slice.call(arguments)
2. var args = [].splice.call(arguments)
3. var args = Array.from(arguments)
4. var args =[... arguments]
bind的实现原理:
Function.prototype.bind = function(oThis){
if(typeof this !== 'function'){//检测对象类型不是函数抛出错误
throw new TypeError('被绑定的对象需要是函数')
}
var self = this//存储当前this
var args = [].slice.call(arguments, 1)//取出函数的参数,从第二位开始取
var func = function(){}//定义一个函数
fBound = function(){
var bindArgs = [].slice.call(arguments)
// instanceof用来检测某个实例对象的原型链上是否存在这个构造函数的prototype属性,
//this instanceof func === true时,说明返回的fBound被当做new的构造函数调用,
//此时this=fBound(){},否则this=window, 如果是的话使用新创建的this代替硬绑定的this
return self.apply(this instanceof func ? this : oThis, args.concat(bindArgs))
}
//维护原型关系
if(this.prototype){
func.prototype = this.prototype
}
//使fBound.prototype是func的实例,返回的fBound若作为new的构造函数,新对象的__proto__就是func的实例
fBound.prototype = new func()
return fBound
}
call和apply
call和apply都可以改变this指向,都要求只有函数可以调用该方法
区别:
传参方式不同
call的第一个参数是对象,其他参数将会被作为函数的参数传进去
apply只接收两个参数,第一个参数是对象,第二个参数是数组,数组内元素将会被作为参数一个个传进去
第一个参可以为null,此时这个对象指向window
funa.call(obj,1,2,3)//this指向的是obj,传入的参数是1,2,3
funa.call(obj,[1,2,3])//this指向的是obj,传入的参数是[1,2,3],undefine
funa.apply(obj,[1,2,3])//this指向的是obj,传入的参数是1,2,3
柯里化
把使用多个参数的一个函数,转化为多个使用一个参数的函数
// 实现一个add方法,使计算结果能够满足如下预期:
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;
function add() {
// 第一次执行时,定义一个数组专门用来存储所有的参数
var args = Array.prototype.slice.call(arguments);
// 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
var adder = function() {
args.push(...arguments);
return adder;
};
// 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
adder.toString = function () {
//reduce:数组中的元素依次执行回调函数
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
垃圾回收机制
在V8引擎中,将内存分为了新生代和老生代两部分。
一般情况下,新生代的存活时间比较短,老生代的存活时间比较长且数量多。
新生代内存中使用GC算法,老生代使用的标记清除算法和标记压缩算法。
新生代将内存分为两块空间,FROM和TO,这两块空间一定是有一块被用的,有一块是空闲的。
新分配的对象会先进入FROM空间,当From空间满了以后,新生代GC算法启动,就会向TO空间进行复制,在复制过程中会对对象进行检测,删除失活对象,当所有对象都被转移到TO空间时,再把两个空间互换,至此一次垃圾回收完成
老生代就比较多内容了,有很多空间,有两种情况会进入到老生代空间,第一种就是已经经历了一次GC算法的对象,另一种是TO空间的对象占比大小超过25%。
在老生代空间中出现以下情况会启动标记清除算法:
1.某一块空间没有分块
2.空间中对象超过一定限制
3.空间不能保证新生代对象进入老生代空间时
标记清除算法:遍历一遍堆中的对象,标记出活的对象,然后再将没有标记的对象清除,这个过程会非常耗时间。
标记压缩算法:清除对象后可能造成堆内存产生碎片,当碎片产生足够多时,触发压缩算法,此时将会把活的对象统一向一端移动,然后清理掉不需要的内存空间
浮点数精度
在计算时,十进制的小数会被转换为二进制进行计算,此时浮点数用二进制表示是无穷的。而js的64位双精度浮点数的小数部分最多可以有53位,此时计算0.1+0.2后,由于位数截断,再转换为十进制时就比0.3大一点,由此产生误差
new操作符
new操作其实做了四个步骤
1.创建一个空的对象;
2.设置空对象的__proto__指向构造函数的prototype,用来继承构造函数上的原型公有属性和方法;
3.调用构造函数,将构造函数中的this改变为空对象的this,继承构造函数的属性;
4.返回一个对象。
用代码实现一遍如下:
function myNew(fun){
return function(){
//创建一个空对象,并把__proto__指向构造函数的prototype
let obj = {
__proto__ : fun.prototype
}
//改变构造函数的this指向空对象的this
fun.call(obj,...arguments)
//返回新的对象
return obj
}
}
function Person(name,age){
this.name = name;
this.age = age;
}
let child = myNew(Person)('nike',20)
console.log(child)
事件循环机制
JS是单线程的:
由于Js存在操作Dom元素的情况,如果他是多线程的话,有可能在一个线程中删除了某个Dom元素,在另一个线程中又会对这个Dom元素进行操作,这个时候就会出现报错,所以JS在设计时就被设计为单线程的。
JS是非阻塞的:
既然是单线程他就应该是阻塞的,但是在JS中为了使程序运算效果更快,引入了Event Loop,实现了非阻塞性。
加入了EventLoop之后,js的事件队列中就有主线程,微任务,宏任务,这三个队列。
由于js是在script标签中编写的,而script又被定义为宏任务,所以
主线程就是当前执行的线程,从页面的第一个script开始,建立主线程,一直往下走
宏任务:
script
setInterval
setTimeout
PostMasge
I/O
微任务:
new Promise.then()
MotationObserver
当主线程向下执行时,如果遇到微任务,就把这个任务放到微任务队列,遇到宏任务就放在宏任务队列,然后继续向下执行,直到主线程执行完毕,优先向微任务队列中提取任务加到主线程,如果微任务队列中没有任务,则在宏任务中提取任务到主线程。换句话说,每一次主线程执行完毕要去提取队列时,都先提取一遍微任务,由于这些都是队列,所以遵循的原则是先进先出。
看一个例子:
console.log('start')
setTimeout(function() {
console.log('setTimeout')
}, 0)
Promise.resolve().then(function() {
console.log('promise1')
}).then(function() {
console.log('promise2')
})
console.log('end')
//输出顺序:start、end、promise1、promise2、setTimeout
分析上面的例子,首先全部代码压入执行栈。
打印start;
遇到setTimeout,放入宏任务队列;
遇到Promise,放入微任务队列;
打印end;
主线程走到底了,去微任务拿到Promise事件;
打印promise1;
遇到第二个promise.then,放到微任务队列;
主线程又走到底了,去微任务拿到Promise事件;
打印promise2;
主线程又走到底了,去微任务拿不到事件,去宏任务拿到setTimeout事件;
打印setTimeout;
再看一个相对复杂的例子:
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
//最后输出顺序:script start、async1 start、async2、promise1、script end、async1 end、promise2、setTimeout
promise原理
promise是用来异步计算的,主要作用就是把异步操作队列化,按照期望的顺序去执行,返回预期结果;
使用时可以在对象之间传递和操作promise。
同步:类似队列,先进先出,不允许同时执行其他操作,需要等上一个操作执行完毕才能走下一个事件。
异步:可以同时运行多个事件,此时返回数据结果与顺序无关。
promise可以解决的问题:回调地狱,比如上一个请求的输出,将会作为下一个请求的输入,不可倒序计算。
//回调地狱演示
setTimeout(() => {
console.log('1');
setTimeout(() => {
console.log('2');
setTimeout(() => {
console.log('3');
setTimeout(() => {
console.log('4');
}, 1000);
}, 1000);
}, 1000);
}, 1000);
promise有两个重要关键词:resolve:把状态从pending转为fulfilled;reject:把状态从pending转为rejected;
状态只有pending–>fulfilled或者pending–>rejected,且状态一经改变就无法再次变化。
promise有三个状态:pending、fulfilled、rejected;待定(初始状态)、实现(操作成功)、否决(操作失败)
promise在调用时,有两种方式:
1、每次都执行promise.then((data)=>{}),这种情况下,每一次then都会使用resolve传过来的值,这就是promise具有状态缓存的特性。
2、链式调用,promise.then().then().then(),这种情况下,如果上一个then有return值,那当次then接收到 的就是return来的参数,如果没有return,接收到的就是undefined。
//创建一个promise
let promise = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve('success')
},1000)
})
//显式调用
promise.then((data)=>{console.log('resolve:'+data)});//resolve:success
promise.then((data)=>{console.log('resolve:'+data)});//resolve:success
promise.then((data)=>{console.log('resolve:'+data)});//resolve:success
//链式调用
promise.then((data)=>{console.log('resolve:'+data)})//注意此处then里面是一个函数,才会正确执行,由于没有return,下一次的then,将会得到resolve(undefined)
.then(5)//then里面是其他数据类型,值穿透,此句代码在本次实例中无效
.then((data)=>{console.log('resolve:'+data)})//接收上一次的resolve,由于没有return,所以接收到undefined————resolve:undefined