点这里,欢迎关注
https://www.cnblogs.com/pianruijie/p/11454598.html
https://segmentfault.com/a/1190000018001871
https://segmentfault.com/a/1190000000533094
https://www.cnblogs.com/lulin1/p/9712311.html
http://www.frontopen.com/1702.html
js的执行总共有三个过程:语法分析 --> 预编译(词法分析) --> 解释执行
1.语法分析:
2.预编译阶段:(在进入执行环境时即进入到了预编译阶段)
3.解释执行阶段
01.执行环境:
作用域模型有两种:词法作用域(静态作用域)和动态作用域。
词法作用域是指在词法阶段(预编译)定义的作用域,说它是静态的是因为代码在执行时,其词法作用域就已经确定了,词法分析器处理代码时其词法作用域是不变的。
词法作用域也称为执行环境,其定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象。环境中定义的所有变量和函数都保存在这个对象上。
02.全局执行环境:
全局执行环境是最外围的一个执行环境。根据 ECMAScript 实现所在的宿主环境不同,表示执行环
境的对象也不一样。在 Web 浏览器中,全局执行环境被认为是 window 对象。因此所有全局变量和函数都是作为window对象的属性和方法创建的。
03.作用域的分类:
**ES5:**只存在全局作用域和局部作用域(函数作用域)。局限:在if和for循环中声明的变量会变成全局变量;
ES5中通过立即执行函数可以模拟一个块级作用域,封装一些临时变量。
**ES6:**新增了块级作用域的概念,凡是{}包起来的都具有块级作用域,let,const声明的变量只能在块级作用域中使用。
有了块级作用域,就可以不用立即执行函数了。
04.作用域毁销毁:
某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁(全局执行环境直到应用程序退
出——例如关闭网页或浏览器——时才会被销毁)。
05.执行流机制:
每个函数都有自己的执行环境(也就是有与之相关的变量对象)。当执行流进入一个函数时,会立马创建该函数的执行环境,该执行环境会被推入一个环境栈中。
**而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。**ECMAScript 程序中的执行流
正是由这个方便的机制控制着。
06.作用域链:
当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。
作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。
作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其活动对象(activation object)作为变量对象。活动对象在最开始时只包含一个变量,即 arguments 对象(这个对象在全局环境中是不存在的)。作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直延续到全局执行环境;
全局执行环境的变量对象始终都是作用域链中的最后一个对象。
07.变量的查找规则:
沿着作用域链一级一级向上查找。
08.作用域链与原型链的区别:
作用域链是针对变量的查找规则,原型链是针对构造函数的查找规则。
https://www.cnblogs.com/lichunyan/p/7894867.html
//函数声明,存在函数提升
fn();
function fn(){
console.log(111)
};
function (){
}() //这是错误的语法,因为函数声明时必须带有函数名
//函数表达式
//函数表达式是作为表达式语句的一部分存在;当它没有函数名称的时侯,则称为匿名函数;
let fn2=function(){
console.log(111)
}
fn2();
let fn3=function fn(){
consploe.log(222)
}
fn3()
//只有函数表达式才能被执行符号()执行
+function test(){
console.log(222)
}()
//里面的funciton会被解析为一个函数表达式,所以立即执行函数也被称为立即执行的函数表达式
(function(){
})()
function makeFunc() {
var name = "Mozilla";
function displayName() {
alert(name);
}
return displayName;
}
var myFunc = makeFunc();
myFunc();
闭包:
闭包是一种现象,由于存在作用域链的缘故,内部执行环境可以通过作用域链访问到外部执行环境的变量,闭包的产生是由于内部执行环境引用 了外部执行环境的变量,导致该变量在外部函数执行后其引用次数不为0,所以垃圾回收机制不会对该变量进行回收。所以可以一直访问并修改该变量。
立即执行函数其实相当于立即执行的函数表达式。因为立即执行函数是由两个括号组合起来的,js引擎遇到第一个括号时会将其解析为函数表达式,然后遇到第二个括号时就会立即执行。
有什么用:
因为立即执行函数会开辟一个作用域**,**所有可以避免全局变量的污染。比如我们想让一个变量常驻在内存中,就可以在立即执行函数包裹某个函数,让这个函数引用立即执行函数中的变量,这样就会形成一个闭包,该变量不会被垃圾回收机制回收。
应用:
1.循环绑定事件时,为每一次循环绑定自己对应的索引值。(立即执行函数与闭包的结合,让立即执行函数内部的函数引用其变量)
for (var i = 0; i < btns.length; i++) {
((i) => {
btns[i].onclick = function () {
console.log(i);
};
})(i);
}
2.定时器,为每一个定时器函数绑定自己对应的索引值。(立即执行函数与闭包的结合)
for (var i = 0; i < 10; i++) {
((i) => {
setTimeout(() => {
console.log(i);
}, 1000);
})(i);
}
原理都是通过立即执行函数开辟新的作用域,让内部的函数引用立即函数的变量,形成闭包,从而可以保证每一个定时器函数引用的变量值都是不同的。
3.封装js库(开辟一个新的作用域,防止污染全局变量)
应用1,2属于立即执行函数与闭包的结合应用,应用3应用的是立即执行函数会开辟一个新的作用域的特性。
https://www.cnblogs.com/momo798/p/9177767.html
在进行窗口的resize、scroll,输入框内容校验等操作时,如果事件处理函数调用的频率无限制,会加重浏览器的负担,导致用户体验非常糟糕。此时我们可以采用debounce(防抖)和throttle(节流)的方式来减少调用频率,同时又不影响实际效果。
函数的防抖和节流是闭包的充分应用。
函数防抖(debounce):
当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始延时。
// 防抖
function debouce(fn, wait) {
let timeout = null;
return function () {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(fn, wait);
};
}
// 处理函数
function handle() {
console.log(Math.random());
}
// 滚动事件
window.addEventListener("scroll", debouce(handle, 1000));
函数节流(throttle):
当持续触发事件时,保证一定时间段内只调用一次事件处理函数。
方法一:时间戳
function throttle(fn, delay) {
let prev = Date.now();
console.log(prev);
return function () {
let now = Date.now();
if (now - prev > delay) {
fn();
prev = Date.now();
}
};
}
function handle() {
console.log(Math.random());
}
window.addEventListener("scroll", throttle(handle, 1000));
方法二:定时器
function throttle(fn, delay) {
let timer = null;
return function () {
if (!timer) {
timer = setTimeout(function () {
fn();
timer = null;
}, delay);
}
};
}
function handle() {
console.log(Math.random());
}
window.addEventListener("scroll", throttle(handle, 1000));
方法三:setTimeout+requestAnimationFrame(看看就好)
//requestAnimationFrame可以保证每次重绘最多调用一次回调函数。因此可以用来实现节流。
let enabled = true;
function fn() {
console.log(Math.random());
}
window.addEventListener("scroll", () => {
//设置标记变量,可以过滤掉多余requestAnimationFrame的调用
if (enabled) {
enabled = false;
window.requestAnimationFrame(fn);
//手动限制操作执行的频率
window.setTimeout(() => {
enabled = true;
}, 1000);
}
});
节流与防抖的区别:
函数节流不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数,而函数防抖只是在最后一次事件后才触发一次函数。
比如在页面的无限加载场景下,我们需要用户在滚动页面时,每隔一段时间发一次 Ajax 请求,而不是在用户停下滚动页面操作时才去请求数据。这样的场景,就适合用节流技术来实现。
利用闭包来实现:
function createPerson(name) {
var name = name;
function Person() {
}
Person.prototype.getName = function () {
return name;
};
Person.prototype.setName = function (value) {
name = value;
};
return Person;
}
let Person = createPerson("liu");
let person = new Person();
console.log(person.name); //undefined
console.log(person.getName()); //liu
person.setName("haha");
console.log(person.getName()); //haha
let Person2 = createPerson("liu1");
let person2 = new Person2();
console.log(person2.getName()); //liu1
利用闭包来解决,在函数的开始设置一个标记值,当执行完传入的函数后,更改这个标记值。
function sing(arg1, arg2) {
console.log(`i like the singer ${
arg1} and ${
arg2}`);
}
function once(func) {
let b = true;
return function () {
if (b) {
func.apply(null, arguments);
b = false;
} else {
console.log(undefined);
}
};
}
// sing("adele", "wangfei");
let fn = once(sing);
fn("adele", "wangfei"); //i like the singer adele and wangfei
fn("adele", "wangfei"); //undefined
fn("adele", "wangfei"); //undefined
该函数与防抖和节流原理相同,都是利用闭包来实现。
js内存的分配以及无用内存的回收都实现了自动管理。垃圾回收机制的原理是找出不再继续使用的变量,然后释放其占用的内存。垃圾收集器会按照固定的时间间隔周期性地执行这一操作。
垃圾回收方式有两种:
标记清除:
这是最常见的垃圾回收方式,当变量进入环境时,就标记这个变量为”进入环境“,从逻辑上讲,永远不能释放进入环境的变量所占的内存,只要执行流程进入相应的环境,就可能用到他们。当离开环境时,就标记为离开环境。
垃圾回收器在‘运行的时候’会给存储在内存中的所有变量都加上标记,然后去掉环境变量中的变量,以及被环境变量中的变量所引用的变量(条件性去除标记),然后再删除所有被标记的变量,最后垃圾回收器,完成了内存的清除工作,并回收他们所占用的内存。
引用计数法:
另一种不太常见的方法就是引用计数法,引用计数法的含义就是跟踪记录每个值被引用的次数。
当声明了一个变量,并用一个引用类型的值赋值给该变量,则这个值的引用次数为1。如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放。
var arr=[1,2,3,4];
arr=null;//解除arr对[1,2,3,4]引用,这块内存就可以被垃圾回收机制释放了。
引用计数法存在一个问题:循环引用。
function problem() {
var objA = new Object();
var objB = new Object();
objA.someOtherObject = objB;
objB.anotherObject = objA;
}
循环引用导致的问题就是当函数执行完毕后,这两个对象的计数均不为0,如果大量存在这种相互引用,就会导致内存泄漏。
原型对象,对象原型,实例对象的区别:
构造函数默认带有一个prototype属性,这个的属性值是一个对象,即原型对象。
原型对象上有一个constructor属性,指向了这个构造函数。
由构造函数创建的实例对象身上有一个__proto__属性,这个属性指向了构造函数的原型对象,这个属性也被称为对象原型。
原型链:
实例对象的__proto__指向了构造函数的原型对象;
构造函数的原型对象的__proto__指向了Object的原型对象;
Object的原型对象的__proto__指向为null;
所以,原型链顶端是Object构造函数的原型对象。
对象身上属性和方法的查找规则:
依据原型链逐层向上查找。
利用原型链实现继承:
ES6之前JS没有类和继承的概念,但可以通过原型来实现继承。
让子构造函数的原型对象指向父构造函数的实例对象,这样当子构造函数不存在某个方法时,就会去原型对象上找,也即父实例对象,然后再找不到则会去父构造函数的原型对象上找。
注意1:之所以这里子构造函数的原型对象不直接指向父构造函数,是因为如果改变该原型对象,实则更改的是父构造函数的原型对象。
注意2:将原型对象指向父构造函数的实例对象后,原来原型对象上的constructor属性就被覆盖了,因此需要单独为其添加constructor属性,并指向子构造函数。
直接在原数组构造函数的原型对象上添加去重函数即可:
Array.prototype.cut=function(){
let obj={
}
for(let i=0;i<this.length;i++){
let tmp=this[i];
if(obj[tmp]){
this.splice(i,1);
i--;
}else{
obj[tmp]=tmp;
}
}
obj=null;
return this;
}
let arr=[1,2,3,4,3,4,5,6,1];
let newArr=arr.cut();
console.log(newArr)//[1, 2, 3, 4, 5, 6]
Array.prototype.cut=function(){
return [...new Set(this)];
}
let newArr=[11,22,33,44,55,22,33].cut();
console.log(newArr);//[11, 22, 33, 44, 55]
https://blog.csdn.net/lxcao/article/details/52792466
共四步:
1、创建一个空对象 obj
2、设置原型链 obj.__proto__=Func.prototype
3、调用 Func.apply(obj,arguments),让Func中的this指向创建的这个对象,并执行Func的函数体。
4、判断Func的返回值类型。(默认是返回前面创建的对象obj)
如果是值类型,返回原obj。如果是引用类型,就返回这个引用类型的对象。
//这里体现了工厂模式的设计思想,ObjectFactory相当于一个工厂,设计了一个统一的接口,会根据参数生成对应的实例对象。
function Person(name, age) {
this.name = name;
this.age = age;
}
function ObjectFactory() {
// 创建一个空对象 obj
var obj = {
};
var constructor = Array.prototype.shift.call(arguments);
// 设置原型链 obj.__proto__=Func.prototype
obj.__proto__ = constructor.prototype;
// 调用 Func.apply(obj,arguments),让Func中的this指向创建的这个对象,并执行Func的函数体。
var result = constructor.apply(obj, arguments);
// 判断Func的返回值类型。如果是值类型,返回原obj。如果是引用类型,就返回这个引用类型的对象。
return typeof result === "object" ? result : obj;
}
var person = ObjectFactory(Person, "liu", 21);
console.log(person); //Person { name: 'liu', age: 21 }
this的7种绑定:
1.普通函数 window
2.对象的方法 对象
3.构造函数 实例对象
4.事件处理函数 事件源
5.定时器函数 window
6.立即执行函数 window
7.箭头函数(神仙棒,搅屎棍,可能会改变以上所以情况中的this指向,所以在使用箭头函数的时候,需要擦亮双眼看清this指向的到底是谁)
箭头函数中的this是静态的,也就是作用域是不会被改变的,始终指向的是该箭头函数声明时所在的真正的执行环境
//实例1:
let o = {
fn: function () {
setTimeout(() => {
console.log(this);
});
},
};
o.fn();//{fn: ƒ}
//实例2:
function fn2() {
setTimeout(() => {
console.log(this);
});
}
fn2();//window
//实例3:
window.name = "liu1";
let func5 = () => {
console.log(this.name);
};
// 箭头函数中this的指向不会改变
func5.call(obj); //liu1
//实例4:
var a = 0;
function foo(){
console.log(this.a);
};
var obj = {
a : 2,
foo:foo
}
setTimeout(obj.foo,100);//0
//实例4等价于:
var a = 0;
setTimeout(function foo(){
console.log(this.a);
},100);//0
call | apply | bind | |
---|---|---|---|
是否会立即执行函数 | 会 | 会 | 不会,但会返回一个新的函数 |
传递的第二个参数 | arg1,arg2的形式 | 数组的形式 | arg1,arg2的形式 |
实际应用场景 | 实现属性的继承 | 常跟数组有关系,可以借助Math对象查找数组的最大值最小值 | 应用1:定时器函数中默认的this为window,可以通过bind指定其中的this,好处是不会立即调用该函数 应用2:适用于任何想要改变this指向,但不想立即执行的情况,比如绑定事件处理函数时,可以通过bind改变this的指向。 |
//原理:原函数中this的指向是由调用函数的上下文环境决定的,如果将上下文环境改为目标对象,则最终函数内部this指向的就是该目标对象
//简单原理:
function fn1() {
console.log(this.name);
}
var name = "liu";
var obj = {
name: "liu2",
};
obj.fn = fn1;
obj.fn(); //liu2
//具体实现:
function fn1() {
console.log(this.name);
console.log("参数为", ...arguments);
}
var name = "liu";
var obj = {
name: "liu2",
};
Function.prototype.my_call = function (obj) {
//细节点1,判断是否传入第一个参数
obj = obj || window;
//核心:将原函数挂载在目标对象的一个属性上,通过该对象.属性调用该函数时函数内部的指向的就是该目标对象。
obj.fn1 = this;
//var rs = obj.fn1(...[...arguments].slice(1));
var str = "obj.fn(";
for (var i = 1; i < arguments.length; i++) {
str += arguments[i] + ",";
}
str = str.substring(0, str.length - 1);
str += ")";
var rs = eval(str);
//细节点2:删除添加在原对象上的函数属性,不留痕迹,彻彻底底。
delete obj.fn1;
return rs;
};
fn1.my_call(obj, 1, 2, 3); //liu2 参数为 1, 2, 3
fn1.my_call(); //liu 参数为
//原理与实现call函数类似,都是通过改变原函数的调用环境来实现。
function fn() {
console.log(this.name);
console.log("传入的参数为", ...arguments);
}
var name = "liu";
var obj = {
name: "xiao",
};
// fn.apply(obj, [1, 2, 3]);//xiao
Function.prototype.my_apply = function (obj, args) {
obj = obj || window;
//核心与call方法的实现一致
obj.fn = this;
//细节:需要根据第二个参数来决定函数的调用方式
if (args && args.length) {
var str = "obj.fn(";
for (var i = 0; i < args.length; i++) {
str += args[i] + ",";
}
str = str.substring(0, str.length - 1);
str += ")";
var rs = eval(str);
} else {
var rs = obj.fn();
}
delete obj.fn;
return rs;
};
fn.my_apply(obj, [1, 2, 3]);
fn.my_apply(obj);
function fn() {
console.log(this.name);
console.log("传入的参数为", ...arguments);
}
var name = "liu";
var obj = {
name: "liu2",
};
// var newFn = fn.bind(obj, 1, 2, 3, 4);
// newFn(); //liu2
Function.prototype.my_bind = function (obj) {
obj = obj || window;
//核心1:将原函数绑定在目标对象的属性上
obj.fn = this;
const args = arguments;
//核心2;利用闭包返回一个函数
return function () {
var str = "obj.fn(";
for (var i = 1; i < args.length; i++) {
str += args[i] + ",";
}
str = str.substring(0, str.length - 1);
str += ")";
var rs = eval(str);
return rs;
};
};
var newFn = fn.my_bind(obj, 1, 2, 3, 4);
newFn(); //liu2 传入的参数为 1 2 3 4
表示Function构造函数的原型对象,指向的是Object构造函数的原型对象
Object.getPrototypeOf:用于获取对象的原型对象,可以用来代替__proto__;因为__proto__中的下划线可能存在语义性的问题,该属性可能还存在兼容性的问题。
let object = {
};
console.log(Object.getPrototypeOf(object) === object.__proto__); //true
function Animal(name, size, likes) {
this.name = name || "Animal";
this.size = size || "small";
this.sleep = function () {
console.log(this.name + " is sleeping");
};
this.likes = likes || [];
}
Animal.prototype.eat=function(food){
console.log(this.name+'is eating '+food);
}
//核心: 将父类的实例作为子类的原型
//优点:简单,容易实现
//缺点:无法实现多继承,无法向父类构造函数传参;原型对象上的所有属性被所有实例共享,只有一份
function Cat() {
}
Cat.prototype = new Animal(); //核心代码
Cat.prototype.name = "tom";
var cat = new Cat();
console.log(cat.name);
cat.sleep();
console.log(cat instanceof Cat); //true
console.log(cat instanceof Animal); //true 即子类的实例,也是父类的实例
var cat2 = new Cat();
cat.likes.push("play");
console.log(cat2.likes); //[ 'play' ]
//优点:可以向父类传递参数,可以实现多继承(call多个父类实例)
//缺点:
//实例并不是父类的实例,只是子类的实例
//只能继承父类的实例属性和方法,不能继承原型属性/方法
function Cat(name) {
Animal.call(this,name); //核心代码
}
var cat = new Cat("tom");
console.log(cat.name); //tom
cat.sleep(); //tom is sleeping
// 核心:在子构造函数中创建一个父类实例,可以在上面添加子类的新特性,然后返回这个对象
// 特点:可以通过new调用,也可直接调用
// 缺点:实例是父类的实例,不是子类的实例; 不支持多继承; 而且在子构造函数的原型对象上添加方法是无效的,根据new关键字的执行过程可知。
function Cat(name) {
var instance = new Animal(); //核心代码
instance.name = name || "Tom";
return instance;
}
Cat.prototype.sing = function () {
console.log("sing a song");
};
var cat = new Cat("jim");
console.log(cat.name); //jim
cat.sleep(); //jim is sleeping
cat.sing(); // cat.sing is not a function
// 利用for..in可以遍历对象的原型对象上的属性的特性
// 特点:可以多继承
// 缺点:因为存在拷贝,内存占用高;无法获取父类不可枚举的方法(for in 只能获取枚举的属性)
function Cat(name) {
var animal = new Animal();
for (key in animal) {
Cat.prototype[key] = animal[key]; //核心代码
}
Cat.prototype.name = name || "tom";
}
var cat = new Cat("jim");
console.log(cat.name);
cat.sleep();
console.log(cat instanceof Animal); //false
console.log(cat instanceof Cat); //true
// 原型链继承+构造继承
// 优点:
// 弥补了原型链继承的局限性,可以传参;不存在引用属性共享的问题,因为代码1将实例属性绑定在了实例对象上,不需要再去原型对象上找属性了。
// 弥补了构造继承的局限性,可以继承原型上的属性和方法,既是子类的实例,也是父类的实例。
// 缺点:初始化了两次实例方法/属性,多消耗了一些内存
function Cat(name, size) {
Animal.call(this, name, size); //核心代码1
}
Cat.prototype = new Animal(); //核心代码2
var cat = new Cat("alice", "big");
console.log(cat.name); //alice
console.log(cat.size); //big
cat.sleep(); //alice is sleeping
// 解决组合继承中的冗余问题,将 实例属性/方法的继承 与 原型对象上属性/方法的继承 分开
function Cat(name, size) {
Animal.call(this, name, size); //核心代码1:继承实例属性和方法
(function () {
var Super = function () {
};
Super.prototype = Animal.prototype; //核心代码2:通过寄生方式,砍掉父类的实例方法/属性,这样就不会初始化两次实例方法/属性,避免的组合继承的缺点
Cat.prototype = new Super();
})();
}
var cat = new Cat("alice", "big");
console.log(cat.name); //alice
console.log(cat.size); //big
cat.sleep(); //alice is sleeping
class Animal {
constructor(name, size) {
this.name = name;
this.size = size;
}
sing() {
console.log("sing a song");
}
}
class Cat extends Animal {
constructor(name, size) {
super(name, size);
}
eat() {
console.log("i like eating mouse");
}
}
var cat = new Cat("tom", "small");
console.log(cat); //Cat { name: 'tom', size: 'small' }
cat.sing(); //sing a song
cat.eat();
``