JavaScript 常见知识总结
JavaScript 定义了七种内建类型:
null
undefined
boolean
number
string
object
symbol
– 在 ES6 中被加入的!注意: 除了 object
,所有这些类型都被称为“基本类型(primitives)”。
typeof 操作符可以检测给定值的类型,而且总是返回七种字符串值中的一种
typeof undefined === "undefined"; // true
typeof true === "boolean"; // true
typeof 42 === "number"; // true
typeof "42" === "string"; // true
typeof { life: 42 } === "object"; // true
// 在 ES6 中被加入的!
typeof Symbol() === "symbol"; // true
例外:null
类型
typeof null === "object"; // true
//正确方法
var a = null;
(!a && typeof a === "object"); // true
原因:这是个历史遗留bug。
JS类型值是存在32 BIT 单元里,32位有1-3位表示TYPE TAG,其它位表示真实值
而表示object的标记位正好是低三位都是0
1:整型(int)
000:引用类型(object)
010:双精度浮点型(double)
100:字符串(string)
110:布尔型(boolean)
另外还用两个特殊值:
undefined,用整数−2^30(负2的30次方,不在整型的范围内)
null,机器码空指针(C/C++ 宏定义),低三位也是000
typeof
可以返回的第七种字符串值是function
:
typeof function a(){ /* .. */ } === "function"; // true
数组是一个object
:
typeof [1,2,3] === "object"; // true
1.instanceof方法能准确的识别变量的所属具体对象
instanceof 运算符用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。
语法
object instanceof constructor
描述
instanceof 运算符用来检测 constructor.prototype 是否存在于参数 object 的原型链上。
typeof和instanceof都可以用来判断变量,它们的用法有很大区别
typeof会返回一个变量的基本类型,instanceof返回的是一个布尔值
var num = new Number(10);
console.log(num instanceof Number);//返回true
var arr = new Array(1,2,3);
console.log(arr instanceof Array); // true
通俗来讲,instanceof就是判断变量是否属于某种类型。
在任何值上调用 Object 原生的 toString() 方法,都会返回一个 [object NativeConstructorName] 格式的字符串。每个类在内部都有一个 [[Class]] 属性,这个属性中就指定了上述字符串中的构造函数名。但是它不能检测非原生构造函数的构造函数名。
要想区分对象、数组、函数、单纯使用typeof是不行的。在JS中,可以通过Object.prototype.toString方法,判断某个对象之属于哪种内置类型。
分为Null、String、Boolean、Number、Undefined、Array、Function、Object、Date、Math
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(“abc”);// "[object String]"
Object.prototype.toString.call(123);// "[object Number]"
Object.prototype.toString.call(true);// "[object Boolean]"
**函数类型**
Function fn(){
console.log(“test”);
}
Object.prototype.toString.call(fn); // "[object Function]"
**日期类型**
var date = new Date();
Object.prototype.toString.call(date); // "[object Date]"
**数组类型**
var arr = [1,2,3];
Object.prototype.toString.call(arr); // "[object Array]"
**正则表达式**
var reg = /[hbc]at/gi;
Object.prototype.toString.call(reg); // "[object RegExp]"
**自定义类型**
function Person(name, age) {
this.name = name;
this.age = age;
}
var person = new Person("Rose", 18);
Object.prototype.toString.call(person); // "[object Object]"
很明显这种方法不能准确判断person是Person类的实例,而只能用instanceof 操作符来进行判断,如下所示:
console.log(person instanceof Person); // true
var isNativeJSON = window.JSON && Object.prototype.toString.call(JSON);
console.log(isNativeJSON);// 输出结果为”[object JSON]”说明JSON是原生的,否则不是
null
undefined
boolean
number
string
symbol
– 在 ES6 中被加入的!基本数据类型是指存放在栈中的简单数据段,数据大小确定,内存空间大小可以分配,它们是直接按值存放的,所以可以直接按值访问和操作。
object
:
String
Number
Boolean
Object
Array
Function
Date
RegExp
Error
引用数据类型是保存在堆内存中的对象。不可以直接访问堆内存空间中的位置和操作堆内存空间。只能操作对象在栈内存中的引用地址。引用类型数据在栈内存中保存的实际上是对象在堆内存中的引用地址。通过这个引用地址可以快速查找到保存中堆内存中的对象。
var a = [1,2,3,4,5];
var b = a;//传址 ,对象中传给变量的数据是引用类型的,会存储在堆中;
var c = a[0];//传值,把对象中的属性/数组中的数组项赋值给变量,
//这时变量C是基本数据类型,存储在栈内存中;改变栈中的数据不会影响堆中的数据
alert(b);//1,2,3,4,5
alert(c);//1
//改变数值
b[4] = 6;
c = 7;
alert(a[4]);//6
alert(a[0]);//1
这就是传值与传址的区别。因为a是数组,属于引用类型,所以它赋予给b的时候传的是栈中的地址(相当于新建了一个不同名“指针”),而不是堆内存中的对象。而c仅仅是从a堆内存中获取的一个数据值,并保存在栈中。所以b修改的时候,会根据地址回到a堆中修改,c则直接在栈中修改,并且不能指向a堆内存中。
在JS中,基本数据类型变量大小固定,并且操作简单容易,所以把它们放入栈中存储。
引用类型变量大小不固定,所以把它们分配给堆中,让他们申请空间的时候自己确定大小,这样把它们分开存储能够使得程序运行起来占用的内存最小。
栈内存由于它的特点,所以它的系统效率较高。
堆内存需要分配空间和地址,还要把地址存到栈中,所以效率低于栈。
无论什么时候,只要创建了一个函数,就会根据为该函数创建一个 prototype
属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会获得一个 constructor
,该属性是一个指向 prototype
属性所在函数的指针。
原型链规定了对象如何查找属性,对于一个对象来说,如果它本身没有某个属性,则会沿着原型链一直向上查找,知道找到属性或者查找完整个原型链。
原型链是实现继承的主要方法,其基本思想是利用原型链让一个引用类型继承另一个引用类型的属性和方法。
obj = Object.create(null)
创建一个没有任何属性的对象
而obj = {}
会创建一个从Object继承属性和方法的对象(原型为Object)
作用:查找引用类型的属性
在JavaScript中,对象内部都有一个原型对象,当一个对象查询某个属性或方法的时候,JavaScript引擎首先会搜索对象本身
是否存在答案,如果没有,就会去它的原型对象中继续搜索
,如果没有,再去它的原型的原型中去找
,这就形成了一个原型链。直到Object的prototype为止
。
在对象使用属性或调用方法的时候,会优先在自身的属性中寻找,如果找不到就去隐式原型__proto__
里面依次寻找,如果找不到就返回null
,我们把__proto__
与prototype 的链条关系称为“原型链”。js对象就是通过原型链,实现属性的继承。
实例对象的__proto__
指向构造函数的prototype
,从而实现继承。
prototype
对象相当于特定类型所有实例对象都可以访问的公共容器
function Person() {} //创建构造函数
var person = new Person(); //使用 new 创建一个实例对象 person
console.log(person.__proto__ == Person.prototype) // true
console.log(Person.prototype.constructor == Person) // true
console.log(person.constructor === Person); // true
//当获取 person.constructor 时,其实 person 中并没有 constructor 属性,当不能读取到constructor 属性时,会从 person 的(隐式)原型也就是 person.__proto__(Person的显式原型Person.prototype) 中读取,正好原型中有该属性,所以:
console.log(person.constructor === Person.prototype.constructor); true
var obj = new Object();
console.log(Person.prototype.__proto__ === Object.prototype) // true
console.log(obj.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true
// ES5方法,获得对象的原型
//__proto__属性并不存在于 Person.prototype 中,实际上,它是来自于 Object.prototype ,与其说是一个属性,
//不如说是一个 getter/setter,当使用 obj.__proto__ 时,可以理解成返回了 Object.getPrototypeOf(obj)。
console.log(obj.__proto__ === Object.getPrototypeOf(obj)); // true
console.log(Object.getPrototypeOf(person) === Person.prototype) // true
Person
:构造函数。构造函数本身就是一个函数,与普通函数没有任何区别,不过为了规范一般将其首字母大写。构造函数和普通函数的区别在于,使用new生成实例的函数就是构造函数,直接调用的就是普通函数。Person.prototype
:实例原型,也叫原型对象,包含所有实例共享的属性和方法。prototype表示该函数的显式原型,函数的 prototype 属性指向了一个对象,这个对象正是调用该构造函数而创建的实例(person
)的原型(person.__proto__
)。这个属性是原型对象⽤来创建新对象实例,⽽所有被创建的对象都会共享原型对象,因此这些对象便可以访问原型对象的属性。person
:实例(对象)。通过new
调用Person
构造函数所创建的一个实例对象。person.__proto__
:隐式原型。这是每一个JavaScript对象(除了 null )都具有的一个属性,叫__proto__
,这是一个访问器属性(即 getter 函数和 setter 函数),会指向该对象的原型
(构造该对象的构造函数的原型)。通过它可以访问到对象的内部[[Prototype]]
(一个对象或 null )。constructor
:返回创建实例对象时构造函数的引用,即返回实例原型的构造函数的引用。此属性的值是对函数本身的引用,而不是一个包含函数名称的字符串。Object()
:Object
构造函数,创建一个对象包装器,会根据给定的参数创建对象(实例)。在 JavaScript 中,几乎所有的对象都是 Object
类型的实例,它们都会从 Object.prototype
继承属性和方法。Object.prototype()
:原型对象就是通过Object
构造函数生成的,实例的 __proto__
指向构造函数的 prototype
,可以理解成,Object.prototype()
是所有对象的根对象。每个对象拥有一个原型对象,通过 __proto__
指针指向上一个原型 ,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向 null
,因为Object.prototype
的原型是null
。这种关系被称为原型链 (prototype chain),通过原型链一个对象会拥有定义在其他对象中的属性和方法。
当一个对象想要查找某个方法或属性时,如果对象本身没有,则会去对象的原型对象中查找,一层层往上查找,直到Object.prototype
。
小结:
typeof Function // "function"
typeof Function.prototype // "function"
第一行的结果,我们大家都能理解,第二行的运行结果大家估计就够呛了,按照往常的理解,构造函数的原型肯定是对象;例如:var fn=function(){};
var fn=function(){};
typeof fn; // "function"
typeof fn.prototype; // "object"
继承是指一个对象直接使用另外一个对象的属性和方法
先创建一个Person
类
function Person (name, age) {
this.name = name
this.age = age
}
// 方法定义在构造函数的原型上
Person.prototype.getName = function () {
console.log(this.name)
}
想创建一个Teacher
类,我希望它可以继承Person
所有的属性,并且额外添加属于自己特定的属性:subject——这个属性包含了教师教授的学科。
定义Teacher
的构造函数
function Teacher (name, age, subject) {
Person.call(this, name, age)
this.subject = subject
}
属性的继承是通过在一个类内执行另外一个类的构造函数,通过call指定this为当前执行环境,这样就可以得到另外一个类的所有属性。
Person.call(this, name, age)
方法都定义在prototype里,那其实我们只需要把Person.prototype
的备份赋值给Teacher.prototype
即可
Teacher.prototype = Object.create(Person.prototype)
Object.create()
方法创建一个新对象(new Teacher()
),使用现有的对象来提供新创建的对象的__proto__
,即构造函数Teacher()
创建的实例的__proto__
,即Teacher.prototype
。
为什么是备份呢?
因为如果直接赋值,那会是引用关系,意味着修改Teacher. prototype
,也会同时修改Person.prototype
,这是不合理的。
另外注意一点就是,在给Teacher
类添加方法时,应该在修改prototype
以后,否则会被覆盖掉,原因是赋值前后的属性值是不同的对象。
最后还有一个问题,我们都知道prototype
里有个属性constructor
指向构造函数本身,但是因为我们是复制其他类的prototype
,所以这个指向是不对的,需要更正一下。
如果不修改,会导致我们类型判断出错
Teacher.prototype.constructor = Teacher
继承方法的最终方案:
Teacher.prototype = Object.create(Person.prototype)
Teacher.prototype.constructor = Teacher
在原型链上查询属性比较耗时,对性能有影响,试图访问不存在的属性时会遍历整个原型链。
遍历对象属性时,每个可枚举的属性都会被枚举出来。 要检查是否具有自己定义的属性,而不是原型链上的属性,必须使用hasOwnProperty
方法。
hasOwnProperty
是 JavaScript 中唯一处理属性并且不会遍历原型链的方法。
//遍历对象和子对象
var obj = {
name: "zhang" ,
age: 18,
son : {
name: "wang",
age: 2
}
}
for(var prop in obj){
//排除原型链上的属性
if(obj.hasOwnProperty(prop)) {
console.log(obj[prop]);
}
}
// 父类
class Person {
constructor(name, age) {
console.log(`构造函数执行了,${name}`);
this.name = name;
this.age = age;
}
getPersonName() {
return `名字为${this.name}`;
}
}
let p1= new Person('jona');
console.log(p1.getPersonName());
// 子类
class Teacher extends Person {
constructor(args, subject) {
// super() 指的是调用父类
// 调用的同时,会绑定 this 。
// 如:Foo.call(this, who)
super(args);
this.subject = subject;
}
getTeacherName(){
return super.getPersonName();//调用父级的方法也是用super
}
}
let p2 = new Teacher('子类');
console.log(p2.getTeacherName());
先创建父类,然后通过extends
关键字创建子类,通过super关键字将父类的属性和方法继承到子类.
function SuperType() {
this.name = "super"
}
function SubType() {}
// 利用原型链实现继承
SubType.prototype = new SuperType()
var instance1 = new SubType()
console.log(instance1.name) // super
简单的原型继承存在以下两个问题:
call()
和 apply()
方法在新创建的对象上执行构造函数。function SuperType(age, name) {
this.colors = ["blue", "red"]
this.age = age
this.name = name
}
function SubType() {
SuperType.call(this, ...arguments)
}
var instance1 = new SubType(23, "sillywa")
instance1.colors.push("yellow")
console.log(instance1.colors, instance1.name)
var instance2 = new SubType(12, "xinda")
console.log(instance2.colors, instance2.name)
借用构造函数继承也有一些缺点,比如方法都只能在构造函数中定义,没有办法实现方法的复用。
function SuperType(name) {
this.name = name
this.colors = ["red", "yellow"]
}
// 方法写在原型上
SuperType.prototype.sayName = function() {
return this.name
}
function SubType(name, age) {
// 通过 构造函数继承属性
SuperType.call(this, name)
this.age = age
}
// 通过原型继承方法
SubType.prototype = new SuperType()
// 重写了 SubType 的 prototype 属性,因此其 constructor 也被重写了,需要手动修正
SubType.prototype.constructor = SubType
// 定义子类自己的方法
SubType.prototype.sayAge = function() {
return this.age
}
function create(o) {
function F(){}
F.prototype = o
return new F()
}
在传入一个参数的情况下,这个函数行为等同于Object.create()
Object.create() 方法的第二个参数与 Object.defineProterties() 方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属性。例如:
var person = {
name: "sillywa"
}
var person1 = Object.create(person, {
name: {
value: "John"
}
})
console.log(person1.name) // John
function createAnother(original) {
var clone = Object.create(original)
clone.sayHi = function() {
console.log("Hi")
}
return clone
}
在 ES6 规范中,引入了 class 的概念。使得 JS 开发者终于告别了,直接使用原型对象模仿面向对象中的类和类继承时代。
但是JS 中并没有一个真正的 class 原始类型, class 仅仅只是对原型对象运用语法糖。
typeof className == "function"
通过类型判断,我们可以得知,class 的并不是什么全新的数据类型,它实际只是 function
(或者说 object
)。
class 定义属性
当我们使用 class 定义属性(方法)的时候,实际上等于是在 class 的原型对象上定义属性。
class Foo {
constructor(){ /* constructor */ }
describe(){ /* describe */ }
}
// 等价于
function Foo (){
/* constructor */
}
Foo.prototype.describe = function(){ /* describe */ }
constructor
是一个比较特殊的属性,它指向构造函数(类)本身。可以通过以下代码验证。
Foo.prototype.constructor === Foo // true
在传统面向对象中,类是可以继承类的。这样子类就可以复制父类的方法,达到代码复用的目的。
ES6 也提供了类继承的语法 extends,如下:
class Foo {
constructor(who){
this.me = who;
}
//静态方法
static displayName = "Foo";
static identity() {
return "I am " + this.me;
}
identify(){
return "I am " + this.me;
}
}
class Bar extends Foo {
constructor(who){
// super() 指的是调用父类
// 调用的同时,会绑定 this 。
// 如:Foo.call(this, who)
super(who);
}
speak(){
alert( "Hello, " + this.identify() + "." );
console.log(this.identify === super.identify);// 这里的super和this等价
}
}
Foo.displayName // "Foo"
var b1 = new Bar( "b1" );
b1.speak();
如果子类中定义了构造函数,那么它必须先调用 super()
才能使用 this 。
当实例 b1 调用 speak 方法时,b1 本身没有 speak,所以会到 Bar.prototype 原型对象上查找,并且调用原型对象上的 speak 方法。调用 identify 方式时,由于 this 指向的是 b1 对象。所以也会先在 b1 本身查找,然后沿着原型链,查找 Bar.prototype,最后在 Foo.prototype 原型对象上找到 identify 方法,然后调用。
Parent.prototype.constructor.call(this)
class Parent {
static myMethod(msg) {
console.log('static', msg);
}
myMethod(msg) {
console.log('instance', msg);
}
}
class Child extends Parent {
static myMethod(msg) {
super.myMethod(msg);
}
myMethod(msg) {
super.myMethod(msg);
}
}
Child.myMethod(1); // static 1
var child = new Child();
child.myMethod(2); // instance 2
作用:
闭包就是能够读取其他函数内部变量的函数。
作用:
第一个就是可以读取自身函数外部的变量(沿着作用域链寻找)
第二个就是让这些外部变量始终保存在内存中
由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数,且封闭了定义时的环境"。
所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
function foo() {
console.log(a);
}
function bar() {
let a = 3;
foo();
}
let a = 2;
bar(); // 2
之所以输出 2,是因为 foo 是一个闭包函数
。
闭包就是 函数能够记住并访问它的词法作用域,即使当这个函数在它的词法作用域之外执行时。
前面说过,词法作用域也叫静态作用域,变量在词法阶段确定,也就是定义时确定。虽然在 bar 内调用,但由于 foo 是闭包函数
,即使它在自己定义的词法作用域以外的地方执行,它也一直保持着自己的作用域
。所谓闭包函数,即 这个函数封闭了它自己的定义时的环境,形成了一个闭包,所以 foo 并不会从 bar 中寻找变量,这就是静态作用域的特点。
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 2 -- 哇噢,看到闭包了,伙计。
有赖于它被声明的位置,bar()
拥有一个词法作用域闭包
覆盖着 foo()
的内部作用域
,闭包为了能使 bar()
在以后任意的时刻可以引用这个作用域而保持它的存在。
bar()
依然拥有对那个作用域的引用,而这个引用称为闭包
。
所以,在几微秒之后,当变量 baz 被调用时(调用我们最开始标记为 bar 的内部函数),它理所应当地对编写时的词法作用域
拥有 访问权,所以它可以如我们所愿地访问变量 a。
这个函数在它被编写时的词法作用域之外
被调用。闭包 使这个函数可以继续访问它在编写时被定义的词法作用域
。
无论我们使用什么方法将内部函数 传送 到它的词法作用域之外,它都将维护一个指向它最开始被声明时的作用域的引用
,而且无论我们什么时候执行它,这个闭包
就会被行使。
闭包:一个函数声明时,对自己的声明时的作用域进行一个打包并绑定,无论这个函在词法作用域内部还是外部执行,其引用的作用域一直为自己定义时的作用域
。而这个引用称为闭包
。
块作用域和闭包携手工作:
for (let i=1; i<=5; i++) {
setTimeout( function timer(){
console.log( i );
}, i*1000 );
}
在用于 for 循环头部的 let
声明被定义了一种特殊行为。这种行为说,这个变量将不是只为循环声明一次,而是为每次迭代声明一次。并且,它将在每次后续的迭代中被上一次迭代末尾的值初始化。
当然,闭包的作用域链中保存的元素,该元素将无法被销毁,在垃圾回收时不会被收回。如果保存元素为一个引用变量,而且不是必须要保存的,那么它也会因此被保存下来占据大量的内存,造成内存泄漏。
如何避免闭包引起的内存泄漏
闭包和闭包引起的内存泄露
JavaScript程序每次创建字符串、数组或对象时,解释器都必须分配内存来存储那个实体。只要像这样动态地分配了内存,最终都要释放这些内存以便他们能够被再用。对于持续运行的服务进程(daemon),必须及时释放不再用到的内存。否则,JavaScript的解释器将会消耗完系统中所有可用的内存,造成系统崩溃。
如何识别内存泄漏
document.body.removeChild(document.getElementById('button'))
用weakMap/weakSet存储dom节点最后可以被回收,防止内存泄漏。
引用计数算法定义“内存不再使用”的标准很简单,就是看一个对象是否有指向它的引用。 如果没有其他对象指向它了,说明该对象已经不再需要了。
引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其所占的内存空间给收回来。这样,垃圾收集器下次再运行时,它就会释放那些引用次数为0的值所占的内存。
问题:循环引用,如果两个对象相互引用,尽管他们已不再使用,垃圾回收不会进行回收,导致内存泄露;
解决:手动解除引用
当变量进入执行环境时,就标记这个变量为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到他们。当变量离开环境时,则将其标记为“离开环境”。
JavaScript 中有个全局对象,浏览器中是 window。定期的,垃圾回收期将从这个全局对象开始,找所有从这个全局对象开始引用的对象,再找这些对象引用的对象…对这些活着的对象进行标记,这是标记阶段。清除阶段就是清除那些没有被标记的对象。
工作流程:
问题:
效率较低;因为在标记-清除阶段,整个程序将会等待,所以如果程序出现卡顿的情况,那有可能是收集垃圾的过程。
在清除之后,内存空间是不连续的,即出现了内存碎片。如果后面需要一个比较大的连续的内存空间时,那将不能满足要求。而标记-整理方法可以有效地解决这个问题。标记阶段没有什么不同,只是标记结束后,标记-整理方法会将活着的对象向内存的一边移动,最后清理掉边界的内存。不过可以想象,这种做法的效率没有标记-清除高。
JavaScript 的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript 的主要用途是与用户互动,以及操作 DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。 比如,假定 JavaScript 同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
js 是单线程执行的,js中的任务按顺序一个一个的执行。如果一个任务耗时过长,后边一个任务也得等着,因此将任务分为了同步任务和异步任务;而异步任务又可以分为微任务和宏任务。
当我们打开网站时,网页的渲染过程就是一大堆同步任务,像页面骨架和页面元素的渲染,而加载图片、音乐之类的任务就是异步任务。
同步任务在主线程上执行,异步任务放在主线程之外的一个任务队列。主线程执行完毕后,读取任务队列的内容。
(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。"任务队列"是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。
当前主线程上执行的就是一个宏任务。例: 整体script 的代码、setTimeout、setInterval、postMessage、I/O、html解析过程、获取资源(fetch、XMLHttpRequest)、DOM操作的响应UI Rendering等。
例:Promise.then catch finally(注意不是说 Promise,new promise直接执行)、process.nextTick、await后面的代码。process.nextTick优先级最高,总是最先执行。
JS异步还有一个机制,就是遇到宏任务,先执行宏任务,将宏任务放入宏任务的event queue,然后再执行微任务,将微任务放入微任务的eventqueue,这两个queue不是一个queue。当你往外拿的时候先从微任务里拿这个回调函数,然后再从宏任务的queue拿宏任务的回调函数,如下图:
首先执行 宏任务 => 微任务的Event Queue => 宏任务的Event Queue
当前 宏任务 执行完毕后,会先查看微任务队列,如果有任务,优先执行,否则执行下一个宏任务。
上述过程会不断重复,也就是常说的Event Loop(事件循环)。
async function async1() {
console.log("async1 start");//2
// 第三个宏任务
await setTimeout(() => {
console.log("async1 end");//9
new Promise(function (reslove) {
reslove();
}).then(function () {
// 第三个宏任务中的第一个微任务
console.log("promise4");//10
});
}, 0);
async2();//第一个宏任务中的第一个微任务
}
async function async2() {
console.log("async2");// 5
}
//--------------------------------上面是函数的声明
console.log("script start");// 第一个宏任务(script)1
// 第二个宏任务
setTimeout(() => {
console.log("setTimeOut");// 7
new Promise(function (reslove) {
reslove();
}).then(function () {
// 第二个宏任务中的第一个微任务
console.log("promise3"); //8
});
}, 0);
async1();//在第一个宏任务下执行
//在第一个宏任务下执行
new Promise(function (reslove) {
console.log("promise1");//正常执行 3
reslove();
}).then(function () {
console.log("promise2");//第一个宏任务下的第二个微任务 6
});
console.log("script end");// 4
宏任务和微任务到底是什么?
渲染是微任务完成之后执行:
不要在.then()中写耗时的循环,会影响后续Dom渲染以及宏任务的释放。
微任务和宏任务的区别
宏任务:DOM渲染后触发,由浏览器规定(Web APIs)
微任务:DOM渲染前执行,微任务是ES6语法规定
宏任务 -> 微任务 -> DOM渲染 -> 下一轮宏任务 -> …
JavaScript 中,变量可以在使用后声明,也就是变量可以先使用再声明。
以下两个实例将获得相同的结果:
a = 2;
var a;
console.log(a);//输出2
var a;
a = 2;
console.log(a);//输出2
变量提升(hoisting)会将当前作用域的所有变量的声明提升到程序的顶部。
console.log(a);//输出undefined
var a = 2;
var a = 2是分为两步的:
var a;
a = 2;
而js只会将第一步提升到顶部,所以上面的语句等价于:
var a;
console.log(a);
a = 2;
js和其他语言一样,都要经历编译和执行阶段。而js在编译阶段的时候,会搜集所有的变量声明并且提前声明变量,而其他的语句都不会改变他们的顺序,因此,在编译阶段的时候,第一步就已经执行了,而第二步则是在执行阶段执行到该语句的时候才执行。
那为什么会进行变量提升呢?主要有以下两个原因:
(1)提高性能
在JS代码执行之前,会进行语法检查和预编译,并且这一操作只进行一次。这么做就是为了提高性能,如果没有这一步,那么每次执行代码前都必须重新解析一遍该变量(函数),而这是没有必要的,因为变量(函数)的代码并不会改变,解析一遍就够了。
在解析的过程中,还会为函数生成预编译代码。在预编译时,会统计声明了哪些变量、创建了哪些函数,并对函数的代码进行压缩,去除注释、不必要的空白等。这样做的好处就是每次执行函数时都可以直接为该函数分配栈空间(不需要再解析一遍去获取代码中声明了哪些变量,创建了哪些函数),并且因为代码压缩的原因,代码执行也更快了。
(2)容错性更好
变量提升可以在一定程度上提高JS的容错性,看下面的代码:
a = 1;
var a;
console.log(a);
如果没有变量提升,这两行代码就会报错,但是因为有了变量提升,这段代码就可以正常执行。
虽然,我们在可以开发过程中,可以完全避免这样写,但是有时代码很复杂的时候。可能因为疏忽而先使用后定义了,这样也不会影响正常使用。由于变量提升的存在,而会正常运行。
总结:
二者优先级:函数提升会优先于变量提升,而且不会被同名的变量覆盖,但是,如果这个同名变量已经赋值了,那函数变量就会被覆盖。当二者同时存在时,会先指向函数声明。
console.log(a); //f a() {...}
console.log(a()); //2
var a = 1;
function a() {
console.log(2);
}
console.log(a); //1
a = 3;
console.log(a()); //报错,现在的函数a已经被赋值过后的变量a给覆盖了,无法再调用a()
上述代码相当于如下:
function a() {
console.log(2);
}
var a;
console.log(a); //f a() {...}
console.log(a()); //2
a = 1;
console.log(a); //1
a = 3;
console.log(a()); //报错,现在的函数a已经被赋值过后的变量a给覆盖了,无法再调用a()
比较安全可靠:对var或者是直接声明全局变量来说,变量都可以未声明或者在声明语句之前就使用,而使用了let之后,该变量必须在其声明语句后,才能使用,否则就会报错。这就在一定程度上避免了变量滥用的情况。
var i = 5; // j的临时性死区
(function hh() {
var i
console.log(i) // undefined
i = 10
})() // j的临时性死区
// j的临时性死区
let j = 55; // 接下来可以愉快的使用let了
console.log(j)
console.log(j+10)
(function hhh() {
console.log(j) // 新的j的临时性死区
let j = 77 //又有一个声明语句,从这个函数的开始部分到这里,都是新的j的临时性死区
})()
foo();
function foo() {
console.log('foo');//输出foo
}
var foo = 2;
foo();
function foo() {
console.log('1');
}
function foo() {
console.log('2');//输出2
}
函数声明会被提升,但是函数表达式不会。
foo();
var foo = function() {
console.log('foo');//输出TypeError
}
等价于:
var foo;
foo(); // TypeError
bar(); // ReferenceError
foo = function() {
var bar = ...self...
// ...
}
它其实也是分为两部分,一部分是var foo,而一部分是foo = function() {}。这道题的结果应该是报了TypeError(因为foo声明但未赋值,因此foo是undefined)。
call
、bind
、 apply
这三个函数的第一个参数都是 this
的指向对象,用来改变函数执行时的this指向
call()
: 通过 foo.call(obj)
使用 明确绑定 来调用 foo
,允许我们强制函数的 this
指向 obj
。call
的参数是直接放进去的,通过逗号隔开。apply()
: apply
的所有其他参数都必须放在一个数组或类数组里面传进去。this
绑定的角度讲,call(..)
和 apply(..)
是完全一样的。bind()
:bind(..)
返回一个硬编码的新函数,它使用你指定的 this 环境来调用原本的函数,需要重新调用执行这个新函数,而 apply
和 call
则是立即调用。。它的参数传递和 call 一样。bind
的第一个参数是 null
或者 undefined
,this
就指向全局对象 window
。function foo(a, b, c) {
console.log(this.a, a, b, c);
return this.a + a + b + c;
}
var obj = {
a: 2
};
var b = foo.call(obj, 3, 4, 5); // 2 3 4 5
console.log(b); // 14
var c = foo.apply(obj, [3, 4, 5]); // 2 3 4 5
console.log(c); // 14
var baz = foo.bind(obj, 3, 4, 5);
var d = baz(3); // 2 3 4 5
console.log(d); // 14
Object.create()
方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
。
// 父类
function supFather(name) {
this.name = name;
this.colors = ['red', 'blue', 'green']; // 复杂类型
}
supFather.prototype.sayName = function (age) {
console.log(this.name, 'age');
};
// 子类
function sub(name, age) {
// 借用父类的方法:修改它的this指向,赋值父类的构造函数里面方法、属性到子类上
supFather.call(this, name);
// 创建新属性
this.age = age;
}
// 重写子类的prototype,修正constructor指向
function inheritPrototype(sonFn, fatherFn) {
// sonFn.prototype的原型为fatherFn.prototype
sonFn.prototype = Object.create(fatherFn.prototype); // 继承父类的属性以及方法
sonFn.prototype.constructor = sonFn; // 修正constructor指向到继承的那个函数上
}
inheritPrototype(sub, supFather);
sub.prototype.__proto__ === supFather.prototype // true
sub.prototype.sayAge = function () {
console.log(this.age, 'foo');
};
// 实例化子类,可以在实例上找到属性、方法
const instance1 = new sub("OBKoro1", 24);
const instance2 = new sub("小明", 18);
instance1.colors.push('black')
console.log(instance1) // {"name":"OBKoro1","colors":["red","blue","green","black"],"age":24}
console.log(instance2) // {"name":"小明","colors":["red","blue","green"],"age":18}
var name = '小红';
var b = {
name: '小白',
detail: function(){
console.log(this.name);
}
}
b.detail(); //小白
var c = b.detail;
c(); //小红
this 不是编写时绑定,而是运行时绑定。 它依赖于函数调用的上下文条件。this 绑定与函数声明的位置没有任何关系,而与函数被调用的方式紧密相连。
当一个函数被调用时,会建立一个称为执行环境的活动记录。这个记录包含函数是从何处(调用栈 —— call-stack)被调用的,函数是 如何 被调用的,被传递了什么参数等信息。这个记录的属性之一,就是在函数执行期间将被使用的 this 引用。
this 既不是函数自身的引用,也不是函数 词法 作用域的引用。
this 实际上是在函数被调用时建立的一个绑定,它指向 什么 是完全由函数被调用的调用点来决定的。
独立函数调用,全局作用域中声明变量/调用函数。这种 this 规则是在没有其他规则适用时的默认规则。
另一种要考虑的规则是:调用点是否有一个环境对象(context object),也称为拥有者(owning)或容器(containing)对象。
obj.foo()
。此时this指向对象内的其他属性。就是当一个 隐含绑定 丢失了它的绑定,这通常意味着它会退回到 默认绑定, 根据 strict mode 的状态,其结果不是全局对象就是 undefined。
obj.foo
,实际上是函数本身的引用foo
。调用点在全局作用域/传递一个回调函数,则此时发生隐含丢失,绑定退回默认绑定。绝大多数被提供的函数,当然还有你将创建的所有的函数,都可以访问 call(…) 和 apply(…)。
call(..)
foo.call(..)
使用 明确绑定 来调用 foo
,允许我们强制函数的 this
指向 obj
。function foo() {
console.log( this.a );
}
var obj = {
a: 2
};
var bar = function() {
foo.call( obj );
};
bar(); // 2
setTimeout( bar, 100 ); // 2
// `bar` 将 `foo` 的 `this` 硬绑定到 `obj`
// 所以它不可以被覆盖
bar.call( window ); // 2
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
var obj = {
a: 2
};
var bar = foo.bind( obj );
var b = bar( 3 ); // 2 3
console.log( b ); // 5
function foo(a) {
this.a = a;//这里的this即为下面构建的新对象bar,bar的a属性的值为foo的参数a的值
}
var bar = new foo( 2 );
console.log( bar.a ); // 2
通过在前面使用 new
来调用 foo(..)
,我们构建了一个新的对象并把这个新对象作为 foo(..)
调用的 this
。 new
是函数调用可以绑定 this
的最后一种方式,我们称之为 new 绑定(new binding)。
你需要做的 一切 就是找到调用点然后考察哪一种规则适用于它。
function foo() {
console.log( this.a );
}
var obj1 = {
a: 2,
foo: foo
};
var obj2 = {
a: 3,
foo: foo
};
obj1.foo(); // 2
obj2.foo(); // 3
obj1.foo.call( obj2 ); // 3
obj2.foo.call( obj1 ); // 2
function foo(something) {
this.a = something;
}
var obj1 = {
foo: foo
};
var obj2 = {};
obj1.foo( 2 );
console.log( obj1.a ); // 2
obj1.foo.call( obj2, 3 );
console.log( obj2.a ); // 3
var bar = new obj1.foo( 4 );
console.log( obj1.a ); // 2
console.log( bar.a ); // 4
function foo(something) {
this.a = something;
}
var obj1 = {};
var bar = foo.bind( obj1 );
bar( 2 );
console.log( obj1.a ); // 2
var baz = new bar( 3 );
console.log( obj1.a ); // 2
console.log( baz.a ); // 3
现在,我们可以按照优先顺序来总结一下从函数调用的调用点来判定 this 的规则了。按照这个顺序来问问题,然后在第一个规则适用的地方停下。
new
被调用的吗(new 绑定
)?如果是,this
就是新构建的对象。var bar = new foo()
call
或 apply
被调用(明确绑定),甚至是隐藏在 bind 硬绑定 之中吗?如果是,this
就是那个被明确指定的对象。var bar = foo.call( obj2 )
this
就是那个环境对象。var bar = obj1.foo()
this
(默认绑定)。如果在 strict mode
下,就是 undefined
,否则是 global
对象。var bar = foo()
如果你传递 null
或 undefined
作为 call
、apply
或 bind
的 this
绑定参数,那么这些值会被忽略掉,取而代之的是 默认绑定 规则将适用于这个调用。
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 将数组散开作为参数
foo.apply( null, [2, 3] ); // a:2, b:3
// 或者用ES6扩散操作符
foo(...[2, 3]);
// 用 `bind(..)` 进行柯里化
var bar = foo.bind( null, 2 );
bar( 3 ); // a:2, b:3
与使用四种标准的 this
规则不同的是,箭头函数从封闭它的(函数或全局)作用域采用 this
绑定。
是在函数定义的时候绑定。
function foo() {
// 返回一个箭头函数
return (a) => {
// 这里的 `this` 是词法上从 `foo()` 采用的
console.log( this.a );
};
}
// 等价于:
function foo0() {
var self = this; // 词法上捕获 `this`
setTimeout( function(){
console.log( self.a );
}, 100 );
}
var obj1 = {
a: 2
};
var obj2 = {
a: 3
};
foo0.call(obj1);// 2
var bar = foo.call( obj1 );
bar.call( obj2 ); // 2, 不是3!
箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this ,且继承后不可以被覆盖。
在 foo()
中创建的箭头函数在词法上捕获 foo()
被调用时的 this
,不管它是什么。因为 foo()
被 this
绑定到 obj1
,bar
(被返回的箭头函数的一个引用)也将会被 this
绑定到 obj1
。一个箭头函数的词法绑定是不能被覆盖的(就连 new
也不行!)。
var name = 'window'
var person1 = {
name: 'person1',
show1: function () {
console.log(this.name)
},
show2: () => console.log(this.name),
show3: function () {
return function () {
console.log(this.name)
}
},
show4: function () {
return () => console.log(this.name)
}
}
var person2 = { name: 'person2' }
person1.show1()//作为对象方法进行引用调用,this指向person1,输出person1
person1.show1.call(person2)//改变this指向,输出person2
person1.show2()//箭头函数,捕获其上一级函数上下文中的this,即为window,输出window
person1.show2.call(person2)//call不能改变其绑定,仍然输出window
person1.show3()()//函数中返回函数,相当于函数调用,this指向window,输出window
person1.show3().call(person2)//改变了返回函数的this指向,输出person2
person1.show3.call(person2)()//改变show3对应函数的指向,但仍然是函数调用,this指向window,输出window
person1.show4()()//函数中返回箭头函数,捕获其上一级函数上下文的this,指向obj1,输出person1
person1.show4().call(person2)//不能改变箭头函数this指向,输出person1
person1.show4.call(person2)()//改变箭头函数上一级函数上下文的this指向,则此时this指向person2,输出person2
ES6常用新特性总结
ES6解决了什么问题
箭头函数、解构赋值、let、const、async / await
使用反引号 来创建字符串,可以包含由美元符号加花括号包裹的变量${vraible}。
参数默认值,不定参数,拓展参数[…arry]
对象的创建,继承更加直观了,并且父类方法的调用,实例化,静态方法和构造函数等概念都更加形象化。
//类的定义
class Animal {
//ES6中新型构造器
constructor(name) {
this.name = name;
}
//实例方法
sayName() {
console.log('My name is '+this.name);
}
}
//类的继承
class Programmer extends Animal {
constructor(name) {
//直接调用父类构造器进行初始化
super(name);
}
program() {
console.log("I'm coding...");
}
}
//测试我们的类
var animal=new Animal('dummy'),
wayou=new Programmer('wayou');
animal.sayName();//输出 ‘My name is dummy’
wayou.sayName();//输出 ‘My name is wayou’
wayou.program();//输出 ‘I'm coding...’
(精华)2020年6月29日 JavaScript高级篇 ES6(解构赋值)
Math.pow(3, 2) === 3 ** 2 // 9
[1, 2, 3].indexOf(3) > -1 // true
等同于:
[1, 2, 3].includes(3) // true
async、await异步解决方案
Object.entries()、values()
字符串填充padStart()、padEnd()
for await of
for of方法能够遍历具有 Symbol.iterator 接口的同步迭代器数据,但是不能遍历异步迭代器。ES9 新增的 for await of 可以用来遍历具有 Symbol.asyncIterator 方法的数据结构,也就是异步迭代器,且会等待前一个成员的状态改变后才会遍历到下一个成员,相当于 async 函数内部的 await。
Promise.prototype.finally()
方法返回一个 Promise,在 promise 执行结束时,无论结果是 fulfilled 或者是 rejected,在执行 then() 和 catch() 后,都会执行 finally 指定的回调函数。
Object Resr & Spread 扩展运算符
Object Rest SpreadES6 中添加的最意思的特性之一是 spread 操作符。你不仅可以用它替换 cancat() 和 slice() 方法,使数组的操作 (复制、合并) 更加简单,还可以在数组必须以拆解的方式作为函数参数的情况下,spread 操作符也很实用。
JavaScript事件代理(事件委托)
场景题:一个父元素中不断有div被加入,如何给这些div绑定事件。
可以大量节省内存占用,减少事件注册
如果给每个li列表项都绑定一个函数,那对内存的消耗是非常大的,因此较好的解决办法就是将li元素的点击事件绑定到它的父元素ul身上,执行事件的时候再去匹配判断目标元素。
可以实现当新增子对象时无需再次对其绑定(动态绑定事件)
假设上述的例子中列表项li就几个,我们给每个列表项都绑定了事件;
在很多时候,我们需要通过 AJAX 或者用户操作动态的增加或者删除列表项li元素,那么在每一次改变的时候都需要重新给新增的元素绑定事件,给即将删去的元素解绑事件;
如果用了事件委托就没有这种麻烦了,因为事件是绑定在父层的,和目标元素的增减是没有关系的,执行到目标元素是在真正响应执行事件函数的过程中去匹配的;所以使用事件在动态绑定事件的情况下是可以减少很多重复工作的。
通过event.target
和target.id
去定位元素
document.addEventListener("click", function (event) {
var target = event.target;
switch (target.id) {
case "doSomething":
document.title = "事件委托";
break;
case "goSomewhere":
location.href = "http://www.baidu.com";
break;
case "sayHi": alert("hi");
break;
}
})
优点:
- 节省内存占⽤,减少事件注册
- 新增⼦对象时⽆需再次对其绑定事件,适合动态添加元素
局限性:
- focus、blur 之类的事件本身没有事件冒泡机制,所以⽆法委托
- mousemove、mouseout 这样的事件,虽然有事件冒泡,但是只能不断通过位置去计算定位,对性能消耗⾼,不适合事件委托
事件代理(Event Delegation),又称之为事件委托。是JavaScript中常用绑定事件的常用技巧。顾名思义,“事件代理”即是把原本需要绑定在子元素的响应事件(click、keydown…)委托给父元素,让父元素担当事件监听的职务。事件代理的原理是DOM元素的事件冒泡。
当一个元素接收到事件的时候,会把他接收到的事件传给父级,一直传到window。
一个事件触发后,会在子元素和父元素之间传播(propagation)。这种传播分成三个阶段:
首先发生的是事件捕获,为截获事件提供了机会。然后是实际的目标接收到事件,最后一个阶段是冒泡阶段,可以在这个阶段对事件做出响应。
阻止冒泡有如下方式:
标准的W3C 方式:e.stopPropagation();这里的stopPropagation是标准的事件对象的一个方法,调用即可;不让事件向documen上蔓延,但是默认事件任然会执行,当你掉用这个方法的时候,如果点击一个连接,这个连接仍然会被打开
非标准的IE方式:ev.cancelBubble=true; 这里的cancelBubble是 IE事件对象的属性,设为true就可以了
return false: 事件处理过程中,阻止了事件冒泡,也阻止了默认行为;写上此代码,连接不会被打开,事件也不会传递到上一层的父元素;可以理解为return false就等于同时调用了event.stopPropagation()和event.preventDefault()
event.preventDefault():事件处理过程中,不阻止事件冒泡,但阻止默认行为;调用此方法是,连接不会被打开,但是会发生冒泡,冒泡会传递到上一层的父元素;
window.onload=function(){
document.getElementById("btn").addEventListener("click",function () {
// body...
alert("hello");
});
document.getElementById("div1").addEventListener("click",function(){
alert("div1");
});
document.getElementById("div2").addEventListener("click",function(){
alert("div2");
});
}
冒泡:从里面往外面触发事件,就是alert的顺序是 button、div2、div1。
捕获:从外面往里面触发事件,就是alert的顺序是div1、div2、button。
冒泡false捕获true:
要想冒泡,就要将每个监听事件的第三个参数设置为false,也就是默认的值。
要想捕获,就要将每个监听事件的第三个参数设置为true。
DOM之事件模型分脚本模型、内联模型(同类⼀个,后者覆盖)、动态绑定(同类多个)
<body>
<button onclick="javascrpt:alert('Hello')">Hello1button>
<button onclick="showHello()">Hello2button>
<button id="btn3">Hello3button>
body>
JavaScript事件模型主要分为3种:原始事件模型、DOM2事件模型、IE事件模型。
同⼀个元素,同类事件只能添加⼀个,如果添加多个,后⾯添加的会覆盖之前添加的
这是一种被所有浏览器都支持的事件模型,对于原始事件而言,没有事件流,事件一旦发生将马上进行处理,有两种方式可以实现原始事件:
(1)在html代码中直接指定属性值:
<button id="demo" type="button" onclick="doSomeTing()" />
(2)在js代码中为
document.getElementsById("demo").onclick = doSomeTing()
优点:所有浏览器都兼容
缺点:1)逻辑与显示没有分离;2)相同事件的监听函数只能绑定一个,后绑定的会覆盖掉前面的,如:a.onclick = func1; a.onclick = func2;将只会执行func2中的内容。3)无法通过事件的冒泡、委托等机制(后面会讲到)完成更多事情。
因为这些缺点,虽然原始事件类型兼容所有浏览器,但仍不推荐使用。
可以给同⼀个元素添加多个同类事件。
这种事件模型是捕获和冒泡模型。 此模型是W3C制定的标准模型,现代浏览器(IE6~8除外)都已经遵循这个规范。W3C制定的事件模型中,一次事件的发生包含三个过程:
(1).事件捕获阶段,(2).事件目标阶段,(3).事件冒泡阶段。
在DOM2级中使用addEventListener
和removeEventListener
来注册和解除事件(IE8及之前版本不支持)。这种函数较之之前的方法好处是一个dom对象可以注册多个相同类型的事件,不会发生事件的覆盖,会依次的执行各个事件函数。
addEventListener('事件名称','事件回调','捕获/冒泡')
梳理下常见的不冒泡事件
(addEventListener第三个参数可以在捕获阶段添加事件监听)
js在执行过程中,每遇到一个异步函数,都会将这个异步函数放入一个异步队列中,只有当同步线程执行结束之后,才会开始执行异步队列中的函数。
前端异步(async)解决方案(所有方案)
异步操作同步写法、底层 generator、更符合线性思维更好读、可以 try catch 获取错误
问题:async / await 的传染性; await 将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 await 会导致性能上的降低。
async:
await:
适用场景:
- 一个请求接着一个请求,后一个请求依赖前一个请求
- 并发请求,有的时候我们并不需要等待一个请求回来才发出另一个请求,这样效率是很低的,所以这个时候就需要并发执行请求任务。(await + Promise.all)
- 错误处理:async/await 处理错误非常直观, 使用 try/catch 直接捕获就可以
- 超时处理:一个请求发出,我们是无法确定什么时候返回的,也总不能一直傻傻的等,设置超时处理有时是很有必要的(await + Promise.race )
一次搞懂 Generator 函数
Generator 最大的特点就是可以控制函数的执行。
1、什么是 Generator 函数
Generator函数是ES6提供的一种异步编程解决方案,形式上也是一个普通函数,但有几个显著的特征:
– function关键字与函数名之间有一个星号 “*” (推荐紧挨着function关键字)
– 函数体内使用 yield 表达式,定义不同的内部状态 (可以有多个yield)
– 直接调用 Generator函数并不会执行,也不会返回运行结果,而是返回一个遍历器对象(Iterator Object)
– 依次调用遍历器对象的next方法,遍历 Generator函数内部的每一个状态
Generator函数则没有执行而是返回一个Iterator对象,并通过调用Iterator对象的next方法来遍历,函数体内的执行看起来更像是“被人踢一脚才动一下”的感觉
每次调用Iterator对象的next方法时,内部的指针就会从函数的头部或上一次停下来的地方开始执行,直到遇到下一个 yield 表达式或return语句暂停。换句话说,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而 next方法可以恢复执行
function* gen(){
let res=yield 1
yield res
return 3
}
let g=gen();
console.log(g.next())//{value:1,done:false}
console.log(g.next(333))//{value:333,done:false}
在next中传入参数会作为上一次yield的返回值(会忽略第一个next中传递的参数)
script没有async/defer标签会阻塞DOM渲染。
都只能对外部文件生效,内联文件不生效。
在异步函数中将一个函数进行参数传入,当异步执行完成之后执行该函数。
如果再多几个异步函数,代码整体的维护性,可读性都变的极差,如果出了bug,修复的排查过程也变的极为困难,这个便是所谓的 回调函数地狱。
订阅发布模式定义了一种一对多的依赖关系,让多个订阅者对象同时监听某发布者对象。这个发布者对象在自身状态变化时,会通知所有订阅者对象,使它们能够自动更新自己的状态。vue就是基于发布/订阅者模式。
//创建一个主题发布类
var Publisher=function(){
this.subscribers=[]
}
Publisher.prototype.publish=function(data){
this.subscribers.forEach(function(fn){
fn(data)
})
}
/*
在Function上挂载这个些方法,所有的函数都可以调用这些方法
表示所有函数都可以订阅/取消订阅相关的主题发布
*/
//订阅
Function.prototype.subscribe=function(publisher){
var that=this;
var isExist=publisher.subscribers.some(function(el){
if(el===that){
return true
}
})
if(!isExist){
publisher.subscribers.push(that)
}
//return this是为了支持链式调用
return this
}
//取消订阅
Function.prototype.unsubscribe=function(publisher){
var that=this;
//就是将函数从发布者的订阅者列表中进行删除
publisher.subscribers=publisher.subscribers.filter(function(el){
if(el!==that){
return true
}
})
return this
}
var publisher=new Publisher();
var subscriberObj=function(data){
console.log(data)
}
subscriberObj.subscribe(publisher)
这样就实现了一个简单的发布订阅者模式,每次发布者发布新内容时,就会调用publish方法,然后将内容作为参数,依次调用订阅者函数(subscribers)。
其实,发布/订阅模式与事件监听很类似,
ECMAscript 6 原生提供了 Promise 对象。
Promise 对象表示异步操作的最终完成(或失败)及其结果值。
Promise 对象有以下两个特点:
1、对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态:
pending
: 初始状态,不是成功或失败状态。
fulfilled/resolved
: 意味着操作成功完成。
rejected
: 意味着操作失败。
只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
2、一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise 对象的状态改变,只有两种可能:从 Pending
变为 Resolved
和从 Pending 变为 Rejected
。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对 Promise 对象添加回调函数,也会立即得到这个结果。
创建 promise
时,PromiseStatus 将处于 pending
状态,并且 PromiseValue 为 undefined
,直到 promise 被 resolved
或 rejected
为止。 当 promise 处于 resolved
或 rejected
的状态时,就称为 settled
(已定型)。 所以 promise 通常从 pending
态转换到 settled
状态。
理解 Javascript 中的 Promise
使用 Promise
问题:
function f(val){
return new Promise((resolve,reject) => {
if(val){
resolve({ name:'小明' },100); //成功时也可以传递一个值,但需要注意的是只能传递一个参数,传两个的话第二个参数是拿不到的
}else{
reject('404'); //错误处理的第一种方式:传递参数,错误的原因
}
});
}
//then(resolve,reject),碰到resolve或是reject都是异步的,所以tr catch对它是没有用的
//then方法中的第二个回调reject,是失败时候做的事
f(false)
.then( (data, data2) => {
console.log(data2); //undefined
}, e => {
console.log(e); //404
})
function f(val){
return new Promise((resolve,reject) => {
if(val){
resolve({ name:'小明' });
}else{
reject('404');
}
});
}
f(true)
.then(data => {
console.log(data); //{name:'小明'}
return f(false); //返回的promise是失败的话,后面的then对这个失败没有处理的话,就会继续往下走
})
.then(() => {
console.log('我永远不会被输出')
})
.then(() => {
}, e => console.log('失败')) //
.catch(e => { //上面处理了错误的话,这个catch就不会运行了
console.log(e); //404
})
.then(() => { //catch后面可以继续then,但是如果后面的then出错了,跟上一个catch就没有关系了
console.log(e)
return f(false)
})
.catch() //如果最后一个catch有错误,会无限catch
//标准es中,这个问题没有很好地解决方法,但是第三方的库有对全局的捕获
//finally
//不论成功还是失败,finally中的内容一定会执行
//可以在finally中做一些收尾的工作
function f(val){
return new Promise((resolve,reject) => {
if(val){
resolve({ name:'小明' });
}else{
reject('404');
}
});
}
f(true)
.then(data => {
console.log(data); //{name:'小明'}
return f(false);
})
.catch(e => {
console.log(e) //404
return f(false); //即便返回了一个成功的promise,下面的finally也会执行,如果返回的是失败的promise,控制台最后一行会报错uncaught (in promise) 404
})
.finally( () => {
console.log(100) //100
})
{
let a = 0;
var b = 1;
}
a // ReferenceError: a is not defined
b // 1
const a = 1
a = 2 // Uncaught TypeError: Assignment to constant variable.
const b = '1231'
b = 'xcv' // Uncaught TypeError: Assignment to constant variable.
const c = true
c = false // Uncaught TypeError: Assignment to constant variable.
const obj = {
name: 'cjg'
}
obj.school = 'sysu'
console.log(obj) // Object {name: "cjg", school: "sysu"}
obj = {} // VM183:6 Uncaught TypeError: Assignment to constant variable
let a = 1;
let a = 2;
var b = 3;
var b = 4;
a // Identifier 'a' has already been declared
b // 4
for (var i = 0; i < 10; i++) {
setTimeout(function(){
console.log(i);
})
}
// 输出十个 10
for (let j = 0; j < 10; j++) {
setTimeout(function(){
console.log(j);
})
}
// 输出 0123456789
变量 i 是用 var 声明的,在全局范围内有效,所以全局中只有一个变量 i, 每次循环时,setTimeout 定时器里面的 i 指的是全局变量 i ,而循环里的十个 setTimeout 是在循环结束后才执行,所以此时的 i 都是 10。
变量 j 是用 let 声明的,当前的 j 只在本轮循环中有效,每次循环的 j 其实都是一个新的变量,所以 setTimeout 定时器里面的 j 其实是不同的变量,即最后输出 12345。(若每次循环的变量 j 都是重新声明的,如何知道前一个循环的值?这是因为 JavaScript 引擎内部会记住前一个循环的值)。
console.log(a); //ReferenceError: a is not defined
let a = "apple";
console.log(b); //undefined
var b = "banana";
变量 b 用 var 声明存在变量提升,所以当脚本开始运行的时候,b 已经存在了,但是还没有赋值,所以会输出 undefined。
变量 a 用 let 声明不存在变量提升,在声明变量 a 之前,a 不存在,所以会报错。
const PI = "3.1415926";
PI // 3.1415926
const MY_AGE; // SyntaxError: Missing initializer in const declaration
所以ES6非严格模式下,与var声明的全局变量都会成为window的属性
而使用let声明的全局变量,不会成为window的属性
var PI = "a";
if(true){
console.log(PI); // ReferenceError: PI is not defined
const PI = "3.1415926";
}
ES6 明确规定,代码块内如果存在 let 或者 const,代码块会对这些命令声明的变量从块的开始就形成一个封闭作用域。代码块内,在声明变量 PI 之前使用它会报错。
const 如何做到变量在声明初始化之后不允许改变的?其实 const 其实保证的不是变量的值不变,而是保证变量指向的内存地址所保存的数据不允许改动。此时,你可能已经想到,简单类型和复合类型保存值的方式是不同的。是的,对于简单类型(数值 number、字符串 string 、布尔值 boolean),值就保存在变量指向的那个内存地址,因此 const 声明的简单类型变量等同于常量。而复杂类型(对象 object,数组 array,函数 function),变量指向的内存地址其实是保存了一个指向实际数据的指针,所以 const 只能保证指针是固定的,至于指针指向的数据结构变不变就无法控制了,所以使用 const 声明复杂类型对象时要慎重。
在ES6规范有一个词叫做Global Enviroment Records(也就是全局环境变量记录),它里面包含两个内容,一个是Object Enviroment Record,另一个是Declarative Enviroment Record。函数声明和使用var声明的变量会添加进入Object Enviroment Record中,而使用let声明和使用const声明的变量会添加入Declarative Enviroment Record中。
let Arrow = () => {
console.log(arguments);
}
console.log(Arrow.prototype); // undefined
let newArrow = new Arrow();
//Uncaught TypeError: Arrow is not a constructor
let Arrow1 = () => {
console.log(arguments);
}
Arrow1(1, 2, 3);
//VM465:2 Uncaught ReferenceError: arguments is not defined
let Arrow2 = (...args) => {
console.log(args);
}
Arrow2(1, 2, 3);
let obj2 = {
a: 10,
b: function(n) {
let f = (n) => n + this.a;//this指向obj
return f(n);
},
c: function(n) {
let f = (n) => n + this.a;
let m = {
a: 20
};
return f.call(m,n);//this仍然指向obj
},
d: function(n) {
console.log(this.a)
}
};
var a = 5;
console.log(obj2.b(1)); // 11
console.log(obj2.c(1)); // 11
console.log(obj2.d()); // 10
console.log(obj2.d.call(this, a)); // 5
5.箭头函数不能当做Generator函数,不能使用yield关键字
调用函数:RHS引用
函数内将值作为参数传给函数时:LHS查询
作用域:是通过标识符名称查询变量的一组规则/收集并维护一张所有被声明的标识符(变量)的列表,并对当前执行中的代码如何访问这些变量强制实施一组严格的规则。
这种查询也许是为了向这个变量赋值
,这时变量是一个 LHS(左手边)引用
,或者是为取得它的值
,这时变量是一个 RHS(右手边)引用
。
遍历嵌套 作用域 的简单规则:引擎 从当前执行的 作用域 开始,在那里查找变量,如果没有找到,就向上走一级继续查找,如此类推。如果到了最外层的全局作用域,那么查找就会停止,无论它是否找到了变量。
如果 RHS 查询在嵌套的 作用域 的任何地方都找不到一个值,这会导致 引擎 抛出一个 ReferenceError
。
现在,如果一个 RHS 查询的变量被找到了,但是你试着去做一些这个值不可能做到的事,比如将一个非函数的值作为函数运行,或者引用 null 或者 undefined 值的属性,那么 引擎 就会抛出一个不同种类的错误,称为 TypeError
。
ReferenceError
是关于 作用域 解析失败的,而 TypeError
暗示着 作用域 解析成功了,但是试图对这个结果进行了一个非法/不可能的动作。
JavaScript 采用是词法作用域(lexical scoping),也就是 静态作用域:
作用域:(scope)是标识符(变量)在程序中的可见性范围,它关注的是标识符(变量)的可访问性(可见性)
程序设计概念:通常来说,一段程序代码中所用到的名字并不总是有效/可用的,而限定这个名字的可用性的代码范围就是这个名字的 作用域。
定义:在词法分析时被定义的作用域。它是一组关于 引擎 如何查询变量和它在何处能够找到变量
的规则。词法作用域是基于,你,在写程序时,变量和作用域的块儿在何处被编写决定的,因此它在词法分析器处理你的代码时(基本上)是固定不变的。
一旦找到第一个匹配,作用域查询就停止了。相同的标识符名称可以在嵌套作用域的多个层中被指定,这称为“ 遮蔽(shadowing) ”(内部的标识符“遮蔽”了外部的标识符)。无论如何遮蔽,作用域查询总是从当前被执行的最内侧的作用域开始,向外/向上不断查找,直到第一个匹配才停止。
不管函数是从 哪里 被调用的,也不论它是 如何 被调用的,它的词法作用域
是由 这个函数被声明的位置
唯一 定义的。
与词法作用域对应的还有一个 动态作用域 :
动态作用域看起来在暗示,有充分的理由,存在这样一种模型,它的作用域是在运行时被确定的
,而不是在编写时静态地确定的。
function foo() {
console.log( a ); // 2
}
function bar() {
var a = 3;
foo();
}
var a = 2;
bar();
动态作用域本身不关心函数和作用域是在哪里和如何被声明的
,而是关心 它们是从何处被调用的
。换句话说,它的作用域链条是基于调用栈
的,而不是代码中作用域的嵌套。
当 foo()
不能为 a
解析出一个变量引用时,它不会沿着嵌套的(词法)作用域链向上走一层,而是沿着调用栈
向上走,以找到 foo()
是 从何处
被调用的。如果JS有动态作用域,那么因为 foo()
是从 bar()
中被调用的,它就会在 bar() 的作用域中检查变量,并且在这里找到持有值 3 的 a。
但是 this
机制有些像动态作用域,this
关心的是 函数是如何被调用的。
关键的差异:词法作用域是编写时的
,而动态作用域(和 this)是运行时的
。词法作用域关心的是 函数在何处被声明
,但是动态作用域关心的是函数 从何处 被调用
。
根据在内部函数可以访问外部函数变量的这种机制,用链式查找决定哪些数据能被内部函数访问。
作用域链的作用主要用于查找标识符,当作用域需要查询变量的时候会沿着作用域链依次查找,如果找到标识符就会停止搜索,否则将会沿着作用域链依次向后查找,直到作用域链的结尾。
每个函数运行时都会产生一个执行环境,而这个执行环境怎么表示呢?
js为每一个执行环境(EC)关联了一个变量对象(VO)。环境中定义的所有变量和函数都保存在这个对象中。
全局执行环境是最外围的执行环境,全局执行环境被认为是window对象,因此所有的全局变量和函数都作为window对象的属性和方法创建的。
js的执行顺序是根据函数的调用来决定的,当一个函数被调用时,该函数环境的变量对象就被压入一个 执行环境栈(ECS) 中。而在函数执行之后,栈将该函数的变量对象弹出,把控制权交给之前的执行环境变量对象。
function f1() {
var f1Context = 'f1 context';
function f2() {
var f2Context = 'f2 context';
function f3() {
var f3Context = 'f3 context';
console.log(f3Context);
}
f3();
console.log(f2Context);
}
f2();
console.log(f1Context);
}
f1();
JavaScript代码是如何被执行的
JavaScript是单线程语言。所以JavaScript是按顺序执行的:先编译再执行,但其中存在变量提升
主要核心流程分为两步 – 编译 和 执行
- 首先将 JavaScript代码 转换为 低级中间代码 或者 机器能够理解的机器代码 ;
- 执行转换后的代码并输出执行结果;
比如:
准备好基础环境,向V8提交要执行的JS代码
首先,V8 会接收到要执行的 JavaScript 源代码,不过这对 V8 来说只是一堆 字符串,V8 并不能直接理解这段字符串的含义,它需要 结构化 这段字符串;
结构化字符串(JS源代码)
结构化,是指信息经过分析后可 分解成多个互相关联的组成部分。各组成部分间有明确的层次结构,方便使用和维护,并有一定的操作规范;
生成AST、作用域
结构化之后,就生成了 抽象语法树(AST),AST 是便于 V8 理解的结构;在生成 AST 的同时,V8 还会 生成相关的作用域,作用域中存放相关变量;
生成字节码
生成AST的步骤可以拆分成以下两个小步骤:
ES5的对象属性名都是字符串,这容易造成命名冲突。ES6引入一种新的数据类型:Symbol,用于创建独一无二的值。Symbol 通过 Symbol() 函数生成。
symbol不是一个构造函数,不能使用new来创建,与普通函数调用类似
var s1 = Symbol();//创建了一个symbol的实例
console.log(s1);//Symbol()
console.log(typeof s1);//symbol
symbol可以传递一个字符串参数,参数作用是对symbol类型的描述,便于区分这个symbol是哪一个
var s1 = Symbol("symbol");
console.log(s1);//Symbol(symbol)
console.log(typeof s1);//symbol
symbol类型的值具有唯一性,是一个独一无二的值,每一个 Symbol 的值都不相等。相同参数 Symbol() 返回的值不相等
var s1 = Symbol("symbol");
var s2 = Symbol("symbol");
console.dir(s1 == s2);//false
Symbol作为属性名,该属性不会出现在 for in, for of 循环中,也不会被 Object.keys()、Object.getOwnPropertyNames() 返回。但他也不是私有属性,有一个Object.getOwnPropertySymbols() (和Reflect.ownKeys())方法可以获取对象的所有 Symbol 属性。
Symbol.for()
方法首先会搜索’全局Symbol注册表’,看是否存在一个键值为 “uid” 的’Symbol’。如果存在,该方法会’返回这个已存在的Symbol’,否则,会创建一个新的’Symbol’,并使用该键值将其记录到全局’Symbol’注册表中,然后’返回这个新的Symbol’。这就意味着此后使用同一个键值去调用’Symbol.for()‘方法都将会’返回同一个Symbol’
// 创建一个全局的Symbol
let uid = Symbol.for("uid")
// 在对象中使用这个uid私有属性
let object = {
[uid]: "12345"
}
console.log(object[uid]) // "12345"
console.log(uid) // "Symbol(uid)"
// 查看全局Symbol注册表中是否有uid,如果有就使用现有的,如果没有就创建新的Symbol
let uid2 = Symbol.for("uid")
console.log(uid === uid2) // true
console.log(object[uid2]) // "12345"
console.log(uid2) // "Symbol(uid)"
你可以使用’Symbol.keyFor()'方法在’全局Symbol注册表’中根据’Symbol’检索出对应的键值
let uid = Symbol.for("uid")
console.log(Symbol.keyFor(uid)) // "uid"
let uid2 = Symbol.for("uid")
console.log(Symbol.keyFor(uid2)) // "uid"
let uid3 = Symbol("uid")
console.log(Symbol.keyFor(uid3)) // undefined,因为全局Symbol注册表中没有uid3,uid3是局部的
以 Symbol 值作为属性名,必须使用**[]**运算符定义和获取属性值
如果对象下同时添加了与symbol同名的常规属性,则使用[]访问的是symbol属性,使用点运算符访问的是常规属性
var n = Symbol("uname");
var obj = {};
obj.n = "jerry";
obj[n] = "tom";
console.log(obj[n]);//tom
console.log(obj.n);//jerry
JavaScript中Number.MAX_SAFE_INTEGER表示最⼤安全数字,计算结果9007199254740991,即在这个数范围内不会出现精度丢失(⼩数除外)。
但是⼀旦超过这个范围,js就会出现计算不准确的情况,这在⼤数计算的时候不得不依靠⼀些第三⽅库进⾏解决,因此官⽅提出了BigInt来解决此问题。
BigInt 是一种内置对象,它提供了一种方法来表示大于2^53 - 1
的整数。这原本是 Javascript中可以用 Number 表示的最大数字。BigInt 可以表示任意大的整数。
可以用在一个整数字面量后面加 n 的方式定义一个 BigInt ,如:10n,或者调用函数BigInt()。
const theBiggestInt = 9007199254740991n;
const alsoHuge = BigInt(9007199254740991);
// ↪ 9007199254740991n
const hugeString = BigInt("9007199254740991");
// ↪ 9007199254740991n
const hugeHex = BigInt("0x1fffffffffffff");
// ↪ 9007199254740991n
const hugeBin = BigInt("0b11111111111111111111111111111111111111111111111111111");
// ↪ 9007199254740991n
使用 typeof 测试时, BigInt 对象返回 “bigint” :
typeof 1n === 'bigint'; // true
typeof BigInt('1') === 'bigint'; // true
fill(value, start, end)
:将数组start到end的值用value填充concat()
join()
,数组变字符串,默认用‘,
’分隔,一般用arr.join(‘’)push()
pop()
shift()
unshift()
slice()
,左闭右开splice(index,howmany,item1,.....,itemX)
向/从数组中添加/删除项目,然后返回被删除的项目。该参数是开始插入和(或)删除的数组元素的下标,index包括自身sort()
reverse()
indexOf()/lastIndexOf()
(前往后/后往前)every()/some()
(每一项/任意一项)filter()
:返回数组内所有满足条件的元素map()
forEach()
find()
:查找第一个符合条件的数组元素findIndex()
:查找第一个符合条件的数组元素的索引includes()
map方法
1.map方法返回一个新的数组,数组中的元素为原始数组调用函数处理后的值。
2.map方法不会对空数组进行检测,map方法不会改变原始数组。
forEach方法
1.forEach方法用来调用数组的每个元素,将元素传给回调函数。
2.forEach对于空数组是不会调用回调函数的。
深入理解ES6–7.Set和Map
1. Set
ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。
Set本身是一个构造函数,用来生成 Set 数据结构。
[value, value],键值与键名是一致的(或者说只有键值,没有键名);
可以遍历,keys(),values(),entries()
方法有:add、delete、has、clear。set.size返回大小
Array.from方法可以将Set结构转为数组。
2.WeakSet
WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。
不能遍历,方法有 add、delete、has。
3.Map
JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。
可以遍历keys()、values()、entries()、forEach(),方法有set、get、has、delete,可以跟各种数据格式转换。
4.WeakMap
WeakMap结构与Map结构类似,也是用于生成键值对的集合。
只接受对象最为键名(null 除外),不接受其他类型的值作为键名;
键名是弱引用,不会被计入垃圾回收机制,不阻止垃圾回收器回收它所引用的 key。键值可以是任意的,键名所指向的对象可以被垃圾回收,此时键名是无效的;
WeakMap的专用场合就是,它的键所对应的对象(如dom节点),可能会在将来消失。WeakMap结构有助于防止内存泄漏。
不能遍历,方法有 get、set、has、delete。
set的方法:add、delete、has
map的方法:set、delete、has、get
set 无序且不重复
Map和WeakMap的区别:
WeakMap 的 key 只能是对象,且不能被遍历。当把 WeakMap 里面的 key 设为 null 之后,可以被垃圾回收机制回收,可以用于存储DOM节点。
let a = {name: "sillywa"}
let b = {age: 90}
let map = new Map()
let weakMap = new WeakMap()
map.set(a, "test1")
weakMap.set(b, "test2")
a=null
b=null
map.get(a)
weakMap.get(b)
让人头疼的类型转换
模块化是一个语言发展的必经之路,其能够帮助开发者拆分和组织代码,随着前端技术的发展,前端编写的代码量也越来越大,就需要对代码有很好的管理,而模块化能够帮助开发者解决命名冲突、管理依赖、提高代码的可读性、代码解耦以及提高代码的复用性
require(['moduleA', 'moduleB', 'moduleC'], function (moduleA, moduleB, moduleC){
// do something
});
define(['moduleA', 'moduleB', 'moduleC'], function (moduleA, moduleB, moduleC){
// do something
return {};
});
define(function(require,exports,module){
var a = reuire('require.js');
a.dosomething();
return {};
});
for of遍历的只是数组内的元素,而不包括数组的原型属性method和索引name
封装的目的是增强安全性和简化编程,使用者不必了解具体的实现细节,而只是要通过外部接口,以特定的访问权限来使用类的成员。
面相对象的不就是使用程序处理事情时以对象为中心去分析吗,与面向过程不同,面向过程关心处理的逻辑、流程等问题,而不关心事件主体。而面向对象即面向主体,所以我们在解决问题时应该先进行对象的封装(对象是封装类的实例,比如张三是人,人是一个封装类,张三只是对象中的一个实例、一个对象)。
继承
继承是面向对象的基本特征之一,继承机制允许创建分等级层次的类。继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。
继承机制可以很好的描述一个类的生态,也提高了代码复用率.
多态
同一个行为具有多个不同表现形式或形态的能力。是指一个类实例(对象)的相同方法在不同情形有不同表现形式。多态机制使具有不同内部结构的对象可以共享相同的外部接口。这意味着,虽然针对不同对象的具体操作不同,但通过一个公共的类,它们(那些操作)可以通过相同的方式予以调用。
多态存在的三个必要条件:
简言之,多态其实是在继承的基础上的。
1、
a、浏览器创建、获取(sessionStorage、localStorage)数组内容
b、路由(浏览器地址)传参、获取数组内容
创建、传参的时候使用JSON.stringify()
(如果不使用JSON.stringify()存进去的将是[object object],所以如果我们开发中遇到了获取内容的时候是[object object]不妨试试JSON.stringify()),
比如创建sessionStorage
sessionStorage.setItem("keyName",JSON.stringify(data))
function deepClone(obj) {
return JSON.parse(JSON.stringify(obj))
}
//判断数组是否包含某对象
let data = [
{name:'echo'},
{name:'听风是风'},
{name:'天子笑'},
],
val = {name:'天子笑'};
JSON.stringify(data).indexOf(JSON.stringify(val)) !== -1;//true
//判断两数组/对象是否相等
let a = [1,2,3],
b = [1,2,3];
JSON.stringify(a) === JSON.stringify(b);//true
编译阶段编译阶段分为 词法分析、 语法分析、 预编译 三个阶段。
词法分析
将 字符流 转换为 词法单元流(token),就像英文句子一个个单词独立翻译,举例:
var result = testNum1 - testNum2;
词法分析后的结果为:
NAME “result”
EQUALS
NAME “testNum1”
MINUS
NAME “testNum2”
SEMICOLON
语法分析
将上一步生成的 token 数据根据语法规则生成对应的 “抽象语法树”(Abstract Syntax Tree, AST)。
在这个过程中 JS 会对全部的脚本代码进行解析,通过 Javascript引擎 检查你的代码是否存在错误,如果有格式错误就会抛出一个错误。
如果成功构建完 AST,接着就通过 AST 生成字节码,也就生成了计算机可执行的代码,该过程叫做代码生成。
如:var a=2;
抽象语法树转为一组机器指令,用来创建一个叫作 a 的变量(包括分配内存等),并将值 2 储存在 a 中。
预编译(并非完全按顺序执行)
当 JavaScript引擎 解析脚本时,他会在预编译阶段对所有声明的 变量 和 函数 进行处理。
在这个过程中,会先预声明变量,之后再预声明函数(也就是变量提升优先于函数提升)。
JavaScript引擎在预编译的过程中,碰到任何变量,如果变量未经声明就赋值,就会将此变量归为全局对象所有。
脚本代码块script执行前
- 查找全局变量声明(包括隐式全局变量声明,省略var声明),变量名作全局对象的属性,值为undefined
- 查找函数声明,函数名作为全局对象的属性,值为函数引用
函数执行前:
- 创建AO对象(Active Object)
- 查找函数形参及函数内变量声明,形参名及变量名作为AO对象的属性,值为undefined
- 实参形参相统一,实参值赋给形参
- 查找函数声明,函数名作为AO对象的属性,值为函数引用
火狐:归并
chrome:插入排序和快速排序结合的排序算法。数组长度不超过10时,使用插入排序。长度超过10使用快速排序。在数组较短时插入排序更有效率。
浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
关于JSON.parse(JSON.stringify(obj))实现深拷贝应该注意的坑
是浏览器提供的一个按帧对网页进行重绘的 API
requestAnimationFrame 比起 setTimeout、setInterval的优势主要有两点:
问题:requestAnimationFrame是在主线程上完成。这意味着,如果主线程非常繁忙,requestAnimationFrame的动画效果会大打折扣。
执行时间:
requestAnimationFrame是个宏任务,在浏览器每次刷新页面之前执行。它但它和那些平行级别的宏任务执行顺序是不确定的
通过抛出异常的方式跳出循环 通过return跳过当次循环
var arr = [1,3,5,7,9];
var id = 5;
try {
arr.forEach(function (curItem, i) {
if(curItem === 1) return;
console.log(curItem)
if (curItem === id) {
throw Error(); //满足条件,跳出循环
}
})
} catch (e) {
}
Object.entries(obj).length === 0 && obj.constructor === Object // ES7
Object.keys(obj).length === 0 && obj.constructor === Object // ES6
// ES5
function isEmpty(obj) {
for(var prop in obj) {
if(obj.hasOwnProperty(prop)) {
return false;
}
}
return JSON.stringify(obj) === JSON.stringify({});
}
// loadash/underscore
_.isEmpty({}); // true
switch case与if else的区别:
switch case会生成一个跳转表来指示实际的case分支的地址,而if…else却需要遍历条件分支直到命中条件
switch case的优缺点
(1)switch case的优点:
当分支较多时,用switch的效率是很高的。因为switch是确定了选择值之后直接跳转到那个特定的分支.
(2)switch case的缺点:
for(j = 0; j < arr.length; j++) {
}
// 将arr.length提出来性能更高
const lenght = arr.length;
for(j = 0; j < length; j++) {
}
性能比较:for循环 > for of > foreach > map > for in
for......in
:会得到对象原型链上的属性Object.keys(obj).forEach()
/Object.values(obj).forEach()
/Object.entries(obj)
Object.getOwnPropertyNames(obj)
const obj = {
id:1,
name:'zhangsan',
age:18
}
Object.getOwnPropertyNames(obj).forEach(function(key){
console.log(key+ '---'+obj[key])
})
Reflect.ownKeys(obj)
var obj = {'0':'a','1':'b','2':'c'};
Reflect.ownKeys(obj).forEach(function(key){
console.log(key,obj[key]);
});
Object.getOwnPropertySymbols(obj)
var obj = {'0':'a','1':'b','2':'c'};
Object.getOwnPropertySymbols(obj).forEach((key) => {
console.log(obj[key])
}); // 什么都没有,因为该对象还没有 Symbol 属性
// 给对象添加一个不可枚举的 Symbol 属性
Object.defineProperties(obj, {
[Symbol('baz')]: {
value: 'Symbol baz',
enumerable: false
}
})
// 给对象添加一个可枚举的 Symbol 属性
obj[Symbol('foo')] = 'Symbol foo'
Object.getOwnPropertySymbols(obj).forEach((key) => {
console.log(obj[key]) // Symbol baz, Symbol foo
})
NumberObject.toString(radix);
,radix为进制数var num = 100;
console.log(num.toString(2)); // 1100100
toString()方法可把一个 Number 对象转换为一个字符串,并返回结果。
parseInt(string, radix);
,radix为进制数var num = '1100100';
console.log(parseInt(num,2)); //
parseInt() 函数可解析一个字符串,并返回一个整数。
通过类似传统面向对象语言,使用构造函数方式 为每个实例添加方法和属性, 这种方式,存在一个问题, 不能达到函数共用,每个实例都会复制到方法。
一般,我们可以通过原型属性(prototype)改造这种方式,达到不同实例共用同一个方法,也可以把原型对象上的所有方法,使用字面量方式简写
如果我们想把面向对象的使用方式更加的优雅,比如链式调用, 我们应该在每个方法中返回对象本身,才能继续调用方法, 即返回this;
var Util = function(){
return {
checkUserName : function(){
console.log( 'userName...' );
return this;
},
checkUserPwd : function(){
console.log( 'userPwd...' );
return this;
},
checkUserEmail : function(){
console.log( 'userEmail...' );
return this;
}
}
}
// 方法中如果没有返回this,下面这种调用方式是错误的
Util().checkUserEmail().checkUserName();
// 方法中返回对象本身,可以链式调用
Util().checkUserEmail().checkUserName().checkUserPwd();
在实际开发中,我们经常需要扩展一些功能和模块。扩展可以在本对象或者父类对象或者原型上,从功能上来说,是没有问题的,但是确造成了全局污染:通俗点说,并不是说有的函数都需要checkUserName这个方法,而我们这样写,所有的函数在创建过程中都会从父类的原型链上继承checkUserName, 但是这个方法,我们根本不用, 所以浪费性能, 为了解决这个问题,我们应该要在需要使用这个方法的函数上添加,不是所有的都添加
可以改造addMethod方法, 在原型上添加函数,而不是实例上, 这样我们就可以达到类式的链式调用
Function.prototype.addMethod = function( name, fn ){
this.prototype[name] = fn;
return this;
};
var fn1 = function(){};
fn1.addMethod( 'checkUserName', function(){
console.log( 'userName:ghostwu' );
return this;
} ).addMethod( 'checkUserEmail', function(){
console.log( 'userEmail' );
return this;
} ).addMethod( 'checkUserPwd', function(){
console.log( 'userUserPwd' );
return this;
} );
new fn1().checkUserName().checkUserEmail().checkUserPwd();
JS对象式编程
parseInt()函数将给定的字符串以指定的基数解析为整数。
parseInt(string,radix)
第二个参数表示使用的进制,我们一般使用10进制,也可能会有到8或者16进制。为了避免对“0”和“0x”开头的字符串解析错误,各种javascript编程规范都规定必须要明确给出第二个参数的值,如parseInt(“123”,10).
parseInt从头解析string为整数,在遇到不能解析的字符时就返回已经解析的整数部分,如果第一个字符就不能解析,就直接返回NaN。
Number()在不用new操作符时,可以用来执行类型转换。如果无法转换为数字,就返回NaN。
像“123a”,parseInt()返回是123,Number()返回是NaN。
不同类型的字符串使用这两个函数的转换区别:
// 当字符串是由数字组成的时候 他们转换的数字一样的没有差别
let numStr = '123'
console.log(parseInt(numStr)) //123
console.log(Number(numStr)) //123
// 当字符串是由字母组成的时候
let numStr = 'abc'
console.log(parseInt(numStr)) //NaN
console.log(Number(numStr)) //NaN
// 当字符串是由数字和字母组成的时候
let numStr = '123a'
console.log(parseInt(numStr)) //123
console.log(Number(numStr)) //NaN
// 当字符串是由0和数字
let numStr = '0123'
console.log(parseInt(numStr)) //123
console.log(Number(numStr)) //123
// 当字符串包含小数点
let numStr = '123.456'
console.log(parseInt(numStr)) //123
console.log(Number(numStr)) //123.456
// 当字符串为null时
let numStr = null
console.log(parseInt(numStr)) //NaN
console.log(Number(numStr)) //0
// 当字符串为''(空)时
let numStr = ''
console.log(parseInt(numStr)) //NaN
console.log(Number(numStr)) //0
js监听页面元素变化window.MutationObserver
js怎么监听元素属性变化
使用proxy拦截set
对于计算机而言,两个数字在相加时是以二进制形式进行的,在呈现结果时才转换成十进制。JS中的数字是用IEEE 754 双精度版本(64位)浮点数来存储的。
如:0.7=(0.1 0110 0110...)B
0.7*2=1.4========取出整数部分1
0.4*2=0.8========取出整数部分0
0.8*2=1.6========取出整数部分1
0.6*2=1.2========取出整数部分1
0.2*2=0.4========取出整数部分0
0.4*2=0.8========取出整数部分0
0.8*2=1.6========取出整数部分1
0.6*2=1.2========取出整数部分1
0.2*2=0.4========取出整数部分0
// 0.1 转化为二进制
0.0 0011 0011 0011 0011...(0011无限循环)
// 0.2 转化为二进制
0.0011 0011 0011 0011 0011...(0011无限循环)
// 相加后
= 0.010011001100110011001100110011001100110011001100110100
= 0.30000000000000004(十进制)
0.1 在二进制中是无限循环的一些数字,其实不只是 0.1,其实很多十进制小数用二进制表示都是无限循环的。这样其实没什么问题,但是 JS 采用的浮点数标准却会裁剪掉我们的数字。
parseFloat((0.1 + 0.2).toFixed(10)) === 0.3 // true
双精度是1位符号,11位指数,52位小数。
JavaScript中数字的存储标准是 IEEE754 浮点数标准。代码中使用的无论是整数还是小数,都是同一种数据类型——64 位双精度浮点型。
所有数字以二进制存储,每个数字对应的二进制分为三段:符号位、指数位、尾数位。
用于存储的二进制有 64 位,其中符号位在六十四位的第一位,0 表示正数,1 表示负数。符号位之后的 11 位是指数位,决定了数字的范围。指数位之后的 52 位是尾数位,决定了数字的精度。
在 JavaScript 中,双精度浮点型的数转化成二进制的数保存,读取时根据指数位和尾数位的值转化成双精度浮点数。
比如说存储 8.8125 这个数,它的整数部分的二进制是 1000,小数部分的二进制是 1101。这两部分连起来是 1000.1101,但是存储到内存中小数点会消失,因为计算机只能存储 0 和 1。
1000.1101 这个二进制数用科学计数法表示是 1.0001101 * 2^3,这里的 3 (二进制是 0011)即为指数。
现在我们很容易判断符号位是 0,尾数位就是科学计数法的小数部分 0001101。指数位用来存储科学计数法的指数,此处为 3。指数位有正负,11 位指数位表示的指数范围是 -1023~1024,所以指数 3 的指数位存储为 1026(3 + 1023)。
可以判断 JavaScript 数值的最大值为 53 位二进制的最大值: 2^53 -1。
PS:科学计数法中小数点前的 1 可以省略,因为这一位永远是 1。0.5 二进制科学计数为 1.00 * 2^-1。
现在再看这倒题的话其实只要记住,setTimeout 和 setInterval 是在相应的时间把任务添加到任务队列,但不能保证它立即执行。记住这个点应该就能够解决这类问题。
setTimeout和setInterval的回调函数,都是经过n毫秒后被添加到队列中,而不是过n毫秒后立即执行。
上图可见,setInterval每隔100ms往队列中添加一个事件;100ms后,添加T1定时器代码至队列中,主线程中还有任务在执行,所以等待,some event执行结束后执行T1定时器代码;又过了100ms,T2定时器被添加到队列中,主线程还在执行T1代码,所以等待;又过了100ms,理论上又要往队列里推一个定时器代码,但由于此时T2还在队列中,所以T3不会被添加,结果就是此时被跳过;这里我们可以看到,T1定时器执行结束后马上执行了T2代码,所以并没有达到定时器的效果。
综上所述,setInterval有两个缺点:
可以这么理解:每个setTimeout产生的任务会直接push到任务队列中;而setInterval在每次把任务push到任务队列前,都要进行一下判断(看上次的任务是否仍在队列中)。
因而我们一般用setTimeout模拟setInterval,来规避掉上面的缺点。
减轻服务器的压力,优先加载可视区域的内容,其他部分等进入了可视区域再加载,从而提高性能。
监听onscroll事件判断资源位置
如上图所示,让在浏览器可视区域的图片显示,可视区域外的不显示,所以当图片距离顶部的距离top-height等于(小于)可视区域h和滚动区域高度s之和时说明图片马上就要进入可视区了,就是说当top-height<=s+h时,图片在可视区。
首先为所有懒加载的静态资源添加自定义属性字段,比如如果是图片,可以指定data-src为真实的图片地址,src指向loading的图片。
然后当资源进入视口的时候,将src属性值替换成data-src的值。
可以使用元素的getBoundingRect().top判断是否在视口内,也可以使用元素距离文档顶部的距离img.offsetTop减去scrollTop是否小于视口高度来判断。
API函数:
页可见区域宽: document.body.clientWidth;
网页可见区域高: document.body.clientHeight;
网页可见区域宽: document.body.offsetWidth (包括边线的宽);
网页可见区域高: document.body.offsetHeight (包括边线的宽);
网页正文全文宽: document.body.scrollWidth;
网页正文全文高: document.body.scrollHeight;
网页被卷去的高: document.body.scrollTop;
网页被卷去的左: document.body.scrollLeft;
网页正文部分上: window.screenTop;
网页正文部分左: window.screenLeft;
屏幕分辨率的高: window.screen.height;
屏幕分辨率的宽: window.screen.width;
屏幕可用工作区高度: window.screen.availHeight;
dom事件机制、DOM0,DOM2,DOM3
维护给每个请求/响应带上序列号,每次只会使用当前收到响应序列号最大的数据。
for(let i = 0; i < 5; i++) {
(function(j) {
setTimeout(() => {
console.log(j);
}, 1000);
})(i);
}
for(let i = 0; i < 5; i++) {
setTimeout(console.log(i), 1000);
}
console.log()不为回调函数,则为同步函数和for循环一起执行
function timer(i) {
setTimeout(() => {
console.log(i);
}, 1000);
}
for(let i = 0; i < 5; i++) {
timer(i);
}
== 允许在相等比较过程中进行强制类型转换,而 === 不允许。
而在 Object.is 中,Object.is(NaN, NaN) -> true , Object.is(-0, +0) ->false,其余与 === 一致
实现一个 Object.is:
Object.prototype.is = function(a, b) {
if(a === b) {
// 可能为 -0 +0
return x !== 0 || 1/x === 1/y
}
// x 与 y 都为 NaN
return x !== x && y !== y
}
1 == '1'
↓
1 == 1
'1' == true
↓
'1' == 1
↓
1 == 1
'1' == { name: 'test' }
↓
'1' == '[object Object]'
const addImageDG = (event) => {
if (event.target.files[0]) {
let reader = new FileReader();
let file = event.target.files[0];
const x = Math.floor(Math.random() * 500)
const y = Math.floor(Math.random() * 400 + 80)
reader.readAsDataURL(file);
reader.onload = function (e) {
var image = new Image();
image.src = e.target.result;
image.onload = function () {
var imageCanvas = document.getElementById('imgCanvas');
var context = imageCanvas.getContext('2d');
const { width, height } = resizeImg(image);
const canvasWidth = getCanvasSizeG().width;
const canvasHeight = getCanvasSizeG().height;
context.drawImage(image, x, y, width, height);
//let imgData = imageCanvas.toDataURL("image/jpeg");
protocol.addImageDL2(x / canvasWidth, y / canvasHeight, width / canvasWidth, height / canvasHeight, e.target.result);
}
}
}
}