针对性深入javascript和常见问题

201 阅读11分钟

一、原型链

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、变量对象

当进入执行上下文时,这时候还没有执行代码,

变量对象会包括:

  1. 函数的所有形参 (如果是函数上下文)

    由名称和对应值组成的一个变量对象的属性被创建

    没有实参,属性值设为 undefined

  2. 函数声明

    由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建

    如果变量对象已经存在相同名称的属性,则完全替换这个属性

  3. 变量声明

    由名称和对应值(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,否则返回resreturn 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))
	}
}

十七、generator原理