【前端基础知识复习】

原型链

  每一个构造函数都会有一个原型对象,可以使用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
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值