全局代码执行过程
代码被解析,v8引擎内部会帮助我们创建一个对象(GlobalObject => go)
变量环境和环境记录
-
早期ECMA版本规范
- 每一个执行上下文会被关联到一个变量对象VO,在源码中的变量和函数声明会被作为属性添加到VO中,函数的参数也会被添加到VO中
-
最新ECMA规范
- 每一个执行上下文会被关联到一个变量环境VE,在执行代码中的变量和函数声明会被作为绑定这个VE的环境记录添加到VO中,函数的参数也会被添加到VO中
内存管理
-
生命周期
- 分配内存
- 使用
- 释放
-
手动管理:C C++ 早期OC (malloc函数和free函数)
-
自动管理:Java JavaScript Python Swift Dart...
内存分配
- 对于基本数据类型的分配会在执行时,直接在栈空间分配
- 对于复杂数据类型会在堆内存中开辟一块空间,并将这块空间的指针返回值变量引用
垃圾回收
- 垃圾回收机制 GC(Garbage Collection)
- 回收不再使用的对象
- 语言运行环境内置垃圾回收器 GC (java的JVM,js的引擎...), 大多数GC都是垃圾回收器
常用的GC算法
-
引用计数
- 当一个对象被引用时,引用计数+1,当引用计数为0时,这个对象就可以被销毁
- 弊端:循环引用,互相引用,引用计数无法为0,需要手动置为null
-
标记清除 (js引擎广泛采用)
- 设置一个根对象(root object),垃圾回收器定期从根对象开始寻找有引用的对象,没有找到的对象即未被引用的对象就被认为是不可用的对象,将会被回收
- 解决循环引用
高阶函数
-
把函数作为参数或者作为返回值的函数
-
数组中常见高阶函数
- filter (item,index,arr) => {return boolean}
- map (item,index,arr) => {return item}
- forEach (item,index,arr) => {item} 没有返回值
- find (item,index,arr) => {return boolean}
- findIndex (item,index,arr) => {return boolean}
- reduce ((preValue, item) => { return 下一次的preValue}, initialValue)
参数作用域
-
当函数的参数有默认值时,会形成一个新的作用域,这个作用域用于保存参数的值
-
var x = 0; function foo( x, y = function () { x = 3; console.log(x); // undefined } ) { console.log(x); // 3 var x = 2 y() console.log(x); // 2 } foo() console.log(x); // 0
闭包
- 一个普通的函数,如果可以访问外层作用域的自由变量,那这个函数就是一个闭包
- 闭包:函数+可以访问的自由变量
- 广义上看,JS的函数都是闭包
- 狭义上看,JS中的函数如果访问了外层作用域的变量,那这个函数就是闭包
内存泄漏
- 引用链中的对象无法释放
- 手动释放:可以将其设置为null
this
- 全局作用域
- 浏览器 this => window
- node this => { }; 因为执行函数时,默认call到一个空对象上
- this的绑定和定义的位置没有关系
- this的绑定和调用方式以及调用的位置有关系
- this是在运行时绑定的
绑定规则
-
默认绑定 独立函数调用,指向window
-
隐式绑定 通过某个对象发起的函数调用,谁调用指向谁
-
显式绑定 call/apply/bind
-
指定this的绑定对象
-
apply传参是数组
-
bind传参的三种方式
-
// 1. var newSum = sum.bind('系统bind', 10, 20, 30, 40) newSum() // 2. var newSum = sum.bind("系统bind"); newSum(10, 20, 30, 40); // 3. var newSum = sum.bind("系统bind", 10, 20); newSum(30, 40);
-
-
默认绑定和显式绑定bind冲突,显式绑定优先级大于默认绑定
-
-
new绑定 this指向创建出来的对象
- 创建一个全新的对象
- 这个新对象会被执行prototype连接
- 这个新对象会绑定到函数调用的this上(this绑定在这个步骤完成)
- 如果函数没有返回其他对象,表达式会返回这个新对象
系统API中this的指向
- setTimeout this指向window
- 监听点击 this指向被绑定的元素
- 数组 forEach/map/filter/find/findIndex
- this指向window,
- 如果传第二个参数,this指向该对象,
- 箭头函数会指向window
绑定规则优先级
- 默认绑定优先级最低
- 显式绑定优先级高于隐式绑定
- bind显式绑定优先级高于call/apply显式绑定
- new绑定优先级高于隐式绑定
- new绑定优先级高于bind显式绑定
- new关键字不能和apply/call一起使用
特殊绑定
忽略显式绑定
- apply / call / bind :当传入null/undefined时,自动将this绑定为全局对象
间接函数引用
-
新代码时加小括号的话,前面的代码要加分号
var obj = { foo: function() { console.log(this); } } var obb = {}; // obb.bar = obj.foo // obb.bar() // obb (obb.bar = obj.foo)() // this指向window 加上小括号并且将函数赋值被视为独立函数调用
箭头函数 ()=> { }
规则
-
不会绑定this,arguments属性
-
根据外层作用域来决定this
var name = 'zzy' var foo = () => { console.log(this); } var obj = { foo: foo } foo() // window obj.foo() // window foo.call('call') // window -
箭头函数没有显式原型,不能作为构造函数来使用(不能和new一起来使用,会抛出错误)
-
高阶函数使用时,可以传入箭头函数
- filter,map,reduce联合使用
var arr = [112, 312, 31, 41]; var result = arr.filter((e) => e % 2 === 0) .map((e) => e * 2) .reduce((preValue, e) => preValue + e); console.log(result); //848
常见简写
-
只有一个参数时,**( )**可以省略
-
只有一行代码时,{ }可以省略,会默认返回执行结果
-
只有一行代码,返回一个对象,应该给对象加上**()**
var foo = () => ({name: 'zzy', age: 22})
常用场景
-
var obj = { data1: [], data2: [], getData: function () { // 没有箭头函数之前 var _this = this; // 将obj赋值给_this setTimeout(function () { var res = ["a", "b", "c"]; _this.data1 = res; // 将res保存到obj.data1 }, 2000); // 有了箭头函数之后 setTimeout(() => { var res = ["a", "b", "c"]; this.data2 = res; // 将res保存到this.data2,向外层作用域寻找this }, 2000); }, }; obj.getData();
面试题
-
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 等价于person.sayName() (b = person.sayName)(); // window 间接函数引用 } sayName(); -
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 preson1只隐式绑定了foo3(),后面执行是独立函数调用 person1.foo3.call(person2)(); // window person2只显式绑定到了foo3(),后面执行是独立函数调用 person1.foo3().call(person2); // person2 后面执行时显式绑定到了person2上 person1.foo4()(); // person1 上层作用域是person1 person1.foo4.call(person2)(); // person2 将foo4的作用域显式绑定到了person2,所以执行时的上层作用域是person2 person1.foo4().call(person2); // person1 上层作用域是person1 -
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 person1.foo2.call(person2) // person1 同上 person1.foo3()() // window 在全局进行调用的 person1.foo3.call(person2)() // window 在全局进行调用的 person1.foo3().call(person2) // person2 显式绑定到person2 person1.foo4()() // person1 向上层作用域寻找,是被person1隐式绑定的foo4 person1.foo4.call(person2)() // person2 向上层作用域寻找,是被person2显式绑定的foo4 person1.foo4().call(person2) // person1 向上层作用域寻找,是被person1隐式绑定的foo4,call调返回的箭头函数不绑定this,所以是person1 -
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.obj.foo1.call(person2)() // window 全局调用 person1.obj.foo1().call(person2) // person2 被person2绑定后调用 person1.obj.foo2()() // obj 上层作用域被obj隐式绑定 person1.obj.foo2.call(person2)() // person2 上层作用域被person2显式绑定 person1.obj.foo2().call(person2) // obj 箭头函数不被person2绑定,依然是obj
实现apply,call,bind
mycall
-
Function.prototype.mycall = function(thisArg, ...args) { // 1.获取需要被执行的函数 var fn = this // 2.对thisArg转成对象类型(防止传入的是非对象类型) thisArg = (thisArg !== undefined && thisArg !== null) ? Object(thisArg) : window // 3.调用需要被执行的函数 thisArg.fn = fn var result = thisArg.fn(...args) delete thisArg.fn return result }
myapply
-
Function.prototype.myapply = function(thisArg, argArray) { // 1.获取需要被执行的函数 var fn = this // 2.对thisArg转成对象类型(防止传入的是非对象类型) thisArg = (thisArg !== undefined && thisArg !== null) ? Object(thisArg) : window // 3.调用需要被执行的函数 thisArg.fn = fn var result // if (!argArray) { // // 无参数 // result = thisArg.fn() // } else { // // 有参数 // result = thisArg.fn(...argArray) // } // 1.if-else // argArray = argArray ? argArray : [] // 2,三元运算符 argArray = argArray || [] // 3.逻辑或 result = thisArg.fn(...argArray) delete thisArg.fn return result }
mybind
-
Function.prototype.mybind = function(thisArg, ...argArray) { // 1.获取需要调用的函数 var fn = this // 2.绑定this thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window function proxyFn(...args) { // 3.将函数放入thisArg中调用 thisArg.fn = fn // 对传入的两个参数进行合并 var finalArgs = [...argArray, ...args] var result = thisArg.fn(...finalArgs) delete thisArg.fn // 4.返回结果 return result } return proxyFn }
arguments
类数组(array-like)
- 是对象类型(传递给函数的参数)
拥有数组的一些特性
arguments.length
- 获取参数长度
arguments[index]
- 根据索引获取某一个参数
但没有数组的一些方法,forEach,map...
arguments.callee
- 获取当前arguments所在的函数
- 禁止使用
arguments.callee(),会造成递归
arguments转数组
for循环
-
var arr = []; for (let i = 0; i < arguments.length; i++) { arr.push(arguments[i]); }
Array.prototype.slice.call(arguments)
-
var arr = Array.prototype.slice.call(arguments)
[ ].slice.call(arguments)
-
var arr = [].slice.call(arguments)
ES6语法 Array.from(arguments)
-
var arr = Array.from(arguments)
展开运算符 ...[arguments]
-
var arr = [...arguments]
箭头函数中的arguments
-
会向上层作用域中去寻找arguments
-
上层作用域是全局
-
var foo = () => { console.log(arguments); } foo() -
在浏览器全局中没有arguments
-
在node全局中有arguments
-
JS函数式编程
JavaScript纯函数
维基百科定义纯函数
- Pure Function
- 此函数在相同的输入值时,需产生相同的输出
- 函数的输出和输入值以外的其它隐藏信息或状态无关,也和I/O设备产生的外部输出无关
- 该函数不能有语义上可观察的函数副作用,如:触发事件,使输出设备输出,更改输出值以外物件的内容...
总结纯函数定义
- 确定的输入,一定会产生确定的输出
- 函数在执行过程中,不能产生副作用
副作用 side-effect
- 在执行一个函数时,除了返回函数值外,还对调用函数产生了附加的影响,比如:修改了全局变量,修改了参数,改变了外部存储
- 非纯函数容易产生bug
数组函数对比
-
splice修改原数组,不是纯函数
-
slice未修改原数组,是纯函数
-
slice的实现
Array.prototype.myslice = function(start,end) { var arr = this start = start || 0 end = end || arr.length var newArr = [] for (var i = start; i < end; i++) { newArr.push(arr[i]) } return newArr }
-
纯函数/非纯函数例子
-
// 1.splice------------------------------------------------------- var names = ['a', 'b', 'c', 'd'] var newNames = names.splice(0,2) console.log(newNames); // [ 'a', 'b' ] console.log(names); // [ 'c', 'd' ] splice修改了原数组,不是纯函数 // 2.slice-------------------------------------------------------- var chars = ['a', 'b', 'c', 'd'] var newChars = chars.slice(0,2) console.log(newNames); // [ 'a', 'b' ] console.log(chars); // [ 'a', 'b', 'c', 'd' ] slice没有修改原数组,是纯函数 // 3.foo是纯函数------------------------------------------------------- function foo(num1, num2) { return num1 * 2 + num2 * num2 } // 4.bar不是纯函数,修改了外界变量--------------------------------------- var flag = 'aaa' function bar() { flag = "bbb" } bar() console.log(flag); // bbb // 5.baz不是纯函数,修改了传入的参数--------------------------------------- function baz(info) { info.age = 33 } var obj = { age: 22 } baz(obj) console.log(obj.age); // 33 // 6.test是纯函数,确定的输入产生确定的输出-------------------------- function test(info) { return { ...info, age: 22 } } test(obj)
纯函数的优势
- 安心编写,安心使用
- 只用单纯实现自己的业务逻辑
- 不需要关心传入的内容是如何获得
- 不需要关心依赖其他的外部变量是否发生了修改
- 确定的输入,一定有确定的输出
React的规则
- 所有React组件都必须像纯函数一样保护他们的props不被更改
JavaScript柯里化
维基百科定义柯里化
- Currying 柯里化,卡瑞化,加里化
- 把接收多个参数的函数,变成接收一个单一参数的函数,并且返回接收余下的参数,而且返回结果的新函数
- 柯里化声称:如果你固定某些参数,你将得到接收余下参数的一个函数
总结柯里化定义
- 只传递给函数一部分参数来调用他,让它返回一个函数去处理剩余的参数,该过程即为柯里化
柯里化过程
-
// 普通函数 function add(x,y,z) { return x + y + z } var result = add(1,2,3) console.log(result); // 6 // 柯里化 function sum(x) { return function(y) { return function(z) { return x + y + z } } } var res = sum(1)(2)(3) console.log(res); // 6 // 简化柯里化 var simplifySum = x => y => z => x + y + z console.log(simplifySum(1)(2)(3)); // 6
柯里化优势
使函数职责单一
- 单一职责原则 SRP(Single Responsibility principle)
- 将每次传入的参数在单一的函数中进行处理,处理完之后在下一个函数中再使用处理后的结果
逻辑的复用
- 可以将功能一步一步实现,后续需求不会影响之前的逻辑,就可以实现复用
柯里化的实现
-
// 编写柯里化函数 function myCurring(fn) { function curried(...args) { // 判断当前已经接收的参数的个数,和函数本身需要接收的参数是否已经一致 if (args.length >= fn.length) { // fn.call(this, ...args) return fn.apply(this, args); } else { // 没有达到需要的参数时,返回一个新的函数,继续接收剩余的参数 function curried2(...args2) { // 接收到参数后,需要递归去调用curried,来检查参数个数是否满足要求 // return curried.apply(this, [...args, ...args2]); return curried.apply(this, args.concat(args2)) } return curried2; } } return curried; }
组合函数
- 组合函数(compose)是在JavaScript开发过程中一种对函数的使用技巧,模式
通用组合函数的实现
-
function myCompose(...fns) { var length = fns.length; for (var i = 0; i < length; i++) { if (typeof fns[i] !== "function") { throw new TypeError("Expected functions"); } } function compose(...args) { var index = 0; var result = length ? fns[index].apply(this, args) : args; // 先加加,在判断 while (++index < length) { // result = fns[index].apply(this, [result]) result = fns[index].call(this, result); } return result; } js return compose; }
with语句(不推荐使用)
-
可以形成自己的作用域
-
var message = "hello world"; var obj = { message: "obj message", }; function foo() { // 不推荐使用 with (obj) { console.log(message); // obj message 严格模式下没有with语句 } } foo();
-
-
不推荐使用
- 容易混淆错误
- 造成兼容性问题
eval函数 (不推荐使用)
-
可以将传入的字符串当作JS代码执行
-
var jsString = 'var message = "hello world"; console.log(message);' eval(jsString) // hello world -
不推荐使用
- 可读性差(代码的可读性是高质量代码重要原则)
- eval是一个字符串,有可能在执行过程中被刻意更改,易被攻击
- eval的执行必须经过JS解释器,不能被JS引擎优化
-
严格模式
-
严格模式 (Strict Mode)
- 对JS代码具有限制性,使代码隐式脱离懒散模式(Sloppy Mode)
- 支持严格模式的浏览器在检测到代码中有严格模式时,会更严格的检测和执行代码
-
严格模式 对正常的JavaScript语义进行了一些限制
- 通过抛出错误来消除原有的静默(silent)错误
- 让JS引擎在执行代码时可以进行更多的优化(不需要对一些特殊的语法进行处理)
- 禁用了在ECMAScript未来版本中可能会定义的一些语法
开启严格模式
- 严格模式支持粒度化的迁移
- 对某个js文件开启
"use strict";加到文件顶部
- 对某个函数开启
"use strict";加到函数内顶部
- 对某个js文件开启
严格模式限制
严格语法限制
- 新手开发者的不规范和失误在严格模式下会被当作错误,以便可以快速改正
- 不允许意外的创建全局变量
- 严格模式会引起静默失败(silent fail,注:不报错也不会生效)的赋值操作抛出异常
- 不允许删除不可删除的属性
- 不允许函数参数名称相同
- 不允许0的八进制语法
- 不允许使用with
- eval不再为上层引用变量
- this绑定不会默认转为对象
- 自执行函数(默认绑定)的this会指向undefined
- setTimeout的普通函数的this依然指向window,箭头函数的this向上层作用域寻找
补充
- _开头表示私有变量