前端学习之路-JavaScript高级
1、函数中this指向问题
-
默认绑定(指向window)
-
普通函数的独立调用
function foo (){ console.log("foo函数this指向",this); } foo(); -
函数定义在对象中,但是独立调用
var obj = { bar:function(){ console.log("bar函数this指向",this); } } var baz = obj.bar; baz(); -
严格模式下,独立调用函数中的this指向为undefined
-
-
显式绑定
不希望对象内部包含这个函数的引用,但又希望在这个对象上强制调用函数时,采用显式绑定.
-
call
-
apply
function foo (name,address){ console.log("foo函数this指向",this); console.log("打印参数",name,address); } //apply //第一个参数:绑定this //第二个参数:传入函数需要的实参,以数组的形式 foo.apply("apply",["Kobe","美国"]); //call //第一个参数:绑定this //第二个参数:传入函数需要的实参,以多参数的形式 foo.call("call","james","美国"); -
bind
function foo(name,age,address){ console.log("foo函数this指向",this); console.log("打印参数:",name,age,address); } var obj = { height:2.0 } var bar = foo.bind(obj,"kobe",43) //参数会自动补全到形参上 bar("美国");//this -> obj
-
-
隐式绑定(指向调用对象)
var obj = { bar:function (){ console.log("bar函数this指向",this); } } obj.bar(); -
new绑定
- 创建一个空对象
- 将this指向这个空对象
function foo(){ console.log("foo函数this指向",this); } new foo(); -
this绑定优先级比较
- new > bind > apply/call > 隐式绑定 >默认绑定
2、箭头函数
- 不会绑定this、arguments属性(箭头函数中的this会去上层作用域查找指向,es6之后采用剩余函数代替arguments)
- 不能作为构造函数使用(不能new)
3、this的面试题
var name = "window";
function Person(name){
this.name;
this.obj = {
name:'obj',
foo1: function () {
return function () {
console.log(this.name);
}
},
foo2: function () {
return () => {
console.log(this.name);
}
}
}
}
var person1 = new Person();
var person2 = new Person();
person1.obj.foo1()();//window person1.obj.foo1()返回的值为函数,后面()相当于函数独立运行
person1.obj.foo1.call(person2)();//window person1.obj.foo1.call(person2)返回的值为函数,后面()相当于函数独立运行
person1.obj.foo1().call(person2);//person2 person1.obj.foo1()返回值为函数,后面调用call改变this指向为person2
person1.obj.foo2()();//obj person1.obj.foo2()返回箭头函数,箭头函数没有this指向,向上层作用域查找,foo2的this指向obj
person1.obj.foo2.call(person2)();//person2 person1.obj.foo2调用call将this指向person2,person1.obj.foo2.call(person2)的返回值为箭头函数,箭头函数没有this指向,向上层作用域查找即person2
person1.obj.foo2().call(person2);//obj person1.obj.foo2()的返回值为箭头函数,箭头函数不能通过call改变this指向,向上层作用域查找即obj
var name = "window";
var person = {
name:'person',
sayName: function () {
console.log(this.name);
}
}
function sayName(){
var sss = person.sayName;
sss(); //window 函数的独立执行即默认绑定
person.sayName();//person 隐式绑定
(person.sayName)();//person 隐式绑定
(b = person.sayName)();//window 间接函数引用,默认绑定
}
sayName();
4、浏览器原理
- 页面渲染流程
1、解析HTML,然后构建DOM Tree。
2、解析过程中遇到link元素会下载css文件(不会影响DOM解析),然后解析CSS文件生成CSSOM Tree。
3、构建出DOM Tree和CSSOM Tree,两者结合构建Render Tree
- link元素为加载完成会阻塞Render Tree的构建
- Render Tree和DOM Tree并不是一一对应的,display:none的元素是不会出现在Render Tree中的
4、在Render Tree 上运行layout以计算每个节点的几何体,确定节点的宽高和位置信息
5、将每个节点paint到屏幕上,将元素的可见部分进行绘制,比如文本、颜色、边框、阴影、替换元素。
-
回流
1、回流reflow或者重排:第一次确定节点的大小和位置为layout,之后对节点的大小和位置进行修改成为回流。
2、引起回流的情况:
- DOM结构发生改变
- 布局layout改变
- 窗口大小改变
- 调用getComputedStyle方法获取大小和位置信息
-
重绘
1、引起重绘的情况:
- 修改背景色、文字颜色、边框颜色、样式等
-
composite合成
绘制的过程可以将布局后的元素绘制到多个合成层中,每个合成层单独渲染,这是浏览器优化的一种手段,默认情况下,标准流中的内容是被会绘制到同一图层,而一些特殊的属性,创建一个新的合成层.常见的一些属性:
- 3D transforms
- video、canvas、iframe
- opacity 动画转换时
- position:fixd
- animation或transition设置了opacity、transform
分层可以提高性能但是以牺牲内存管理为代价
-
script元素与页面解析的关系
在解析HTML过程中,遇到script元素会停止解析HTML并且阻塞构建DOM Tree,然后下载script代码并运行,执行结束后,才会继续解析HTML,继续构建DOM Tree
- defer属性:不用等待脚本下载,继续解析HTML,构建DOM Tree,脚本下载完成后,等待DOMTree构建完成,在DOMContentloaded事件之前先执行defer中的代码,多个defer的脚本可以保持顺序执行
- async属性:不会阻塞解析HTML和构建DOM Tree,但是不能保证顺序执行,因为是独立下载和执行的,不会等待其他脚本,而且不会保证在DOMContentLoaded事件之前或者之后执行
5、V8引擎原理
V8引擎有C++编写,可以独立运行,也可以嵌入到任何C++的程序中.
- parse模块将代码转换为AST(抽象语法树)
- 函数没有调用是不会转为AST的
- lgnition是解释器,将AST转换为字节码
- 会收集TurboFan优化所需要的信息
- 函数只调用一次,Ignition会解释执行字节码
- TurboFan是编译器,可以将字节码编译成CPU可直接运行的机器码
- 一个函数被多次调用,会被标记为热点函数,由TurboFan编译成机器码执行,提高效率
- 后续执行过程中类型发生转变,那么机器码也会逆向转为字节码
6、JS执行原理
首先会创建一个执行上下文栈,每一个执行上下文会在栈内存中创建一个VO对象与之相关联.当执行上下文为全局时,会在堆内存中创建一个GO对象,此时VO即GO.当执行上下文为函数时,会在堆内存中创建一个AO对象,此时VO即AO
-
作用域和作用域链
- 当进入到一个执行上下文时,执行上下文会关联一个作用域链
- 作用域链是一个对象列表,用于变量标识符的求值
var n = 100; function foo1(){ console.log(n); } function foo2(){ var n = 200; console.log(n); foo1(); } foo2(); //打印结果 200 100 //关注函数定义的位置而不是调用的位置
7、垃圾回收机制
-
常见的GC算法
1、引用计数
描述:当一个对象有一个引用指向它时,其引用就+1,若引用为0,对象就被销毁。
缺掉:产生循环引用,占用内存。
2、标记清除(v8采用)
描述:设置一个根对象,垃圾回收器会定期从根对象开始查找有引用到的对象,对于没有引用的对象就是不可用对象,进行清除
3、标记整理
描述:与标记清楚相似,不同的是,回收期间会将保留下来的对象汇集到连续的内存空间中,避免内存碎片化。
4、分代收集
描述:对象被分为新和旧,长期存活的对象被检查的次数会减少
8、闭包
定义:一个普通的函数function,如果它可以访问外层作用域的自由变量,那么这个函数就是一个闭包.
9、函数增强
1、函数对象属性
-
name 获取函数名
-
length 获取参数个数(rest参数是不计入参数个数的)
function demo(m,n){ ...something } demo.name; demo.length;
2、函数中的arguments使用
-
arguments是类数组对象,用于传递给函数的参数。
//arguments转数组方式一(ES6) Array.form(arguments); //方式二(ES6) var newArguments = [...arguments] //方式三 var newArguments = []; for(var arg of arguments){ newArguments.push(arg); } //方式四 var newArguments = [].slice.apply(arguments); -
箭头函数不绑定arguments
-
函数的剩余(rest)参数(替代arguments)
function foo (m,n,...args){
console.log(m,n);
console.log(args);
}
foo(1,2,3,5,5);
-
纯函数
- 确定的输入,一会产生确定的输出
- 函数在执行过程中,不能产生副作用(修改全局变量、参数或者外部存储)
-
柯里化
只传递给函数一部分参数来调用它,让他返回一个函数去处理剩余参数,这个过程称之为柯里化,比如:
//转化前 f(a,b,c) //转化后 f(a)(b)(c) a => b => c=>{}//箭头函数柯里化写法 -
with和eval函数
//with增加作用域 var obj = { var message = "hello world" } with(obj){//obj相当于作用域链中新插入的作用域 console.log(message); } //eval函数可以将传入的字符串当做JavaScript代码执行,然后将最后一句执行语句的结果作为返回值 var evalStr = `var message = "hello world";console.log(message);` eval(evalStr); eval函数的缺点: 1、可读性差 2、字符串在执行过程中如果被刻意篡改,容易造成被攻击风险 3、eval的执行必须经过JavaScript解释器,不能被JavaScript引擎优化 -
严格模式
- 无法意外的创建全局变量
- 引起静默失败(不报错也没效果)的赋值操作抛出异常
- 不允许函数参数有相同的名称
- 不允许使用with
- eval不再为上层引用变量
10、对象增强
1、描述符
-
数据属性描述符
var obj = { name:"kobe", age:18 } Object.defineProperty(obj,"name",{ //属性是否删除 configurable:true, //属性是否可枚举 enumerable:true, // 属性是否能写 writable:ture }) -
存取属性描述符
Object.defineProperty(obj,"name",{ set:function(value){ //用于监听设置值 }, get:function(){ //用于监听获取值 } //两者默认值都是undefined })
11、原型(ES5)
1、对象中的原型
var obj = {
name:"kobe",
age:18
}
console.log(obj.__proto__);//不建议使用,此调用为浏览器自行实现
console.log(Object.getPrototypeOf(obj));
//需要获取一个属性的值时,会优先在自己的对象中查找,如找到则返回,若无则会在原型中查找
2、函数中的原型
function Foo (){}
//函数作为对象看待时,__proto__此为隐式原型
console.log(Foo.__proto__);
console.log(Object.getPrototypeOf(foo));
//函数作为函数时,prototype此为显式原型
console.log(Foo.prototype);
new Foo()
//首先生成一个空对象
//将函数中的this指向这个空对象
//将函数的prototype(显式原型)赋值给空对象的__proto__(隐式原型)
3、原型中的constructor
function Foo(m,n){
}
var foo1= new Foo(1,2);
//下面两者为同一值
console.log(Foo.prototype.constructor);
console.log(foo1.__proto__.constructor);
console.log(foo.prototype.constructor.name);
4、重写原型对象
创建一个函数,同时创建它的prototype对象,这个对象会自动获取constructor属性,此时的constructor属性默认指向的是Object构造函数,而不是新建的函数。如需更改可通过属性描述符Object.definePrototype()的value属性赋值.
12、原型链(ES5)
1、默认原型链
//{}对象字面量的本质
var obj = {};
//相当于
//var obj = new Object()
//obj.__proto__ === Object.prototype
2、利用原型链实现继承
-
方式一:父类的原型直接复制给子类的原型
- 缺点父类和子类共享一个原型对象,修改其中一个。另外一个也会随之改变
-
方式二:创建一个父类的实例化对象,将实例化对象赋值给子类的原型
-
方式三:
function F (){} F.prototype = Sup.prototye; Sub.prototype = new F(); -
方式四:寄生组合式继承
var obj = Object.create(Sup.prototype); Sub.prototype = obj; //优化并封装为工具函数 function inherit(subType,supType){ subType.prototype = Object.create(SupType.prototype); Object.definePrototype(subType.prototype,"constructor",{ enumerable:false, configrable:true, writable:true, value:supType }); }
3、利用构造函数继承属性
function Sup(a,b){
this.a = a;
this.b = b;
}
function sub(a,b,c){
Sup.call(this,a,b);
this.c = c;
}
4、原型继承关系
5、类方法和实例方法
function Foo (){};
//实例方法 只能通过实例化对像调用
Foo.prototype.baz = function(){};
//类方法 可通过类直接调用
Foo.ffo = function(){};
13、class类(ES6)
1、构造方法和实例化
class Foo {
//通过new关键字创建对象时,会自动调用constructor函数
constructor(m,n){
this.m = m;
this.n = n;
}
}
var foo1 = new Foo(1,2);
console.log(foo1.m,foo1.n);//1 2
console.log(foo1.__proto__ === Foo.prototype);//true
2、静态方法(类方法)
class Foo{
//实例方法 只能通过实例化对像调用
baz(){}
//静态方法(类方法) 可通过类直接调用
static ffo(){}
}
3、继承
class Sup{
constructor(m,n){
this.m = m;
this.n = n;
}
ffo1(){}
}
class Sub extends Sup{
constructor(m,n,a,b){
//this.m = m;
//this.n = n;
super(m,n);//在this之前用
this.a = a;
this.b = b;
}
//ffo1(){} 从父类继承
ffo2(){}
}
4、对象字面量增强写法
var obj = {
var name = "kobe";
var age = 18;
sayName(){}//即sayName :function(){}但是不能写成箭头函数,this指向会改变
return {
name,//name:name
age //age:age
}
}
5、对象和数组的解构
//数组结构 有严格的顺序
var names = ["qqq","ccc","rrrr"];
//方式一 不需要可以逗号空开
var [name1,,name3] = names;
console.log(name1,name3);
//方式二
var [name1,...args] = names;
console.log(name1,args);
//对象解构
var obj = {
name:"kobe",
age:18
}
//方式一
var {name,age} = obj;
console.log(name,age);
//方式二 重命名
var {name:newName,age} = obj;
console.log(newName,age);
14、let、const(ES6)
//var 1、重复声明变量2、变量可以作用域提升(可以在声明前访问此变量即作用域提升)
//let const 1、不能重复定义变量且变量不能作用域提升2、const声明的变量不能修改。
//暂时性死区:从块级作用域顶部一直到声明变量完成之前,这块变量区即暂时性死区
15、函数默认参数(ES6)
function foo (m,n){
//1、之前不严谨的写法
//方式一
m = m ? m : "默认值";
//方式二
m = m || "默认值";
//严谨的写法
// 方式一
m = (m === undefined || m === null) ? "默认值" : m;
//方式二(ES6之后新语法优化方式一)
m = m ?? "默认值";
}
//默认参数简化写法
//有默认参数的形参以及后面的形参不会计入length之内,默认参数的形参最好写在参数最后,若有剩余参数则其写最后.
function foo (m,n = "默认值",...args){
}
16、Proxy代理
1、监听对象属性的操作
//方式一(VUE2中响应式数据的基本原理)
//设计弊端:
//1、违反了Object.defineProperty()中的存储数据描述符设计初衷
//2、存在局限性,只能对已有属性进行监听,新增和删除操作无法监听
var obj = {
name:"cobe",
age:18,
address:"美国"
}
const keys = Object.keys(obj);
for (const key of keys) {
let value = obj[key];
Object.defineProperty(obj, key, {
set:function(newValue){
console.log(`监听:给${key}设置了新值:${newValue}`);
value = newValue;
},
get:function(){
console.log(`监听:获取了${key}的值:${value}`);
}
})
}
obj.name = "james";
obj.age;
//方式二:使用proxy代理对象,使用proxy对象的set和get捕获器.
const objProxy = new Proxy(obj,{
set: function (target,key,newValue){
console.log(`监听:设置了属性${key}的值:${newValue}`);
target[key] = newValue;
},
get: function (target,key){
console.log(`监听:获取了属性${key}的值:${target[key]}`);
}
});
objProxy.name = "james";
objProxy.age;
objProxy.tel = 123;//设置新属性
17、Reflect反射
因为ECMA之前对对象设计缺少规范性,将很对API放到了Object上面,造成了Object臃肿,而且一些方法放在其上不在合适,所以新增了Reflect,将一些操作移交至此.
//reflect和proxy结合使用
const objProxy = new Proxy(obj,{
set: function (target,key,newValue){
console.log(`监听:设置了属性${key}的值:${newValue}`);
//target[key] = newValue;
//采用reflect的方法返回boolean值,在操作失败时可以做判断,并不用再直接操作原对象
const isSuccess = Reflect.set(target,key,newValue);
if(!isSuccess){
throw Error(`设置属性${key}为${newValue}失败!`);
}
}
});
18、promise异步处理
1、promise的基本实现
//promise制定了异步处理的规范,解决了之前开发中异步处理代码封装复杂和形形色色的现象
function execode (dalay){
const promise = new Promise((resolve,reject) => {
setTimeout(() => {
if(dalay > 0){
let count = 0;
for (let i = 0; i < dalay; i++) {
count += i;
}
resolve(count);
}else{
reject(`${dalay}有问题`)
}
},3000);
});
return promise;
}
// const promise = execode(0);
// promise.then(res =>{
// console.log("执行成功",res)
// });
// promise.catch(rej =>{
// console.log("执行失败",rej);
// });
execode(100).then(res =>{
console.log("执行成功",res)
}).catch(rej =>{
console.log("执行失败",rej);
});
2、promise的三种状态:
- 待定(pending):初始状态
- 已兑现(fulfilled):操作成功完成
- 已拒绝(reject):操作失败
3、resolve
- 参数为普通值,那么这个值会作为then回调的参数
- 参数为promise,那么新promise会决定原promise状态
- 参数为对象并且该对象实现了then方法,执行该then方法,根据该then结果决定promise状态
4、异步处理解决方案(网络请求为例)
//需求:向服务器发送三次网络请求,且上次的请求结果需要用于下次请求参数
function requestData(url){
return new Promise((resolve,reject) => {
setTimeout(() => {
resolve(url);
},2000);
});
}
//方式一
//形成回调地域
// function getData(){
// requestData("aaa").then(res1 =>{
// requestData(res1+"bbb").then(res2 =>{
// requestData(res2+"ccc").then(res3 =>{
// console.log(res3);
// })
// })
// })
// }
//方式二
function getData(){
requestData("aaa").then(res1 =>{
return requestData(res1+"bbb");
}).then(res2 =>{
return requestData(res2+"ccc");
}).then(res3 =>{
console.log(res3);
})
}
//方式三 生成器函数实现
function* getData(){
const res1 = yield requestData("aaa");
console.log("res1",res1);
const res2 = yield requestData(res1+"bbb");
console.log("res2",res2);
const res3 = yield requestData(res2+"ccc");
console.log("res1",res3);
}
const generator = getData();
generator.next().value.then(res1 =>{
generator.next(res1).value.then(res2 =>{
generator.next(res2).value.then(res3 =>{
generator.next(res3)
})
})
})
//方式四 使用async 和 await语法糖(ES8)
async function getData(){
const res1 = await requestData("aaa");
console.log("res1",res1);
const res2 = await requestData(res1+"bbb");
console.log("res2",res2);
const res3 = await requestData(res2+"ccc");
console.log("res1",res3);
}
getData();
19、async 和 await
1、使用async关键字修饰的函数即异步函数,异步函数的返回值为promise对象,异步函数中的错误会被catch捕获
async function foo (){
//第一种情况:普通返回值,会被当做resolve参数回调
//return "ning"
//第一种情况:那么foo().then()会根据new Promise的返回情况执行
//return new Promise(res =>{})
//第一种情况:返回了一个thenable对象,那么foo().then()会根据thenable对象中then函数的返回情况执行
return {
then:function (resolve,reject){
}
}
}
foo().then(res =>{
console.log("res",res)
})
2、await
-
在async函数中使用
-
通常await后跟表达式,该表达式的返回值为promise对象
-
await会等到promise状态变为fufilled时,继续执行下面代码
20、进程和线程
1、描述
- 进程:一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程。
- 线程:进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程。、
2、JavaScript线程
浏览器是多进程的,每一个tab页都是一个单独的进程,如果采用共用进程只要某一个卡死就会影响其他。每一个进程中有多个线程,其中一个就是JavaScript线程。JavaScript代码是在单个线程中执行的。 单线程同一时间只能做某一操作,那就意味着某些耗时的操作必然会阻塞代码的执行,比如网络请求、定时器等。
3、事件循环
如果在执行代码时遇到阻塞操作,比如网络请求、定时器、DOM监听等,就将这些操作放入一个事件队列中,等到其他无阻塞操作的代码执行完成后,按照队列先进先出的方式执行事件队列中的操作,将这个过程叫做事件循环。
4、宏任务和微任务
- 宏任务:ajax、setTimeout、setInterval、DOM监听、UI Render等
- 微任务:promise的then回调、Mutation Obsever API、queueMicrotask()等
注意:主代码先执行,在执行任何一个宏任务之前,先检查微任务队列是否有任务需要执行
21、代码执行顺序面试题
1、面试题一
setTimeout(function () {
console.log("setTimeout1");
new Promise(function (resolve) {
resolve();
}).then(function () {
new Promise(function (resolve) {
resolve();
}).then(function () {
console.log("then4")
});
console.log("then2");
});
});
new Promise(function (resolve){
console.log("promise1");
resolve();
}).then(function (){
console.log("then1");
});
setTimeout(function () {
console.log("setTimeout2");
});
console.log(2);
queueMicrotask(() => {
console.log("ququeMicrotask1");
});
new Promise(function (resolve){
resolve();
}).then(function () {
console.log("then3");
});
//执行顺序
// promise1
// 2
// then1
// queueMicrotask1
// then3
// setTimeout1
// then2
// then4
// setTimeout2
2、面试题二
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
22、防抖函数、节流函数以及深拷贝函数
1、防抖函数的基本实现
// 防抖函数的基本实现,防抖函数的基本原理即触发事件时,不会立即响应,会做相应的延迟响应,如果频繁触发事件,那么响应也会根据触发事件进行顺延.
function jydebounce (fn,delay){
let timer = null;
const _debounce = function (){
timer = setTimeout(() => {
fn();
time = null;
},delay);
}
return _debounce;
}
2、节流函数的基本实现
// 节流函数的基本实现,节流函数的基本原理即频繁的触发事件,但是不会进行实时响应,而是周期性的触发响应
const jythrottle = function (fn,interval){
let startTime = 0;
const _throttle = function (){
let nowTime = new Date().getTime();
let waiteTime = interval - (nowTime - startTime);
if(waiteTime <= 0){
fn()
startTime = nowTime;
}
}
return _throttle;
}
3、深拷贝函数的基本实现
function isObject (value){
const valueType = typeof value;
return (value !== null) && (valueType === "object" || valueType === "function")
}
function deepCope (originValue){
// 判断参数是否为对象,不是则直接返回
if(!isObject(originValue)){
return originValue;
}
const newObj = {};
for (const key in originValue) {
// 递归实现深度拷贝
newObj[key] = deepCope(originValue[key]);
}
return newObj;
}