文章目录
一、浏览器工作原理和V8引擎
前言:
- JS应用区域
web开发
移动端开发
小程序开发
桌面应用开发
后端开发(node)- TypeScript会取代JavaScript吗?
不会,JS本身没有对变量,函数参数等类型进行限制,这样会有安全隐患,TS是JS的一个高级,因为TS有类型检测或者其他的优化,最终项目的代码还是TS转换为JS才能真正运行的。- JS语言类型(解释型的弱类型动态语言)
静态语言:在变量声明之前需要先定义变量类型。我们把这种在使用之前就需要确认其变量数据类型的称为静态语言
动态语言:在声明变量之前不需要先定义变量类型。我们把这种在使用之前不需要确认其变量数据类型的称为动态语言。
解释型语言:边读代码,边去解释再去做执行
编译型语言:在运行整个文件之前,先编译成可执行文件
弱/强类型语言:通常把会偷偷转换的操作称为隐式类型转换。而支持隐式类型转换的语言称为弱类型语言,不支持隐式类型转换的语言称为强类型语言。
JS:
● 弱类型,意味着你不需要告诉 JavaScript 引擎这个或那个变量是什么数据类型,JavaScript 引擎在运行代码的时候自己会计算出来。
● 动态,意味着你可以使用同一个变量保存不同类型的数据1.JS是单线程
- JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事
- JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。如果多个线程一起操作同一个DOM就会引发很多问题。
- JS解决单线程的弊端:同步任务和异步任务,异步任务放在任务队列中,同步任务是主线程上执行的任务。只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入”任务队列”(task queue)的任务,只有”任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
同步异步任务执行机制
(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。
事件循环:主线程从”任务队列”中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)
1.浏览器内核
- 实质上:浏览器的排版引擎、页面渲染引擎、样板引擎
- 分类:
- Gecko:早期被Netscape和Mozilla Firefox浏览器浏览器使用;
- Trident:微软开发,被IE4~IE11浏览器使用,但是Edge浏览器已经转向Blink;
- Webkit:苹果基于KHTML开发、开源的,用于Safari,Google Chrome之前也在使用;
- Blink:是Webkit的一个分支,Google开发,目前应用于Google Chrome、Edge、Opera等
2.浏览器渲染过程
- 1.解析html文件 css文件
- 2.DOMtree stylerules 此时有JS代码控制DOM该怎样去操作(JS引擎)
- 3结合在一起(形成rendertree)-layout布局(布局结构和大小) 重排(回流)
- 4.渲染到页面 -painting绘制 重绘
1.面试题
网页中输入URL会发生什么
3.JS引擎
- 来源:高级编程语言(浏览器,node) - 机器(cpu执行)指令 (对应的语言引擎来实现翻译成CPU指令)
- 类型:
- JavaScriptCore:WebKit中的JavaScript引擎,Apple公司开发;
- V8:Google开发的强大JavaScript引擎,也帮助Chrome从众多浏览器中脱颖而出;
JS引擎和浏览器渲染过程关系?
- webkit(浏览器渲染引擎) = webcore(HTML解析布局渲染)+ jscore (JS引擎,JS解析)
4.V8引擎
过程:
- js代码 >pase 词法分析,语法分析> AST抽象语法树 >解释为字节码 > 运行结果
- js代码被解析的时候:创建成一个对象(global对象==date、setTimeout、setInterval、window对象 :this)
- V8内部有执行上下文栈(函数调用栈)> 预解析 > 变量提升>作用域 >暂时性死区(constlet,未声明之前调用是不可以的)
- JS基本数据类型内容的分配在执行时,直接在栈中分配
- 复杂数据类型,放在堆中,并且将这块空间的指针返回值变量引用
二、内存管理及作用域提升
1.内存管理
-
概念:不管什么语言,在代码的执行过程中都是需要给它分配内存的,不过有些编程语言(C,C++)需要手动进行内存管理,某些语言(Java,JavaScript,Python等)会自动进行内存管理
-
不管以什么样的方式管理内存,内存的管理都会有如下的生命周期
- 1.分配申请所需的内存
- 2.使用分配的内存
- 3.不需要则JS垃圾回收机制会进行回收
-
JS中的分配内存
-
1.基本数据类型在执行时,直接在栈空间进行分配
-
2.复杂数据类型会在堆中开辟一块新的空间,并且将这块空间的指针返回值遍历引用
2.JS垃圾回收机制
没有垃圾回收机制的弊端:1.手动管理方式非常低效,影响逻辑代码的效率2.不小心会产生内存泄露
JS中的垃圾回收机制(GC):
-
1.GC算法引用计数
当有一个对象有一个引用指向它的时候,那么这个对象的引用加1,当一个对象的引用为0的时候,这个对象就会被销毁。但是这个机制会产生循环引用,造成内存泄露 -
2.GC算法标记清楚
-
设置一个根对象,垃圾回收器会定期从这个根开始,找到所有从根开始有引用到的对象,对象中没有引用的对象就是不可用对象(这个算法很好的解决循环引用问题)
3.作用域提升
1.JS引擎在执行JS代码的过程= 函数调用函数=闭包(内存)
复杂数据类型对应的空间中存储的不是直接是复杂数据类型,而是内存地址
函数的父级作用域链在内存中跟调用位置没有关系的,跟声明位置有关,是在定义位置例如全局定义时,父级就是全局有关
- 1.1.在堆中先创建一个全局对象,这个全局对象包括(date,setTimeout,setInter,funcbtion:undefind,name:undefind)等,初始化的时候都是undefined
- 1.2在调用栈中去创建一个全局对应的执行上下文,开始依次执行,
- 1.3如果遇见对应的复杂数据类型,去(VO:是本身在堆中的初始化创建的对象,本身VO没有+根据作用域链(父级)去查找,跟函数的调用位置是没有关系的)查找执行上下文之后进行赋值编译,此时这个复杂数据类型就保存的是地址,就还会根据这个地址再去堆中找到开辟新的这个复杂数据类型的空间(包含父级作用域+这个复杂数据类型也又包括自己本身的属性和方法也就是执行体)
- 1.4当这个复杂数据类型调用栈中又会在调用栈中去了这个复杂数据类型的执行上下文,依次开始执行,如果在这个上下文中再遇见新的复杂数据类型(函数)存储的是地址,就会在堆中再去开辟新的空间,这样一次进行执行
- 1.5执行完一个函数的执行上下文结束之后,就会销毁以及对应的AO也会进行销毁
补充:foo bar baz代表不知道取什么名字的函数
变量声明最好不要用关键字(例如name,因为window上自己有,容易混淆)
图片:
2.变量环境和记录
VO:GO AO ECMA5之前 6之后又叫做变量环境
解释:每个执行上下文都会关联到一个变量环境中(VO),在执行代码变量和函数的声明都会被环境记录添加到变量环境中,对于函数的参数也会被记录到变量环境中。
4.作用域提升面试题
例1
var n = 100
function foo(){
n = 200 //关键是这个函数里面没有n,要根据作用域链向上找,找到父级将父级中的AO中的n更改为200
}
foo()
console.log(n); //200
例2
function foo(){
console.log(n); //undefined 和上面的区别是本身函数体拥有n值,在创建AO时已经初始化
var n = 200
console.log(n); //200
}
var n = 100
foo()
例3
关键:函数的父级作用域链在内存中跟调用位置没有关系的,是在定义位置例如全局定义时,父级就是全局有关
var n = 100
function foo1() {
console.log(n); //100
}
function foo2() {
console.log(n) //100
foo1()
}
foo2()
console.log(n); //100
例4
关键:初始化AO使函数体中的执行语句无关(例如return)
var a = 100
function foo(){
console.log(a); //undefined
return
var a = 100 //这里在执行上下文的时候会执行,因为是一开始AO中变存在赋值语句,和return无关
//console.log(a); //这里在执行上下文的时候不会执行,因为是执行语句
}
foo()
例5
function foo(){
var a = b = 100
// 上面这句代码在内存中是这样编译的,转成下面两行
var a = 100
b = 100
}
foo()
console.log(a); //undefined
console.log(b); //100
补充:在函数内部使用关键字声明的变量是函数内部的,外部无法访问,不使用关键字,则是外部可以使用(因为没有声明,则会在执行上下文的时候,向父级寻找,依次寻找则外部可以访问)
function foo(){
m =100
}
foo() //要执行函数啊,m才会向上寻找啊
console.log(m); //100
function foo(){
var m =100
}
foo() //要执行函数啊,m才会向上寻找啊
console.log(m); // undefined
三、闭包
1.前言
- 高阶函数:把一个函数作为参数或者该函数会把另外一个函数作为返回值,这个函数就是高阶函数
- function:独立的函数称之为函数
- methods:当一个函数属于一个对象时,就可以称之为这个对象的方法
- JS中函数是一等公民,可以作为参数,可以作为返回值
一等公民意味着函数的运用是非常灵活的 - 数组中的方法(高阶函数)
// 数组的filter方法创建一个新的数组,新数组中的元素通过检查指定数组中符合条件的所有元素
let num = [12,45,89]
let arr = [1,3]
// arr是当前数组的一个引用,想用也可以用,也可以不用传
let newnum = num.filter((item,index,arr)=>{
//返回的是布尔值,通过布尔值进行操作item的存入新数组
return item % 2 == 0
})
console.log(newnum);
// 数组的filter方法创建一个新的数组,新数组中的元素通过检查指定数组中符合条件的所有元素
let num = [12, 45, 89, 4, 8]
let arr = [1, 3]
// arr是当前数组的一个引用,想用也可以用,也可以不用传
// 1.filter 过滤 根据布尔值返回新数组
let newnum = num.filter((item, index, arr) => {
//返回的是布尔值,通过布尔值进行操作item的存入新数组
return item % 2 == 0
})
// console.log(newnum); //根据布尔值进行元素操作是否进入新数组
// 2.map 隐射 每一个进行处理返回一个个单独的值 返回布尔或者返回符合的值
let newnum1 = num.map((item) => {
return item % 2 == 0
})
// console.log(newnum1); //返回布尔值
// 3.forEach 迭代没有返回值数组中的每个函数都执行一次回调函数
num.forEach(function (item) {
// console.log(item);
})
// 4.find
// 返回符合测试条件的第一个数组元素值,为数组中的每个元素都调用一次函数执行:当数组中的元素在测试条件时返回 true 时, find() 返回符合条件的元素,如果没有符合条件的则返回 undefined。
let newnum2 = num.find((item) => {
return item % 2 == 0
})
console.log(newnum2);
// 5.findindex
// 方法返回传入一个测试条件(函数)符合条件的数组第一个元素位置。为数组中的每个元素都调用一次函数执行:当数组中的元素在测试条件时返回 true 时,返回符合条件的元素的索引位置,之后的值不会再调用执行函数。如果没有符合条件的元素返回 -1
let newnum3 = num.findIndex((item) => {
return item % 2 == 0
})
console.log(newnum3);
// 6.find场景运用
let data = [
{
name: 'why', age: "18" },
{
name: 'why', age: "18" },
{
name: 'HHH', age: "18" },
{
name: 'why', age: "18" },
]
let getItem = data.find((item) => {
//返回值返回第一个
return item.name == 'why'
})
console.log(getItem);
let getIndex = data.findIndex((item) => {
//返回值返回第一个
return item.name == 'HHH'
})
console.log(getIndex); //返回索引值从0开始
// 7.reduce 累加器返回一个总值
// reduce((a,b)=>a+b) 累加器,接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值。
let total = num.reduce((prevalue,curvalue)=>{
return prevalue+curvalue
})
console.log(total);
2.闭包
(图片)
- 概念:侠义一个普通的函数如果可以访问外层作用域的变量那么这个函数就是闭包,广义JS中的函数都是闭包
- 作用:记录所需变量值,减少代码有函数内部的变量一直在被外部引用就不会被删除
- 造成的问题:内存泄漏
- 解决办法:将外层作用域(某个函数)设置为空 例:fn = null即可以清除闭包
- 手写闭包打印123
代码片段 - 闭包内存泄漏
本身一个整数型对应的字节数是8byte,但是在JS中,JS引擎(V8引擎)会进行优化,一个数字整形会是4byte
3.闭包面试题:
四、this的绑定、优先级
开发中没有this其实也可以有解决方案(例如:that = this,使用变量记录this值),但是有this后会更加的方便(例如更改变量名),this本身就是一种记录,但是它是在VO中是动态进行绑定的
1.this在不同的环境下,全局指向的都不同
- 浏览器:window、
- node:{} ,因为node内部是先moudule > 加载 > 编译 >放一个函数 >执行这个函数.apply({},) 就是通过applay这个方法将this都绑定到空对象中
2.this的绑定规则
- 默认绑定
函数单独调用所在的环境,跟函数声明的区域有关,跟调用位置无关(注意和作用域链进行区分,作用域链跟声明位置有关,this绑定在默认绑定时函数声明的区域有关,跟调用位置无关因为是单独执行,没有绑定)
function foo() {
console.log(this);
}
function foo1() {
console.log(this);
foo() //
}
function foo2() {
console.log(this);
foo1() //跟调用的位置无关,因为没有绑定只和作用域有关
}
foo2()
- 隐式绑定
前提条件:对象的内部有一个对这个函数的引用,没有引用,在绑定时便会报错,正是通过这个引用才绑定到这个对象上
object.foo()这样去调用函数内部的方法,此时函数的this值就会绑定到object
但是使用变量 fn = object.foo() 再去调用这个函数 fn()此时就是默认绑定
var num= 'window'
var obj ={
num:'obj',
eating:function(){
console.log(this.num+'喝水'); //此时的this指向这个函数的父级,此时饿父级不是这个对象是window
},
runing(){
//简写方式
console.log(obj.num+"跑步");
}
}
let fn = obj.eating
fn()
- 显示绑定
- call(默认传入数据没有格式)
- applay(传入数据是一个数组)
- bind(传入参数可以是数组或者直接传入都可)但是bind可以减少call和apply多次绑定同一个对象造成的代码冗余。因为bind会返回一个回调函数,在使用多顶绑定时直接调用回调函数即可。
function foo() {
console.log(this);
}
// 1.call
foo.call(123) //将foo的this绑定成number Number {123}
// 2.apply
foo.apply('aaa')//将foo的this绑定成字符串 String {'aaa'}
// 3.bind
let bar = foo.bind('aaa') //String {'aaa'}
bar()
- new关键字绑定
通过一个new关键字调用一个函数(构造函数),这个时候this是在调用这个构造函数时创建出来的对象
this == 创建出来的这个对象
这个绑定过程就是new的绑定
function Person(name,age){
this.name = name //一旦使用这个构造函数实例话一个对象,那么这个this就指向这个对象
this.age = age
}
var p1 = new Person('hjl','20')
var p2 = new Person('hhh','18')
console.log(p1.name,p1.age);
补充:[1,2,3].forEach()
特殊函数this的绑定:定时器this永远指向window,DOM监听函数绑定this指向DOM
var arr = [1,2,3,4,5,6]
arr.forEach((item)=>{
console.log(this,item); //指向window
})
arr.map((item)=>{
console.log(this,item); //指向window
})
3.this绑定优先级
优先级:new >显示>隐式>默认
特殊绑定:
- 1.忽略显示绑定:foo.call(null) foo.call(undefined) 绑定给null和undefined,将会自动绑定给window对象
- 2.间接函数调用
var obj1 = {
name: 'obj1',
foo: function () {
console.log(this);
}
}
var obj2 = {
name: 'obj2'
}; //这种形式一定要加分号
// 给obj2新创建一个方法,让它等于obj1里面的foo方法
//赋值之后可以进行在本身进行调用
// 这种调用方式是独立函数调用 因此是window
(obj2.bar = obj1.foo)()
4.箭头函数
箭头函数:ES6新增加的一种函数的编写方法,比普通的函数表表达式更加的简洁
- 箭头函数不会绑定this、argument属性
- 箭头函数不能作为构造函数去实例化对象,因为没有this(不能和new一起使用否则会报错)
- 简写1.当形参只有一个的时候()可以省略2.执行体只有一句的时候{}可以省略,并且会自动将这句执行体当作返回值返回,但是当执行体是一个对象的时候就不可以省略
- 简写filter,map,reduce
let arr = [22,15,448,16]
let newarr = arr.filter(item =>item % 2 == 0).map(item=> item*100).reduce((preitem,item)=>preitem+item)
console.log(newarr);
5.this绑定面试题
面试题1
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();
面试题2
2.
var name = 'window'
var person1 = {
name: 'person1',
foo1: function () {
console.log(this.name)
},
foo2: () => console.log(this.name),
foo3: function () {
return function () {
console.log(this.name)
}
},
foo4: function () {
return () => {
console.log(this.name)
}
}
}
var person2 = {
name: 'person2' }
person1.foo1(); // person1
person1.foo1.call(person2); //person2
person1.foo2(); //window
person1.foo2.call(person2); //window
person1.foo3()(); //window
person1.foo3.call(person2)(); //window
person1.foo3().call(person2); //person2
person1.foo4()(); //person1
person1.foo4.call(person2)(); //person2
person1.foo4().call(person2);//person1
面试题3
3.
var name = 'window'
function Person(name) {
this.name = name
this.foo1 = function () {
console.log(this.name)
},
this.foo2 = () => console.log(this.name),
this.foo3 = function () {
return function () {
console.log(this.name)
}
},
this.foo4 = function () {
return () => {
console.log(this.name)
}
}
}
var person1 = new Person('person1')
var person2 = new Person('person2')
person1.foo1() //person1
person1.foo1.call(person2) //person2
person1.foo2() //person1
person1.foo2.call(person2) //person1
person1.foo3()() //window
person1.foo3.call(person2)() //window
person1.foo3().call(person2) //person2
person1.foo4()() //person1
person1.foo4.call(person2)() //person2
person1.foo4().call(person2) //person1
面试题4
var name = 'window'
function Person(name) {
this.name = name
this.obj = {
name: 'obj',
foo1: function () {
return function () {
console.log(this.name)
}
},
foo2: function () {
return () => {
console.log(this.name)
}
}
}
}
var person1 = new Person('person1')
var person2 = new Person('person2')
person1.obj.foo1()() // window 这里是前面上层函数绑定person1但是返回的函数还是单独调用,因此还是指向window
person1.obj.foo1.call(person2)() //window 这里是前面上层函数绑定person2但是返回的函数还是单独调用,因此还是指向window
person1.obj.foo1().call(person2) //person2
person1.obj.foo2()() //上层函数指向的是obj
person1.obj.foo2.call(person2)() //person2
person1.obj.foo2().call(person2) //上层函数指向的是obj
6.手写call,apply,bind(详细注释)
call,apply,bind是函数的方法,因此我们自定义时,想在任意函数上使用,就要利用原型链上添加方法。
1.myCall
// 封装mycall函数
Function.prototype.myCall = function (thisarg, ...arg) {
//剩余参数获取传入的不确定参数
// console.log('mycall被调用');
// 1.获取绑定对象
var fn = this
//
// 2.对thisarg\arg转成对象类型(防止传入的是null和undefined)
thisarg = (thisarg !== null && thisarg !== undefined) ? Object(thisarg) : window //object()可以识别对应传入的类型
arg= arg || []
// 3.调用需要被执行的函数
thisarg.fn = fn //让绑定的对象在我们的绑定this值对象上创建,从而去执行
let result = thisarg.fn(...arg) //如果后面传入参数,还有对参数进行接收和处理
delete thisarg.fn //执行结束后,就不要再去添加这个fn直接删除以可
// 4.将最终的结果返回回去
return result
}
function foo() {
console.log('foo函数被执行');
}
function sum(num1, num2) {
console.log('sum函数被执行');
return num1 + num2
}
let res = sum.myCall('aaa', '1', '2',) //这里调用了这个函数,这个函数的this值就绑定在了foo上 this = foo
console.log(res);
2.myapply
// 封装myApply函数
Function.prototype.myApply = function (thisarg, argArray) {
//和call不一样的是这里传入的就是一个数组,Object(null;(Object(undefined)是一个空对象不是数组便会报错;而所以如果传入undifiend和null就会报错
// console.log('myApply被调用');
// 1.获取绑定对象
var fn = this
//
// 2.对thisarg和argArray转成对象类型(防止传入的是null和undefined)
thisarg = (thisarg !== null && thisarg !== undefined) ? Object(thisarg) : window //object()可以识别对应传入的类型
// argArray = argArray? argArray : []
argArray = argArray || []
// 3.调用需要被执行的函数
thisarg.fn = fn //让绑定的对象在我们的绑定this值对象上创建,从而去执行
let result = thisarg.fn(...argArray) //如果后面传入参数,还有对参数进行接收和处理
delete thisarg.fn //执行结束后,就不要再去添加这个fn直接删除以可
// 4.将最终的结果返回回去
return result
}
function foo() {
console.log('foo函数被执行');
}
function sum(num1, num2) {
console.log('sum函数被执行');
return num1 + num2
}
let res = sum.myApply('aaa', ['1', '2']) //这里调用了这个函数,这个函数的this值就绑定在了foo上 this = foo
console.log(res);
3.myBind
// 封装myBind函数
Function.prototype.myBind = function (thisarg, ...argArray) {
//和call不一样的是这里传入的就是一个数组,Object(null;(Object(undefined)是一个空对象不是数组便会报错;而所以如果传入undifiend和null就会报错
// console.log('myBind被调用');
// 1.获取绑定对象
var fn = this
//
// 2.对thisarg和argArray转成对象类型(防止传入的是null和undefined)
thisarg = (thisarg !== null && thisarg !== undefined) ? Object(thisarg) : window //object()可以识别对应传入的类型
// argArray = argArray? argArray : []
// 3.调用需要被执行的函数
return function proxyFn(...args) {
thisarg.fn = fn //让绑定的对象在我们的绑定this值对象上创建,从而去执行
let finalArgs = [...args, ...argArray]
let result = thisarg.fn(finalArgs) //如果后面传入参数,还有对参数进行接收和处理
delete thisarg.fn //执行结束后,就不要再去添加这个fn直接删除以可
// 4.将最终的结果返回回去
}
}
function sum(num) {
console.log('sum函数被执行');
return num
}
let res = sum.myBind('aaa', '3','4') //这里调用了这个函数,这个函数的this值就绑定在了foo上 this = foo
let result = res('1','2') //res 此时就是函数proxyFn
console.log(result);
7.argument对象(剩余参数又叫做扩展运算符替代)
- argument 是一个对应于传递给函数的参数的类数组对象
- 类数组拥有数组的特性(length,index),但是没有数组的方法(forEach,map)
function foo(x,y,t){
console.log(arguments);
console.log(arguments[0]);
console.log(arguments[1]);
}
foo('1','2','2')
补充,如果将argument类数组对象转换为真实数组
1.可以使用数组中slice方法+循环元素放入新的空数组中进行绑定+返回新的数组就是转换的argument数组对象
slice方法自己封装也是通过循环去实现
2.Array.prototype.slice.call(可迭代对象) 和[ ].slice.call(可迭代对象) 一样的效果
3.使用Arrary.form() ES6用法用于通过拥有 length 属性的对象或可迭代的对象来返回一个数组
- 箭头函数是不绑定arguments的,所以我们在箭头函数中使用arguments会去上层作用域查找
五、函数柯里化及JS补充
1.纯函数
函数式编程中有一个非常重要的概念叫纯函数,JavaScript符合函数式编程的范式,所以也有纯函数的概念
- 在react开发中纯函数是被多次提及的;
- 比如react中组件就被要求像是一个纯函数(为什么是像,因为还有class组件),redux中有一个reducer的概念,也是要求必须是一个纯函数;
- 所以掌握纯函数对于理解很多框架的设计是非常有帮助的
- vue3中的setup函数编写逻辑更加接近原生开发
纯函数符合这两个条件:
- 确定的输入,一定会产生确定的输出;
- 函数在执行过程中,不能产生副作用;
例如:slice方法就是纯函数方法,不会改变原数组,有固定输入和输出,splice不是纯函数方法,会改变原数组,这个操作就是副作用。但是一般打印console.log这个输出打印不算是格外的输出,有这句话的纯函数也是纯函数
副作用:
- 表示在执行一个函数时,除了返回函数值之外,还对调用函数产生
了附加的影响,比如修改了全局变量,修改参数或者改变外部的存储; - 副作用往往是产生bug的 “温床”。
纯函数的优势:
- 单纯的实现自己的业务逻辑,不用担心输入的内容或者依赖其他的外部变量发生了修改
- 在用的时候,可以确定输入的内容不会被任意修改,并且自己确定的输入,一定会有输出
- 安心的编写和使用
- 例如:react组件都必须像纯函数一样保护props不要被修改
2.柯里化
柯里化:柯里化其实主要是一个过程,就是只传递给函数一部分参数来调用它,让它返回一个函数去处理剩余参数。
柯里化的作用:
- 单一职责原则,一个函数处理的问题尽可能的单一,而不是将一大堆的处理过程交给一个函数来处理,使逻辑更加清晰。
- 复用参数逻辑
// 这个函数就是可以实现对num的复用
function foo(num){
// 这里还可以对num进行处理,利用闭包的特性,使用let声明就不可以
var num = num+num
return function bar(count){
return num + count
}
}
let add = foo(5)//第一个传入的是5也就是num
add(10) //上面接收的是返回的一个函数 第二个传入的是10
console.log(add(10));
¥3.手写柯里化函数
function myCurrying(fn) {
return function curried(...args) {
// 判断参数的长度是否是和参数本身需要接收的参数是否一致了
// 这个也是递归的退出条件
if (args.length >= fn.length) {
return fn.apply(this, args) //这里的this是curryadd()
} else {
return function curried2(...args2) {
return curried.apply(this,[...args, ...args2]) //递归拼接参数,当参数全部递归拿到后,开始调用传进来的函数
}
}
}
}
function add(num1, num2, num3) {
return num1 + num2 + num3
}
let curryAdd = myCurrying(add)
curryAdd(10)(20)(30)
curryAdd(10, 20)(30)
curryAdd(10, 20, 30)
console.log(curryAdd(10)(20)(30));
console.log(curryAdd(10, 20)(30));
console.log(curryAdd(10, 20, 30));
// 补充: 可以foo.length获取函数参数的长度
// function foo(x,y,z,m){}
// console.log(foo.length);
4.组合函数
组合函数:是最终对函数的使用技巧和模式,某一个过程需要两个函数都进行调用,在操作上就显得重复,可以将两个函数合并起来,自动的依次进行调用。这个过程就是对函数的组合叫做组合函数。
¥ 5.手写组合函数
简单方式封装
function add(num) {
return num * 2
}
function square(num) {
return num ** 2 //一个数平方的写法
}
// 1.组合函数简写2 直接依次调用
let result = add(square(10))
console.log(result);
// 2.组合函数简写1 封装一个简单组合函数
function compose(m, n) {
return function (count) {
return m(n(count)) //封装起来依次调用
}
}
let foo = compose(add, square)
console.log(foo(10));
通用组合函数实现
// 封装一个自己的组合函数函数
function myCompose(...fns){
var length = fns.length
//遍历所有的原生,如果不是函数直接报错
for(let i = 0; i < length ; i++){
if(typeof fns[i] !== 'function'){
throw new TypeError('expected arguments are function')
}
}
// 都是函数之后,取出所有的函数进行依次调用
return function compose(...args){
let index = 0
// 声明一个结果参数变量,好后续返回
let result = length ? fns[index].apply(this,args) : args //给result初始化赋值
while(++index < length){
result = fns[index].call(this,result)
}
// result一直进行擦传入函数的调用
return result
}
}
function add(num) {
return num * 2
}
function square(num) {
return num * 2 //一个数平方的写法
}
let foo = myCompose(add,square)
console.log(foo(100));
4.with (不推荐使用,会混淆和错乱)
作用域:全局作用域、函数作用域、类作用域
- with语句:
专门给一个代码块增加一个查找变量的对象(也就是增加一层特定的作用域)
// 1.函数内嵌套
var message = "window"
var obj = {
name: 'why', age: 18, message: 'hahah' }
function foo() {
function bar() {
with (obj) {
//with的作用就是多增加一层作用域传入的对象就是多加的一层作用域
console.log(message);
}
}
bar()
}
foo()
// 2.自己外部使用
with (obj) {
console.log(age);
}
5.eval
eval:是一个特殊的函数,它可以将传入的字符串当作JavaScript代码来运行
let jsString = 'var message = "hahha";console.log(message)'
eval(jsString) //hahha
真正开发中不建议使用:
- 1.代码可读性很差
- 2.eval是一个字符串,有可能在执行过程中造成被攻击的危险
- 3.eval的执行必须经过JS解释器,不能被JS引擎优化
6. 严格模式
严格模式;具有限制性的JavaScript模式,具有严格模式的浏览器,会以更加严格的方式对代码进行解析(没有限制的话,不同版本的浏览器可能会报错)
现在在打包工具中,会自动开启严格模式
- 严格模式通过在文件或者函数开头使用 use strict 来开启。
- 也支持对某一个函数开启严格模式
- 可以支持在js文件中开启严格模式;(粒度话的迁移)
- 具体有哪些限制
- 通过抛出错误来消除一些原有的静默错误
- 严格模式在JS引擎在执行代码时可以进行多次优化(不需要对特殊语法进行处理)
- 禁用了ECMAscript未来版本中可能会定义的一些语法,避免了未来在升级后起的一些冲突
保留字:class/let/class 是未来有可能会升级为关键字的词 var class = ‘abc’ 将保留字使用了,是错误的
关键字:function/var/new
严格语法限制
- 无法意外的创建全局变量
- 严格模式会使引起静默失败(silently fail,注:不报错也没有任何效果)的赋值操作抛出异常
- 严格模式下试图删除不可删除的属性
4.严格模式不允许函数参数有相同的名称 - 不允许0的八进制语法
- 在严格模式下,不允许使用with
- 在严格模式下,eval不再为上层引用变量
- 严格模式下,this绑定不会默认转成对象 也就是说严格模式下,自执行函数会指向undefined
- 定时器和普通函数的使用中的this指向(因为定时器是浏览器中的我们无法确认里面的操作,但是此时this指向window,因此可能是内部给绑定了.apply(window,)),有一些APi是浏览器本身实现的,不是JS引擎实现的
六、对象字面量和对象的封装
面向对象:
用对象来描述事物,有利于将现实的事物,抽离成代码的某个数据结构
Java是纯面向对象的编程语言
JS函数式编程+面向对象编程(类,函数,变量)
实现面向对象,先抽离一个类,再在类中创建对象
1.创建对象的方式
- 早期使用创建对象的方式最多的是使用Object类,并且使用new关键字来创建一个对象:
- 这是因为早期很多JavaScript开发者是从Java过来的,它们也更习惯于Java中通过new的方式创建一个对象;
- 后来很多开发者为了方便起见,都是直接通过字面量的形式来创建对象:
- 这种形式看起来更加的简洁,并且对象和属性之间的内聚性也更强,所以这种方式后来就流行了起来;
2.对属性操作的控制
1.属性描述符:
Object.defineProperty() 等于属性进行添加或者修改
- 可接收三个参数:
- obj要定义属性的对象;
- prop要定义或修改的属性的名称或 Symbol;
- pdescriptor要定义或修改的属性描述符;
- 返回值
- 被传递给函数的对象
Object.defineProperties() 方法:直接在一个对象上定义 多个 新的属性或修改现有属性,并且返回该对象。
2.属性描述符分为:
数据描述符:
-
configurable:表示属性是否可以通过delete删除属性,是否可以修改它的特性,直接在一个对象上定义某个属性时(在使用属性描述符定义的时候,默认为false)
-
enumerate:表示属性是否可以通过for-in或者**Object.keys()**返回该属性(在使用属性描述符定义的时候,默认为false)
-
value:属性的value值,读取属性时会返回该值,修改属性时,会对其进行修改(默认情况下这个值是undefined)
-
writable:表示是否可以修改属性的值(在使用属性描述符定义的时候,默认为false)
存取属性描述符: -
configurable:表示属性是否可以通过delete删除属性,是否可以修改它的特性,直接在一个对象上定义某个属性时(在使用属性描述符定义的时候,默认为false)
-
enumerate:表示属性是否可以通过for-in或者**Object.keys()**返回该属性(在使用属性描述符定义的时候,默认为false)
-
get:获取属性时会执行的函数。默认为undefined
-
set:设置属性时会执行的函数。默认为undefined
3.对象方法补充
n 获取对象的属性描述符:
pgetOwnPropertyDescriptor
pgetOwnPropertyDescriptors
n 禁止对象扩展新属性:preventExtensions
p给一个对象添加新的属性会失败(在严格模式下会报错);
n 密封对象,不允许配置和删除属性:seal
p实际是调用preventExtensions
p并且将现有属性的configurable:false
n 冻结对象,不允许修改现有属性: freeze
p实际上是调用seal
p并且将现有属性的writable: false
4.对象创建的方式
- 1.字面量创建
- 2.new Object方式; (1,2方式有很大的弊端,封装对象时需要配置重复的大量代码)
- 3.工厂模式(封装一个函数里面返回一个创建好的对象(属性和方法)),在调用函数是就可以创建不同的对象,缺点是获取不到对象的最真实类型
- 4.构造函数:在JS中使用new操作符调用则是构造函数,这个构造函数可以确保我们的对象是有自己的的类型的(实际是constructor的属性);缺点是构造函数也是有缺点的,它在于我们需要为每个对象的函数去创建一个函数对象实例,每个实例化对象的方法都是在内存中单独开辟的,是非常消耗内存的。解决办法(原型)
- 5.构造函数+原型 将单纯构造函数创建的弊端解决(将这些函数放到Person.prototype的对象上即可)
5.面试题new操作符调用后的作用
1.在内存中创建一个新的对象(空对象);
2. 这个对象内部的[[prototype]]属性会被赋值为该构造函数的prototype属性;(后面详细讲);
p3. 构造函数内部的this,会指向创建出来的新对象;
p4. 执行函数的内部代码(函数体代码);
p5. 如果构造函数没有返回非空对象,则返回创建出来的新对象;
七、原型、原型链
1.原型:
概念:当中每个对象都有一个特殊的内置属性 [[prototype]],这个特殊的对象可以指向另外一个对象
2.获取一个对象原型的方式有两种:
方式一:通过对象的 proto 属性可以获取到(但是这个是早期浏览器自己添加的,存在一定的兼容性问
题);
方式二:通过 Object.getPrototypeOf 方法可以获取到
3.函数的原型 prototype
所有的函数都有一个prototype的属性,
4使用prototype添加属性,其实是给构造函数的原型对象上添加属性(prototype指向的那个对象)
5.原型对象上面是有一个属性的:constructor,
默认情况下原型上都会添加一个属性叫做constructor,这个constructor指向当前的函数对象
每创建一个函数, 就会同时创建它的prototype对象, 这个对象也会自动获取constructor属性;
原生的constructor属性是不可枚举的.
希望改变 constructor的指向,就可以使用我们前面介绍的Object.defineProperty()函数
2.原型链
从一个对象上获取属性,如果在当前对象中没有获取到就会去它的原型上面获取,它的原型对象上没有就会再向上去 寻找,直到到原型链的尽头(object的原型)
Object的原型:
- 该对象有原型属性,但是它的原型属性已经指向的是null,也就是已经是顶层原型了
- 该对象上有很多默认的属性和方法
- Object是所有类的父类
八、¥继承
面向对象有三大特性:封装、继承、多态
p封装:我们前面将属性和方法封装到一个类中,可以称之为封装的过程;
p继承:继承是面向对象中非常重要的,不仅仅可以减少重复代码的数量,也是多态前提(纯面向对象中);
p多态:不同的对象在执行时表现出不同的形态
1.原型链继承
弊端:某些属性其实是保存在p对象上的;
p第一,我们通过直接打印对象是看不到这个属性的;
p第二,这个属性会被多个对象共享,如果这个对象是一个引用类型,那么就会造成问题;
p第三,不能给Person传递参数,因为这个对象是一次性创建的(没办法定制化
// 1.定义父类构造函数
function Person() {
this.name = 'why'
}
// 2.父类的原型上添加内容
Person.prototype.runing = function () {
console.log(this.name + 'runing');
}
// 3.定义子类构造函数
function Student() {
this.son = 111
}
// 4.创建父类对象,并,并且作为子类的原型对象
var p = new Person()
Student.prototype = p //这一步上student默认的原型对象丢弃,在新创建的新对象上添加方法,并且此时新对象也和person指向它的原型对象,因此得到继承,但是缺点是需要创建新对象
// 5.在子类原型上添加内容
Student.prototype.studying = function () {
console.log(this.name + 'studing');
}
let son = new Student()
son.runing()
2.构造函数继承
在子类型构造函数的内部调用父类型构造函数.
再让子的原型等于父的原型
- 因为函数可以在任意的时刻被调用;
- 因此通过apply()和call()方法也可以在新创建的对象上执行构造函数;
3.组合借用继承
4.原型式继承函数
5.寄生式继承函数
6.寄生组合式继承
7.类继承
ES6中class关键字来直接定义类
p每个类都可以有一个自己的构造函数(方法),这个方法的名称是固定的constructor;
p当我们通过new操作符,操作一个类的时候会调用这个类的构造函数constructor;
p每个类只能有一个构造函数,如果包含多个构造函数,那么会抛出异常
当使用new关键字操作类的时候,就会调用这个constructor函数,并且执行如下操作
- 在内存中创建一个新的对象(空对象)
- 让这个对象的原型被赋值给该类的原型属性
- 构造函数的this值指向新创建出来的新对象
- 执行构造函数里面的代码(函数体代码)
- 如果构造函数没有返回空值对象,则返回新创建的新对象
类的实例(自己手写)
类的静态方法(自己手写)
8.实现类继承(extends super)
子继承父,在内存中其实是将子的隐式原型指向父的原型上,从而去继承方法和属性
class Student extend Person{
//extend
constructor(name,age,sno){
suqer(name,age) //suqer
this.sno = sno
}
personMethod(){
//可以对父类方法重写可以复用父类的处理逻辑
super.personMethod()
console.log("孩子的方法")
}
}
var stu = new Student('why'.18,111)
九、ES6、ES6面对对象
1.多态
不同的数据类型进行同一个操作,表现出不同的行为,就是多态的体现
2.解构
一个从数组或对象中方便获取数据的方法,数组的解构和对象的解构
// 对数组的解构
let arr = [1, 2, 3, 4, 5, 6]
let [item1, item2, item3, item4] = arr
console.log(item1, item2, item3, item4);
// 对象的解构
let obj = {
name: 'hjl',
age: '18',
height: 180
}
// 1.
let {
name, age, height } = obj
console.log(name, age, height);
// 2.对解构出来的重新命名
let {
name: newname } = obj
console.log(newname);
// 3.新添加属性
let {
adress = '深圳' } = obj
console.log(adress);
3.字面量的增强写法
- 属性的简写。key和value相同就只需简写成一个
- 方法的简写,方法名+()
- 计算属性名,[name+123]
4.let const
- let ,const不允许重复声明变量
- let、const没有进行作用域提升,但是会在解析阶段被创建出来。虽然被创建出来了,但是不能被访问,我认为不能称之为作用域提升
- 全局通过var来声明一个变量,事实上会在window上添加一个属性,但是let、const是不会给window上添加任何属性的
- const声明的变量其实是存储的是这个变量的地址,因此其实要更改const声明的引用数据类型,只能保证地址不变,但是不能保证这个地址指向的值不发生改变。基本数据类型,存储的地址其实本身也是自身执行栈中,因此保存的基本数据类型保存的就是本身,不能修改。
5.作用域
- 全局作用域
- 函数作用域
- 块级作用域(let ,const):通过let、const、function、class声明的标识符是具备块级作用域的限制的
会发现函数拥有块级作用域,但是外面依然是可以访问的:因为引擎会对函数的声明进行特殊的处理,允许像var那样进行提升;
暂时性死区:在一个代码中,使用let,const声明的遍历,在声明之前变量是不可以访问的,这种现象称之为暂时性死区
块级作用域的应用:在for循环中使用letconst,因为let声明的i其实就会在每次循环后新开辟一个内存变量,每个i不相互影响
var、let、const的选择
- var所表现出来的特殊性:比如作用域提升、window全局对象、没有块级作用域等都是一些历史遗留问题;
- 对于let和const来说,是目前开发中推荐使用的;
- 我们会有限推荐使用const,这样可以保证数据的安全性不会被随意的篡改
- 只有当我们明确知道一个变量后续会需要被重新赋值时,这个时候再使用let;
6.模板字符串
1.${
}加变量 const mess = `my name is ${
name}`
2.${
}加表达式
3.${
}加函数调用
4.标签模板字符串的使用 foo``函数调用代替()
5.标签模板字符串foo``函数传参`message${
1}abb${
2}` 此时的${
}相当于空格或者分隔符将其它文字作为第一个参数传入或者数组形式传入,${
}里面第一个作为第二个传入
react中开发:all in js (html css js)
7.函数中的默认参数
在ES6中,我们允许给函数一个默认值
function foo(m = "aaa",n= "bbb"){
console.log(m,n); //这里没有传参是不会报错的
}
// 对象的参数默认值以及结构
function foo({
name,age}={
name:"name",age:"age"}){
console.log(name,age);
}
foo()
8.函数剩余参数(其它:展开运算符)
函数中不确定传入参数的多少,可以最后一个参数,使用剩余参数,那么会将剩余的参数放到该参数中,并且作为一个数组
剩余参数必须放到最后一个位置,否则会报错
剩余参数和argument有什么不同?
- 剩余参数只包括那些没有对应形参的实参,而arguments却包含了传给参数的所有实参
- arguments对象不是真正的数组,而rest参数是一个正真的数组,可以进行数组的操作
- arguments是早期的ECMAScript中为了方便去获取所有的参数提供的一个数据结构,而rest参数是ES6中提供并且希望以此来替代arguments的;
函数箭头函数的补充:箭头函数是没有显式原型的,所以不能作为构造函数,使用new来创建对象;
9.展开语法运算符
- 在对象和数组中用来合并数组和元素操作
- 可以在函数调用/数组构造时,将数组表达式或者string在语法层面展开;
- 还可以在构造字面量对象时, 将对象表达式按key-value的方式展开;
- 展开运算符其实是一种浅拷贝;
const info = {
name:"name",
friend:{
name:"haha"}
}
const obj = {
...info}
// 此时通过obj更改info里面的值就会更改源对象的值
obj.friend.name = "xixixi"
console.log(info.friend.name) //变成了xixixi 因为info和obj拷贝过来的复杂数据类型存储的是同一份地址
10.ES6中表达数值的方式
const num1 = 100 //十进制
const num2 = 0b100//二进制 binary
const num3 = 0o100//八进制 octonary
const num4 = 0x100//十六进制 hexadecimal
console.log(num1, num2, num3, num4);
大的数字连接符,连接用下划线分割,更加清晰
const num = 100_00_00_00_00
11.symbol
为什么需要Symbol呢?
-
在ES6之前,对象的属性名都是字符串形式,那么很容易造成属性名的冲突
-
比如原来有一个对象,我们希望在其中添加一个新的属性和值,但是我们在不确定它原来内部有什么内容的情况下,很容易造成冲突,从而覆盖掉它内部的某个属性;
-
比如我们前面在讲apply、call、bind实现时,我们有给其中添加一个fn属性,那么如果它内部原来已经有了fn属性了呢?
-
比如开发中我们使用混入,那么混入中出现了同名的属性,必然有一个会被覆盖掉;
Symbol就是为了解决上面的问题,用来生成一个独一无二的值。 -
Symbol值是通过Symbol函数来生成的,生成后可以作为属性名;
-
也就是在ES6中,对象的属性名可以使用字符串,也可以使用Symbol值;
-
Symbol即使多次创建值,它们也是不同的:Symbol函数执行后每次创建出来的值都是独一无二的;
-
我们也可以在创建Symbol值的时候传入一个描述description:这个是ES2019(ES10)新增的特性;
//1.
const s3 = Symbol("aaa")
console.log(s3.description) //aaa
//2.Symbol 作为属性名
//2.Symbol 作为属性名
const s1 = Symbol()
const s2 = Symbol()
const s3 = Symbol()
const obj = {
[s1] :"abc",
[s2] :"cba"
}
// 3.新增属性
obj[s3] = "abc"
const s4 = Symbol()
// 4.增加属性 defineProperty
Object.defineProperty(obj,s4,{
enumerable:true,
configurable:true,
wirtable:true,
value:"mba"
})
console.log(obj); //{Symbol(): 'abc', Symbol(): 'cba', Symbol(): 'abc', Symbol(): 'mba'}
// 5.使用Symbol作为key的属性名,在遍历的时候Object.keys是无法获取的Symbol的值
console.log(Object.keys(obj)); //[]
console.log(Object.getOwnPropertyNames(obj)); //[]
console.log(Object.getOwnPropertySymbols(obj)) //[Symbol(), Symbol(), Symbol(), Symbol()]
// 6.symbol.for(key) 想创建相同的Symbol应该怎么来做呢?
// 我们可以使用Symbol.for方法来做到这一点
// 并且我们可以通过Symbol.keyFor方法来获取对应的key;
12.set、weakSet
主要用于数组去重
- 常见属性
- size返回set中元素的个数
- 常见方法
- add()添加元素 返回set
- has()是否有此元素返回布尔值
- delete()删除某个元素返回布尔值
- clear()清空set 没有返回值
- for of foreach都可以遍历set
let set = new Set()
// 添加对象时注意,添加的这两个对象是不一样的
set({
})
set({
})
// 这样就是一样的
let obj = {
}
set(obj)
set(obj)