整理自己的JS知识点
正文
-
JS中的数据类型(运行时类型)
运行时类型是代码实际执行过程中我们用到的类型。所有的类型数据都会属于7个类型之一。从变量、参数、返回值到表达式中间结果,任何JavaScript代码运行过程中产生的数据,都具有运行时类型。
-
类型
- Undefined
问:为什么有的编程规范要求用void 0代替undefined?
答:Undefined 类型表示未定义,它的类型只有一个值,就是 undefined。任何变量在赋值前是 Undefined 类型、值为 undefined。因为JavaScript的代码undefined是一个变量,而并非是一个关键字,所以,我们为了避免无意中被篡改,建议使用 void 0 来获取undefined值。
- Null
Null 类型也只有一个值,就是 null,它的语义表示空值,与 undefined 不同,null 是 JavaScript 关键字,所以在任何代码中,你都可以放心用 null 关键字来获取 null 值。
- Boolean
Boolean 类型有两个值, true 和 false,它用于表示逻辑意义上的真和假,同样有关键字 true 和 false 来表示两个值。
- String
问:字符串是否有最大长度?
答:String 用于表示文本数据。String 有最大长度是 2^53 - 1,这在一般开发中都是够用的,但是有趣的是,这个所谓最大长度,并不完全是你理解中的字符数。因为String 的意义并非“字符串”,而是字符串的 UTF16 编码,我们字符串的操作 charAt、charCodeAt、length 等方法针对的都是 UTF16 编码。所以,字符串的最大长度,实际上是受字符串的编码长度影响的。
- Number
问:0.1 + 0.2不是等于0.3么?为什么JavaScript里不是这样的?
答:根据双精度浮点数的定义,Number类型中有效的整数范围是-0x1fffffffffffff至0x1fffffffffffff,所以Number无法精确表示此范围外的整数。同样根据浮点数的定义,非整数的Number类型无法用 ==(===也不行) 来比较.浮点数运算的精度问题导致等式左右的结果并不是严格相等,而是相差了个微小的值。所以实际上,这里错误的不是结论,而是比较的方法,正确的比较方法是使用JavaScript提供的最小精度值:
console.log( Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON);
检查等式左右两边差的绝对值是否小于最小精度,才是正确的比较浮点数的方法。JavaScript中的Number类型有 18437736874454810627(即2^64-2^53+3) 个值。JavaScript为了表达几个额外的语言场景(比如不让除以0出错,而引入了无穷大的概念),规定了几个例外情况:NaN,占用了 9007199254740990,这原本是符合IEEE规则的数字;Infinity,无穷大;-Infinity,负无穷大。
- Symbol
问:ES6新加入的Symbol是个什么东西?
答:Symbol 是 ES6 中引入的新类型,它是一切非字符串的对象key的集合,在ES6规范中,整个对象系统被用Symbol 重塑。 Symbol举例:
// 创建 Symbol 的方式是使用全局的 Symbol 函数 var mySymbol = Symbol("my symbol"); // Symbol.iterator 来自定义 for…of 在对象上的行为: var o = new Object o[Symbol.iterator] = function() { var v = 0 return { next: function() { return { value: v++, done: v > 10 } } } }; for(var v of o) console.log(v); // 0 1 2 3 ... 9 // 代码中我们定义了iterator之后,用for(var v of o)就可以调用这个函数,然后我们可以根据函数的行为,产生一个for…of的行为。这里我们给对象o添加了 Symbol.iterator 属性,并且按照迭代器的要求定义了一个0到10的迭代器,之后我们就可以在for of中愉快地使用这个o对象啦。
- Object
问:为什么给对象添加的方法能用在基本类型上?
答:运算符提供了装箱操作,它会根据基础类型构造一个临时对象,使得我们能在基础类型上调用对应对象的方法。
Object 是 JavaScript 中最复杂的类型,也是 JavaScript 的核心机制之一。在 JavaScript 中,对象的定义是“属性的集合”。属性分为数据属性和访问器属性,二者都是key-value结构,key可以是字符串或者 Symbol类型。
提到对象,我们必须要提到一个概念:类。因为 C++ 和 Java 的成功,在这两门语言中,每个类都是一个类型,二者几乎等同,以至于很多人常常会把JavaScript的“类”与类型混淆。事实上,JavaScript 中的“类”仅仅是运行时对象的一个私有属性,而JavaScript中是无法自定义类型的。
JavaScript中的几个基本类型,都在对象类型中有一个“亲戚”。它们是:
- Number
- String
- Boolean
- Symbol
所以,我们必须认识到 3 与 new Number(3) 是完全不同的值,它们一个是 Number 类型, 一个是对象类型。 Number、String和Boolean,三个构造器是两用的,当跟 new 搭配时,它们产生对象,当直接调用时,它们表示强制类型转换。Symbol 函数比较特殊,直接用 new 调用它会抛出错误,但它仍然是 Symbol 对象的构造器。事实上,JavaScript 中的“类”仅仅是运行时对象的一个私有属性,而JavaScript中是无法自定义类型的。
JavaScript 语言设计上试图模糊对象和基本类型之间的关系,我们日常代码可以把对象的方法在基本类型上使用,比如:
console.log("abc".charAt(0)); //a
甚至我们在原型上添加方法,都可以应用于基本类型,比如以下代码,在 Symbol 原型上添加了hello方法,在任何 Symbol 类型变量都可以调用。
Symbol.prototype.hello = () => console.log("hello"); var a = Symbol("a"); console.log(typeof a); //symbol,a并非对象 a.hello(); //hello,有效
-
问题: 解释下:undefined==null // true undefined===null //false
undefined==null
可以从Javascript规范中找到答案:规范中提到,要比较相等性之前,不能将 null 和 undefined 转换成其他任何值,并且规定null 和 undefined 是相等的。其实是一个设计缺陷。
Number(undefined) // NaN Number(null) // 0
undefined===null
这个很好解释,数据类型不同不能全等。
typeof null //object--->其实null应该是属于NULL类型,这是js设计缺陷所致 typeof undefined //undefined
- Undefined
-
null 和 undefined都代表着无效的值。
-
类型转换
在这个里面,较为复杂的部分是Number和String之间的转换,以及对象跟基本类型之间的转换。我们分别来看一看这几种转换的规则。
- StringToNumber
多数情况下,Number 是比 parseInt 和 parseFloat 更好的选择。
在一些古老的浏览器环境中,parseInt还支持0开头的数字作为8进制前缀,这是很多错误的来源。所以在任何环境下,都建议传入parseInt的第二个参数,而parseFloat则直接把原字符串作为十进制来解析,它不会引入任何的其他进制。
- NumberToString
在较小的范围内,数字到字符串的转换是完全符合你直觉的十进制表示。当Number绝对值较大或者较小时,字符串表示则是使用科学计数法表示的。
- 装箱转换
每一种基本类型Number、String、Boolean、Symbol在对象中都有对应的类,所谓装箱转换,正是把基本类型转换为对应的对象,它是类型转换中一种相当重要的种类。
前文提到,全局的 Symbol 函数无法使用 new 来调用,但我们仍可以利用装箱机制来得到一个 Symbol 对象,我们可以利用一个函数的call方法来强迫产生装箱。
我们定义一个函数,函数里面只有return this,然后我们调用函数的call方法到一个Symbol类型的值上,这样就会产生一个symbolObject。
我们可以用console.log看一下这个东西的type of,它的值是object,我们使用symbolObject instanceof 可以看到,它是Symbol这个类的实例,我们找它的constructor也是等于Symbol的,所以我们无论从哪个角度看,它都是Symbol装箱过的对象:
var symbolObject = (function(){ return this; }).call(Symbol("a")); console.log(typeof symbolObject); //object console.log(symbolObject instanceof Symbol); //true console.log(symbolObject.constructor == Symbol); //true
使用内置的 Object 函数,我们可以在JavaScript代码中显式调用装箱能力。
var symbolObject = Object(Symbol("a")); console.log(typeof symbolObject); //object console.log(symbolObject instanceof Symbol); //true console.log(symbolObject.constructor == Symbol); //true
每一类装箱对象皆有私有的 Class 属性,这些属性可以用 Object.prototype.toString 获取:
var symbolObject = Object(Symbol("a")); console.log(Object.prototype.toString.call(symbolObject)); //[object Symbol]
在 JavaScript 中,没有任何方法可以更改私有的 Class 属性,因此Object.prototype.toString 是可以准确识别对象对应的基本类型的方法,它比 instanceof 更加准确。但需要注意的是,call本身会产生装箱操作,所以需要配合 typeof 来区分基本类型还是对象类型。
- 拆箱转换
在JavaScript标准中,规定了 ToPrimitive 函数,它是对象类型到基本类型的转换(即,拆箱转换)。
对象到 String 和 Number 的转换都遵循“先拆箱再转换”的规则。通过拆箱转换,把对象变成基本类型,再从基本类型转换为对应的 String 或者 Number。
拆箱转换会尝试调用 valueOf 和 toString 来获得拆箱后的基本类型。如果 valueOf 和 toString 都不存在,或者没有返回基本类型,则会产生类型错误 TypeError。
var o = { valueOf : () => {console.log("valueOf"); return {}}, toString : () => {console.log("toString"); return {}} } o * 2 // valueOf // toString // TypeError
我们定义了一个对象o,o有valueOf和toString两个方法,这两个方法都返回一个对象,然后我们进行o2这个运算的时候,你会看见先执行了valueOf,接下来是toString,最后抛出了一个TypeError,这就说明了这个拆箱转换失败了。到 String 的拆箱转换会优先调用 toString。我们把刚才的运算从o2换成 String(o),那么你会看到调用顺序就变了。
var o = { valueOf : () => {console.log("valueOf"); return {}}, toString : () => {console.log("toString"); return {}} } String(o) // toString // valueOf // TypeError
在 ES6 之后,还允许对象通过显式指定 @@toPrimitive Symbol 来覆盖原有的行为。
var o = { valueOf : () => {console.log("valueOf"); return {}}, toString : () => {console.log("toString"); return {}} } o[Symbol.toPrimitive] = () => {console.log("toPrimitive"); return "hello"} console.log(o + "") // toPrimitive // hello
- typeof与运行时类型比较
类型”在 JavaScript 中是一个有争议的概念。一方面,标准中规定了运行时数据类型; 另一方面,JS语言中提供了 typeof 这样的运算,用来返回操作数的类型,但 typeof 的运算结果,与运行时类型的规定有很多不一致的地方。
-
JavaScript对象(面向对象还是基于对象?)
在不同的编程语言中,设计者也利用各种不同的语言特性来抽象描述对象,最为成功的流派是使用“类”的方式来描述对象,这诞生了诸如 C++、Java等流行的编程语言。而 JavaScript 早年却选择了一个更为冷门的方式:原型.
在 ES6 出现之前,大量的 JavaScript 程序员试图在原型体系的基础上,把JavaScript变得更像是基于类的编程,进而产生了很多所谓的“框架”,比如PrototypeJS、Dojo。
如果我们从运行时角度来谈论对象,就是在讨论JavaScript实际运行中的模型,这是由于任何代码执行都必定绕不开运行时的对象模型。不过,幸运的是,从运行时的角度看,可以不必受到这些“基于类的设施”的困扰,这是因为任何语言运行时类的概念都是被弱化的。
-
javascript 对象的特征
不论我们使用什么样的编程语言,我们都先应该去理解对象的本质特征(参考Grandy Booch《面向对象分析与设计》)。总结来看,对象有如下几个特点。
- 对象具有唯一标识性:即使完全相同的两个对象,也并非同一个对象。
- 对象有状态:对象具有状态,同一对象可能处于不同状态之下。
- 对象具有行为:即对象的状态,可能因为它的行为产生变迁。
对象具有唯一标识性。一般而言,各种语言的对象唯一标识性都是用内存地址来体现的, 对象具有唯一标识的内存地址,所以具有唯一的标识。所以,任何不同的JavaScript对象其实是互不相等的,我们可以看下面的代码,o1和o2初看是两个一模一样的对象,但是打印出来的结果却是false。
var o1 = { a: 1 }; var o2 = { a: 1 }; console.log(o1 == o2); // false
关于对象的第二个和第三个特征“状态和行为”,不同语言会使用不同的术语来抽象描述它们,比如C++中称它们为“成员变量”和“成员函数”,Java中则称它们为“属性”和“方法”。
在 JavaScript中,将状态和行为统一抽象为“属性”,考虑到 JavaScript 中将函数设计成一种特殊对象(关于这点,我会在后面的文章中详细讲解,此处先不用细究),所以 JavaScript中的行为和状态都能用属性来抽象。
下面这段代码其实就展示了普通属性和函数作为属性的一个例子,其中o是对象,d是一个属性,而函数f也是一个属性,尽管写法不太相同,但是对JavaScript来说,d和f就是两个普通属性。
var o = { d: 1, f() { console.log(this.d); } };
总结:在JavaScript中,对象的状态和行为其实都被抽象为了属性。如果你用过Java,一定不要觉得奇怪,尽管设计思路有一定差别,但是二者都很好地表现了对象的基本特征:标识性、状态和行为。
在实现了对象基本特征的基础上, 我认为,JavaScript中对象独有的特色是:对象具有高度的动态性,这是因为JavaScript赋予了使用者在运行时为对象添改状态和行为的能力。
举个例子,比如,JavaScript 允许运行时向对象添加属性,这就跟绝大多数基于类的、静态的对象设计完全不同。下面这段代码就展示了运行时如何向一个对象添加属性,一开始我定义了一个对象o,定义完成之后,再添加它的属性b,这样操作是完全没问题的。
var o = { a: 1 }; o.b = 2; console.log(o.a, o.b); //1 2
为了提高抽象能力,JavaScript的属性被设计成比别的语言更加复杂的形式,它提供了数据属性和访问器属性(getter/setter)两类。
-
JavaScript对象的两类属性
对JavaScript来说,属性并非只是简单的名称和值,JavaScript用一组特征(attribute)来描述属性(property)
-
第一类属性,数据属性,数据属性具有四个特征。
- value:就是属性的值。
- writable:决定属性能否被赋值。
- enumerable:决定for in能否枚举该属性。
- configurable:决定该属性能否被删除或者改变特征值。
-
第二类属性是访问器(getter/setter)属性,它也有四个特征。
- getter:函数或undefined,在取属性值时被调用。
- setter:函数或undefined,在设置属性值时被调用。
- enumerable:决定for in能否枚举该属性。
- configurable:决定该属性能否被删除或者改变特征值。
访问器属性使得属性在读和写时执行代码,它允许使用者在写和读属性时,得到完全不同的值,它可以视为一种函数的语法糖。
我们通常用于定义属性的代码会产生数据属性,其中的writable、enumerable、configurable都默认为true。我们可以使用内置函数 Object.getOwnPropertyDescripter来查看,如以下代码所示:
var o = { a: 1 }; o.b = 2; //a和b皆为数据属性 Object.getOwnPropertyDescriptor(o,"a") // {value: 1, writable: true, enumerable: true, configurable: true} Object.getOwnPropertyDescriptor(o,"b") // {value: 2, writable: true, enumerable: true, configurable: true}
如果我们要想改变属性的特征,或者定义访问器属性,我们可以使用 Object.defineProperty,示例如下:
var o = { a: 1 }; Object.defineProperty(o, "b", {value: 2, writable: false, enumerable: false, configurable: true}); //a和b都是数据属性,但特征值变化了 Object.getOwnPropertyDescriptor(o,"a"); // {value: 1, writable: true, enumerable: true, configurable: true} Object.getOwnPropertyDescriptor(o,"b"); // {value: 2, writable: false, enumerable: false, configurable: true} o.b = 3; console.log(o.b); // 2
这里我们使用了Object.defineProperty来定义属性,这样定义属性可以改变属性的writable和enumerable。
我们同样用Object.getOwnPropertyDescriptor来查看,发现确实改变了writable和enumerable特征。因为writable特征为false,所以我们重新对b赋值,b的值不会发生变化。
在创建对象时,也可以使用 get 和 set 关键字来创建访问器属性,代码如下所示:
var o = { get a() { return 1 } }; console.log(o.a); // 1
访问器属性跟数据属性不同,每次访问属性都会执行getter或者setter函数。这里我们的getter函数返回了1,所以o.a每次都得到1。
这样,我们就理解了,实际上JavaScript 对象的运行时是一个“属性的集合”,属性以字符串或者Symbol为key,以数据属性特征值或者访问器属性特征值为value。
对象是一个属性的索引结构(索引结构是一类常见的数据结构,我们可以把它理解为一个能够以比较快的速度用key来查找value的字典)。我们以上面的对象o为例,你可以想象一下“a”是key。{writable:true,value:1,configurable:true,enumerable:true}是value。
我们在前面的类型课程中,已经介绍了Symbol类型,能够以Symbol为属性名,这是JavaScript对象的一个特色。出现“JavaScript不是面向对象”这样的说法是由于JavaScript的对象设计跟目前主流基于类的面向对象差异非常大。可事实上,这样的对象系统设计虽然特别,但是JavaScript提供了完全运行时的对象系统,这使得它可以模仿多数面向对象编程范式(JavaScript中两种面向对象编程的范式:基于类和基于原型),所以它也是正统的面向对象语言。
JavaScript语言标准也已经明确说明,JavaScript是一门面向对象的语言,我想标准中能这样说,正是因为JavaScript的高度动态性的对象系统。
JS 的 OO 和其他基于类的 OO 不同之处,在于 JS 可以在运行时修改对象,而 class based 的类只能预先全部定义好,我们并不能在运行时动态修改类。在我理解来说,条条大路通罗马,面向对象是罗马,class based 是一条路,prototype based 是另一条路。而且 Symbol 的出现,暴露出了许多内置接口,让 JS 又在这条路上走了更远一些。
-
-
-
面向对象的两种编程范式:“基于类”和“基于原型”
从ES6开始,JavaScript提供了class关键字来定义类,尽管,这样的方案仍然是基于原型运行时系统的模拟,但是它修正了之前的一些常见的“坑”,统一了社区的方案,这对语言的发展有着非常大的好处。
实际上,“基于类”并非面向对象的唯一形态,如果我们把视线从“类”移开,Brendan当年选择的原型系统,就是一个非常优秀的抽象对象的形式。
-
什么是原型?
“基于类”的编程提倡使用一个关注分类和类之间关系开发模型。在这类语言中,总是先有类,再从类去实例化一个对象。类与类之间又可能会形成继承、组合等关系。类又往往与语言的类型系统整合,形成一定编译时的能力。
“基于原型”的编程看起来更为提倡程序员去关注一系列对象实例的行为,而后才去关心如何将这些对象,划分到最近的使用方式相似的原型对象,而不是将它们分成类。
原型系统的“复制操作”有两种实现思路:一个是并不真的去复制一个原型对象,而是使得新对象持有一个原型的引用;另一个是切实地复制对象,从此两个对象再无关联。JavaScript显然选择了前一种方式。
-
javascript中的原型
抛开JavaScript用于模拟Java类的复杂语法(如new、Function Object、函数的prototype属性等),原型系统可以总结为以下两点:
- 如果所有对象都有私有字段[[prototype]],就是对象的原型;
- 读一个属性,如果对象本身没有,则会继续访问对象的原型,直到原型为空或者找到为止。
这个模型在ES的各个历史版本中并没有很大改变,但从 ES6 以来,JavaScript提供了一系列内置函数,以便更为直接地访问操纵原型。三个方法分别为:
- Object.create 根据指定的原型创建新对象,原型可以是null;
- Object.getPrototypeOf 获得一个对象的原型;
- Object.setPrototypeOf 设置一个对象的原型。
利用这三个方法,我们可以完全抛开类的思维,利用原型来实现抽象和复用。如下示例
// 创建了cat对象 var cat = { say(){ console.log("meow~"); }, jump(){ console.log("jump"); } } // 依据cat创建tiger对象 var tiger = Object.create(cat, { say:{ writable:true, configurable:true, enumerable:true, value:function(){ console.log("roar!"); } } }) var anotherCat = Object.create(cat); anotherCat.say(); var anotherTiger = Object.create(tiger); anotherTiger.say();
-
早期版本中的类与原型
在早期版本中,程序员只能通过Java风格的类接口来操纵原型运行时,可以说非常别扭。考虑到new和prototype属性等基础设施今天仍然有效,追溯一下早年的JavaScript中的原型和类。
在早期版本的JavaScript中,“类”的定义是一个私有属性 [[class]],语言标准为内置类型诸如Number、String、Date等指定了[[class]]属性,以表示它们的类。语言使用者唯一可以访问[[class]]属性的方式是Object.prototype.toString。
以下代码展示了所有具有内置class属性的对象:
var o = new Object; var n = new Number; var s = new String; var b = new Boolean; var d = new Date; var arg = function(){ return arguments }(); var r = new RegExp; var f = new Function; var arr = new Array; var e = new Error; console.log([o, n, s, b, d, arg, r, f, arr, e].map(v => Object.prototype.toString.call(v)));
在ES3和之前的版本,JS中类的概念是相当弱的,它仅仅是运行时的一个字符串属性。在ES5开始,[[class]] 私有属性被 Symbol.toStringTag 代替,Object.prototype.toString 的意义从命名上不再跟 class 相关。
考虑到JavaScript语法中跟Java相似的部分,我们对类的讨论不能用“new运算是针对构造器对象,而不是类”来试图回避。所以,我们仍然要把new理解成JavaScript面向对象的一部分,下面我就来讲一下new操作具体做了哪些事情。new 运算接受一个构造器和一组调用参数,实际上做了几件事:
- 以构造器的 prototype 属性(注意与私有字段[[prototype]]的区分)为原型,创建新对象;
- 将 this 和调用参数传给构造器,执行;
- 如果构造器返回的是对象,则返回,否则返回第一步创建的对象。
new 这样的行为,试图让函数对象在语法上跟类变得相似,但是,它客观上提供了两种方式,一是在构造器中添加属性,二是在构造器的 prototype 属性上添加属性。下面示例展示了用构造器模拟类的两种方法:
//第一种方法是直接在构造器中修改this,给this添加属性。 function c1(){ this.p1 = 1; this.p2 = function(){ console.log(this.p1); } } var o1 = new c1; o1.p2(); // 第二种方法是修改构造器的prototype属性指向的对象,它是从这个构造器构造出来的所有对象的原型。 function c2(){ } c2.prototype.p1 = 1; c2.prototype.p2 = function(){ console.log(this.p1); } var o2 = new c2; o2.p2();
没有Object.create、Object.setPrototypeOf 的早期版本中,new 运算是唯一一个可以指定[[prototype]]的方法(当时的mozilla提供了私有属性__proto__,但是多数环境并不支持)
-
ES6中的类
在ES6中加入了新特性class,new跟function搭配的怪异行为终于可以退休了(虽然运行时没有改变),在任何场景,我都推荐使用ES6的语法来定义类,而令function回归原本的函数语义。下面我们就来看一下ES6中的类。
ES6中引入了class关键字,并且在标准中删除了所有[[class]]相关的私有属性描述,类的概念正式从属性升级成语言的基础设施,从此,基于类的编程方式成为了JavaScript的官方编程范式。类的基本写法:
class Rectangle { constructor(height, width) { this.height = height; this.width = width; } // Getter get area() { return this.calcArea(); } // Method calcArea() { return this.height * this.width; } }
在现有的类语法中,getter/setter和method是兼容性最好的。我们通过get/set关键字来创建getter,通过括号和大括号来创建方法,数据型成员最好写在构造器里面。类的写法实际上也是由原型运行时来承载的,逻辑上JavaScript认为每个类是有共同原型的一组对象,类中定义的方法和属性则会被写在原型对象之上。此外,最重要的是,类提供了继承能力。我们来看一下下面的代码。
class Animal { constructor(name) { this.name = name; } speak() { console.log(this.name + ' makes a noise.'); } } class Dog extends Animal { constructor(name) { super(name); // call the super class constructor and pass in the name parameter } speak() { console.log(this.name + ' barks.'); } } let d = new Dog('Mitzie'); d.speak(); // Mitzie barks.
以上代码创造了Animal类,并且通过extends关键字让Dog继承了它,展示了最终调用子类的speak方法获取了父类的name。比起早期的原型模拟方式,使用extends关键字自动设置了constructor,并且会自动调用父类的构造函数,这是一种更少坑的设计。所以当我们使用类的思想来设计代码时,应该尽量使用class来声明类,而不是用旧语法,拿函数来模拟对象。一些激进的观点认为,class关键字和箭头运算符可以完全替代旧的function关键字,它更明确地区分了定义函数和定义类两种意图。
总结:在新的ES版本中,我们不再需要模拟类了:我们有了光明正大的新语法。而原型体系同时作为一种编程范式和运行时机制存在。我们可以自由选择原型或者类作为代码的抽象风格,但是无论我们选择哪种,理解运行时的原型系统都是很有必要的一件事。
-
-
JavaScript中的对象分类
在前面的课程中我们所讲解的对象,只是特定的一部分,并不能涵盖全部的JavaScript对象。
比如说,我们不论怎样编写代码,都没法绕开Array,实现一个跟原生的数组行为一模一样的对象,这是由于原生数组的底层实现了一个自动随着下标变化的length属性。
并且,在浏览器环境中,我们也无法单纯依靠JavaScript代码实现div对象,只能靠document.createElement来创建。这也说明了JavaScript的对象机制并非简单的属性集合+原型。
我们日常工作中,接触到的主要API,几乎都是由今天所讲解的这些对象提供的。理解这些对象的性质,我们才能真正理解我们使用的API的一些特性。
-
宿主对象(host Objects):由JavaScript宿主环境提供的对象,它们的行为完全由宿主环境决定。
JavaScript宿主对象千奇百怪,但是前端最熟悉的无疑是浏览器环境中的宿主了。在浏览器环境中,我们都知道全局对象是window,window上又有很多属性,如document。实际上,这个全局对象window上的属性,一部分来自JavaScript语言,一部分来自浏览器环境。JavaScript标准中规定了全局对象属性,w3c的各种标准中规定了Window对象的其它属性。宿主对象也分为固有的和用户可创建的两种,比如document.createElement就可以创建一些dom对象。宿主也会提供一些构造器,比如我们可以使用new Image来创建img元素。
-
内置对象(Built-in Objects):由JavaScript语言提供的对象。
- 固有对象(Intrinsic Objects ):由标准规定,随着JavaScript运行时创建而自动创建的对象实例链接。
-
原生对象(Native Objects):可以由用户通过Array、RegExp等内置构造器或者特殊语法创建的对象。
我们把JavaScript中,能够通过语言本身的构造器创建的对象称作原生对象。在JavaScript标准中,提供了30多个构造器。
通过这些构造器,我们可以用new运算创建新的对象,所以我们把这些对象称作原生对象。几乎所有这些构造器的能力都是无法用纯JavaScript代码实现的,它们也无法用class/extend语法来继承(可以继承方法,但是无法完全实现)。
- 普通对象(Ordinary Objects):由{}语法、Object构造器或者class关键字定义类创建的对象,它能够被原型继承。
- 用对象来模拟函数与构造器:函数对象与构造器对象
在JavaScript中,还有一个看待对象的不同视角,这就是用对象来模拟函数和构造器。事实上,JavaScript为这一类对象预留了私有字段机制,并规定了抽象的函数对象与构造器对象的概念。
函数对象的定义是:具有[[call]]私有字段的对象,构造器对象的定义是:具有私有字段[[construct]]的对象。
我们可以这样说,任何对象只需要实现[[call]],它就是一个函数对象,可以去作为函数被调用。而如果它能实现[[construct]],它就是一个构造器对象,可以作为构造器被调用。
对于为JavaScript提供运行环境的程序员来说,只要字段符合,我们在上文中提到的宿主对象和内置对象(如Symbol函数)可以模拟函数和构造器。用户用function关键字创建的函数必定同时是函数和构造器。不过,它们表现出来的行为效果却并不相同。
对于宿主和内置对象来说,它们实现[[call]](作为函数被调用)和[[construct]](作为构造器被调用)不总是一致的。
// 比如内置对象 Date 在作为构造器调用时产生新的对象,作为函数时,则产生字符串。 console.log(new Date); // 1 console.log(Date()) // 浏览器宿主环境中,提供的Image构造器,则根本不允许被作为函数调用。 console.log(new Image); console.log(Image());//抛出错误 // 再比如基本类型(String、Number、Boolean),它们的构造器被当作函数调用,则产生类型转换的效果。
在ES6之后 => 语法创建的函数仅仅是函数,它们无法被当作构造器使用。
对于用户使用 function 语法或者Function构造器创建的对象来说,[[call]]和[[construct]]行为总是相似的,它们执行同一段代码。如下示例:
function f(){ return 1; } var v = f(); //把f作为函数调用 var o = new f(); //把f作为构造器调用
我们大致可以认为,它们[[construct]]的执行过程如下:
- 以 Object.protoype 为原型创建一个新对象;
- 以新对象为 this,执行函数的[[call]];
- 如果[[call]]的返回值是对象,那么,返回这个对象,否则返回第一步创建的新对象。
这样的规则造成了个有趣的现象,如果我们的构造器返回了一个新的对象,那么new创建的新对象就变成了一个构造函数之外完全无法访问的对象,这一定程度上可以实现“私有”。
function cls(){ this.a = 100; return { getValue:() => this.a } } var o = new cls; o.getValue(); //100 //a在外面永远无法访问到
-
特殊行为的对象
除了上面介绍的对象之外,在固有对象和原生对象中,有一些对象的行为跟正常对象有很大区别。它们常见的下标运算(就是使用中括号或者点来做属性访问)或者设置原型跟普通对象不同
- Array:Array的length属性根据最大的下标自动发生变化。
- Object.prototype:作为所有正常对象的默认原型,不能再给它设置原型了。
- String:为了支持下标运算,String的正整数属性访问会去字符串里查找。
- Arguments:arguments的非负整数型下标属性跟对应的变量联动。
- 模块的namespace对象:特殊的地方非常多,跟一般对象完全不一样,尽量只用于import吧。
- 类型数组和数组缓冲区:跟内存块相关联,下标运算比较特殊。
- bind后的function:跟原来的函数相关联。
- 小实验:获取全部JavaScript固有对象
我们从JavaScript标准中可以找到全部的JS对象定义。JS语言规定了全局对象的属性。
三个值:Infinity、NaN、undefined。
九个函数:
- eval
- isFinite
- isNaN
- parseFloat
- parseInt
- decodeURI
- decodeURIComponent
- encodeURI
- encodeURIComponent
一些构造器:Array、Date、RegExp、Promise、Proxy、Map、WeakMap、Set、WeapSet、Function、Boolean、String、Number、Symbol、Object、Error、EvalError、RangeError、ReferenceError、SyntaxError、TypeErrorURIError、ArrayBuffer、SharedArrayBuffer、DataView、Typed Array、Float32Array、Float64Array、Int8Array、Int16Array、Int32Array、UInt8Array、UInt16Array、UInt32Array、UInt8ClampedArray。
四个用于当作命名空间的对象:
- Atomics
- JSON
- Math
- Reflect
我们使用广度优先搜索,查找这些对象所有的属性和Getter/Setter,就可以获得JavaScript中所有的固有对象。
var set = new Set(); var objects = [ eval, isFinite, isNaN, parseFloat, parseInt, decodeURI, decodeURIComponent, encodeURI, encodeURIComponent, Array, Date, RegExp, Promise, Proxy, Map, WeakMap, Set, WeakSet, Function, Boolean, String, Number, Symbol, Object, Error, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError, ArrayBuffer, SharedArrayBuffer, DataView, Float32Array, Float64Array, Int8Array, Int16Array, Int32Array, Uint8Array, Uint16Array, Uint32Array, Uint8ClampedArray, Atomics, JSON, Math, Reflect]; objects.forEach(o => set.add(o)); for(var i = 0; i < objects.length; i++) { var o = objects[i] for(var p of Object.getOwnPropertyNames(o)) { var d = Object.getOwnPropertyDescriptor(o, p) if( (d.value !== null && typeof d.value === "object") || (typeof d.value === "function")) if(!set.has(d.value)) set.add(d.value), objects.push(d.value); if( d.get ) if(!set.has(d.get)) set.add(d.get), objects.push(d.get); if( d.set ) if(!set.has(d.set)) set.add(d.set), objects.push(d.set); } }
-
-
闭包的理解
闭包就是能够读取其他函数内部变量的函数,或者子函数在外调用,子函数所在的父函数的作用域不会被释放。
function makeFunc() { var name = "Mozilla"; function displayName() { alert(name); } return displayName; } var myFunc = makeFunc(); myFunc();
在一些编程语言中,函数中的局部变量仅在函数的执行期间可用。一旦 makeFunc() 执行完毕,我们会认为 name 变量将不能被访问。然而,因为代码运行得没问题,所以很显然在 JavaScript 中并不是这样的。
JavaScript中的函数会形成闭包。 闭包是由函数以及创建该函数的词法环境组合而成。这个环境包含了这个闭包创建时所能访问的所有局部变量。
// 函数科里化 function makeAdder(x) { return function(y) { return x + y; }; } var add5 = makeAdder(5); var add10 = makeAdder(10); console.log(add5(2)); // 7 console.log(add10(2)); // 12
响应点击事件形成的闭包
function makeSizer(size) { return function() { document.body.style.fontSize = size + 'px'; }; } var size12 = makeSizer(12); var size14 = makeSizer(14); var size16 = makeSizer(16); document.getElementById('size-12').onclick = size12; document.getElementById('size-14').onclick = size14; document.getElementById('size-16').onclick = size16;
<p>Some paragraph text</p> <h1>some heading 1 text</h1> <h2>some heading 2 text</h2> <a href="#" id="size-12">12</a> <a href="#" id="size-14">14</a> <a href="#" id="size-16">16</a>
body { font-family: Helvetica, Arial, sans-serif; font-size: 12px; } h1 { font-size: 1.5em; } h2 { font-size: 1.2em; }
用闭包模拟私有方法
编程语言中,比如 Java,是支持将方法声明为私有的,即它们只能被同一个类中的其它方法所调用。
而 JavaScript 没有这种原生支持,但我们可以使用闭包来模拟私有方法。
var Counter = (function() { var privateCounter = 0; function changeBy(val) { privateCounter += val; } return { increment: function() { changeBy(1); }, decrement: function() { changeBy(-1); }, value: function() { return privateCounter; } } })(); console.log(Counter.value()); /* logs 0 */ Counter.increment(); Counter.increment(); console.log(Counter.value()); /* logs 2 */ Counter.decrement(); console.log(Counter.value()); /* logs 1 */
在之前的示例中,每个闭包都有它自己的词法环境;而这次我们只创建了一个词法环境,为三个函数所共享:Counter.increment,Counter.decrement 和 Counter.value。
该共享环境创建于一个立即执行的匿名函数体内。这个环境中包含两个私有项:名为 privateCounter 的变量和名为 changeBy 的函数。这两项都无法在这个匿名函数外部直接访问。必须通过匿名函数返回的三个公共函数访问。
这三个公共函数是共享同一个环境的闭包。多亏 JavaScript 的词法作用域,它们都可以访问 privateCounter 变量和 changeBy 函数。
我们可以把这个函数储存在另外一个变量makeCounter中,并用他来创建多个计数器。
var makeCounter = function() { var privateCounter = 0; function changeBy(val) { privateCounter += val; } return { increment: function() { changeBy(1); }, decrement: function() { changeBy(-1); }, value: function() { return privateCounter; } } }; var Counter1 = makeCounter(); var Counter2 = makeCounter(); console.log(Counter1.value()); /* logs 0 */ Counter1.increment(); Counter1.increment(); console.log(Counter1.value()); /* logs 2 */ Counter1.decrement(); console.log(Counter1.value()); /* logs 1 */ console.log(Counter2.value()); /* logs 0 */
每个闭包都是引用自己词法作用域内的变量 privateCounter 。
每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境。然而在一个闭包内对变量的修改,不会影响到另外一个闭包中的变量。
以这种方式使用闭包,提供了许多与面向对象编程相关的好处 —— 特别是数据隐藏和封装。
在循环中创建闭包常见错误
- 类的创建和继承
- 类的创建(new一个function,在这个function的prototype里面增加属性和方法。)
// 定义一个动物类 function Animal (name) { // 属性 this.name = name || 'Animal'; // 实例方法 this.sleep = function(){ console.log(this.name + '正在睡觉!'); } } // 原型方法 Animal.prototype.eat = function(food) { console.log(this.name + '正在吃:' + food); }; // 这样就生成了一个Animal类,实力化生成对象后,有方法和属性。
-
类的继承
-
原型链继承
function Cat(){ } // 子类的原型指向父类的实例 Cat.prototype = new Animal(); // 重写子类的属性 Cat.prototype.name = 'cat'; // Test Code var cat = new Cat(); console.log(cat.name); // cat console.log(cat.eat('fish')); // cat正在吃:fish console.log(cat.sleep()); // cat正在睡觉! console.log(cat instanceof Animal); //true console.log(cat instanceof Cat); //true var animal = new Animal(); console.log(animal.name); // Animal console.log(animal.eat('fish')); // Animal正在吃:fish console.log(animal.sleep()); // Animal正在睡觉!
function Person(name,sex){ this.name=name; this.sex=sex; this.friends = {lily:'female',lucy:'female'}; this.showFriends=function(){ var str = '' for(i in this.friends){ str+=i +' '+this.friends[i] +','; } console.log('my friends:'+str); } } Person.prototype.hello=function(){ console.log('hello:'+this.name); } var per1 = new Person('A','male'); per1.hello(); // hello:A per1.showFriends();// my friends:lily female,lucy female, function Student(className){ this.class = className; } Student.prototype = new Person('B','male');//原型继承将子对象的原型对象指向父对象的实例; 缺点:不能由子对象向父对象传递参数, var stu1 = new Student(1);//不能由子对象像父对象传递参数, stu1.name='C'; stu1.hello();// hello:C stu1.friends.C = 'male';//2、对于引用型的属性修改之后会影响其他的实例对象; stu1.showFriends();//2、对于引用型的属性修改之后会影响其他的实例对象; // my friends:lily female,lucy female,C male, console.log(stu1 instanceof Student); // true console.log(stu1 instanceof Person); // true var stu2 = new Student(2); stu2.name='D'; stu2.hello(); // hello:D stu2.showFriends();//2、对于引用型的属性修改之后会印象其他的实例对象; // my friends:lily female,lucy female,C male, console.log(stu2 instanceof Student); // true console.log(stu2 instanceof Person); // true
特点:1.基于原型链,既是父类的实例,也是子类的实例。2.子类的原型指向父类的实例。3.子类继承父类的实例上的属性和方法,也继承原型上的方法。
缺点:1.无法实现多继承 2.当引用类型的属性修改后会影响到其他的所有实例 3.不能由子对象像父对象传递参数
-
构造继承
使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型),这种形式的继承,每个子类实例都会拷贝一份父类构造函数中的方法,作为实例自己的方法
为了解决对象引用的问题,我们调用构造函数来实现继承,保证每个实例拥有相同的父类属性,但值之间互不影响。
function Person(name,sex){ this.name=name; this.sex=sex; this.friends = {lily:'female',lucy:'female'}; this.showFriends=function(){ var str = '' for(i in this.friends){ str+=i +' '+this.friends[i] +','; } console.log('my friends:'+str); } } Person.prototype.hello=function(){ console.log('hello:'+this.name); } //构造函数继承 function Teacher(name,sex,type){ this.type=type; Person.call(this,name,sex); } var tea1 = new Teacher('E','female','数学'); //tea1.hello(); //报错没有继承到原型上的方法 tea1.friends.F = 'male'; tea1.showFriends(); // my friends:lily female,lucy female,F male, var tea2 = new Teacher('G','male','语文'); tea2.friends.H = 'male'; tea2.showFriends(); // my friends:lily female,lucy female,H male, console.log(tea2 instanceof Teacher); // true console.log(tea2 instanceof Person); // false
特点:可以实现多继承,
缺点:1、不能继承父对象原型上的方法 2、每次实例化对象会重新构建函数,浪费内存。
-
组合继承(原型构造继承:最通用)
把父对象方法挂载到父类的原型对象上去,实现方法复用。将子类原型的构造函数重新指向子类本身的构造函数,因为原型链继承而被重写。
//父类:人 function Person () { this.head = '脑袋瓜子'; this.emotion = ['喜', '怒', '哀', '乐']; //人都有喜怒哀乐 } //将 Person 类中需共享的方法放到 prototype 中,实现复用 Person.prototype.eat = function () { console.log('吃吃喝喝'); } Person.prototype.sleep = function () { console.log('睡觉'); } Person.prototype.run = function () { console.log('快跑'); } //子类:学生,继承了“人”这个类 function Student(studentID) { this.studentID = studentID; // 构造继承 Person.call(this); } // 原型链继承 Student.prototype = new Person(); //此时 Student.prototype 中的 constructor 被重写了,会导致 stu1.constructor === Person Student.prototype.constructor = Student; //因重写原型而失去constructor属性,所以要对constrcutor重新赋值.将 Student 原型对象的 constructor 指针重新指向 Student 本身 var stu1 = new Student(1001); console.log(stu1.emotion); //['喜', '怒', '哀', '乐'] stu1.emotion.push('愁'); console.log(stu1.emotion); //["喜", "怒", "哀", "乐", "愁"] var stu2 = new Student(1002); console.log(stu2.emotion); //["喜", "怒", "哀", "乐"] stu1.eat(); //吃吃喝喝 stu2.run(); //快跑 console.log(stu1.constructor); //Student
特点:可以继承实例属性/方法,也可以继承原型属性/方法
缺点:调用了两次父类构造函数,生成了两份实例
-
寄生组合继承
通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性
function Super(name) { this.name = name; this.colors = ['red', 'blue']; }; function Sub(name, age) { Super.call(this, name); this.age = age; } Super.prototype.sayName = function () { return this.name; }; // 我们封装其继承过程 function inheritPrototype(Sub, Super) { // 以该对象为原型创建一个新对象 var prototype = Object.create(Super.prototype); prototype.constructor = Sub; // 为父类原型的副本添加构造函数 Sub.prototype = prototype; } inheritPrototype(Sub, Super); Sub.prototype.sayAge = function () { return this.age; } var instance = new Sub('lee', 40); instance.sayName(); // lee instance.sayAge(); // 40
这种方式只调用了一次父类构造函数,只在子类上创建一次对象,同时保持原型链,还可以使用instanceof和isPrototypeOf()来判断原型,是我们最理想的继承方式。
寄生组合式继承就是一个借用构造函数 + 相当于浅拷贝父类的原型对象。构造函数来继承属性,通过原型链的方式来继承方法。
-
js原型链理解
-
原型对象、构造函数、实例对象之间的关系
- prototype:构造函数中的属性,指向该构造函数的原型对象。
- constructor:原型对象中的属性,指向该原型对象的构造函数
- proto:实例中的属性,指向new这个实例的构造函数的原型对象
-
什么是原型链
在JavaScript 中,每个对象都有一个指向它的原型(prototype)对象的内部链接。这个原型对象又有自己的原型,直到某个对象的原型为 null 为止(也就是不再有原型指向),组成这条链的最后一环。这种一级一级的链结构就称为原型链(prototype chain)。
原型-显式原型-隐式原型-共享原型链
显式原型(explicit prototype property )每一个函数在创建之后都会拥有一个名为prototype的属性,这个属性指向函数的原型对象。用来实现基于原型的继承与属性的共享。
隐式原型 (implicit prototype link) JS中任意对象都有一个内置属性__proto__(部分浏览器为[[prototype]]),指向创建这个对象的函数(即构造函数)constructor的prototype。用来构成原型链,同样用于实现基于原型的继承。
当我们「读取」 obj.toString 时,JS 引擎会做下面的事情:
1. 看看 obj 对象本身有没有 toString 属性。没有就走到下一步。 2. 看看 obj.__proto__ 对象有没有 toString 属性,发现 obj.__proto__ 有 toString 属性,于是找到了 3. 如果 obj.__proto__ 没有,那么浏览器会继续查看 obj.__proto__.__proto__,如果 obj.__proto__.__proto__ 也没有,那么浏览器会继续查,obj.__proto__.__proto__.proto__ 直到找到 toString 或者 __proto__ 为 null(不管你从那个属性开始,连续引用__proto__的指针,最后输出的那个值就是null)。 上面的过程,就是「读」属性的「搜索过程」。 而这个「搜索过程」,是连着由 __proto__ 组成的链子一直走的。 这个链子,就叫做「原型链」。
普通对象
最普通的对象:有__proto__属性(指向其原型链),没有prototype属性。 原型对象(Person.prototype 原型对象还有constructor属性(指向构造函数对象))
函数对象:
凡是通过new Function()创建的都是函数对象。 拥有__proto__、prototype属性(指向原型对象)。
-
详解原型对象
每创建一个函数都会有一个prototype属性,这个属性是一个指针,指向一个对象(通过该构造函数创建实例对象的原型对象)。原型对象是包含特定类型的所有实例共享的属性和方法。原型对象的好处是,可以让所有实例对象共享它所包含的属性和方法。
原型对象属于普通对象。Function.prototype是个例外,它是原型对象,却又是函数对象,作为一个函数对象,它又没有prototype属性。
-
-
-
ES6的类和继承
class Father{ constructor(name){ this.name = name; } sayName(){ console.log(this.name); } } class Son extends Father{ //extents后面跟表示要继承的类型 constructor(name, age){ super(name); //相当于以前的:Father.call(this, name); // 类似于call的继承:在这里super相当于把Father的constructor给执行了,并且让方法中的this是B的实例,super当中传递的实参都是在给Father的constructor传递。 // super只是语法糖,实际还是借用构造函数实现基于原型的继承 this.age = age; } //子类独有的方法 sayAge(){ console.log(this.age); } } var son1 = new Son("李四", 30); son1.sayAge(); // 30 son1.sayName(); // 李四 console.log(son1 instanceof Son); // true console.log(son1 instanceof Father); //true
-
关于super的使用,有几点需要注意:
- 你只能在派生类中使用 super(),否则(没有使用 extends 的类或函数中使用)一个错误会被抛出。
- 你必须在构造函数的起始位置调用 super(),因为它会初始化 this。任何在 super() 之前访问 this 的行为都会造成错误。也即是说super()必须放在构造函数的首行。
- 在类构造函数中,唯一能避免调用 super() 的办法是返回一个对象。
总结:Class中的super(),它在这里表示调用父类的构造函数,用来新建父类的this对象。super()相当于Parent.prototype.constructor.call(this)
-
在子类中屏蔽父类的方法(重写)
如果在子类中声明与父类中的同名的方法,则会覆盖父类的方法。(这种情况在其他语言中称之为 方法的覆写、重写 )
class Father{ constructor(name){ this.name = name; } sayName(){ console.log(this.name); } } class Son extends Father{ //extents后面跟表示要继承的类型 constructor(name, age){ super(name); //相当于以前的:Father.call(this, name); this.age = age; } //子类独有的方法 sayAge(){ console.log(this.age); } //子类中的方法会屏蔽到父类中的同名方法。 sayName(){ super.sayName(); //调用被覆盖的父类中的方法。 console.log("我是子类的方法,我屏蔽了父类:" + this.name); } } var son1 = new Son("李四", 30); son1.sayAge(); // 30 son1.sayName(); // 李四 我是子类的方法,我屏蔽了父类:李四
注意:
如果是调用构造方法,则super不要加点,而且必须是在子类构造方法的第一行调用父类的构造方法 普通方法调用需要使用super.父类的方法() 来调用。 super([arguments]); // 调用 父对象/父类 的构造函数,,super方法返回的是子类的实例对象 super.functionOnParent([arguments]); // 调用 父对象/父类 原型上的方法,但是this指向子类的实例对象
-
静态方法也可以继承
class Father{ static foo(){ console.log("我是父类的静态方法"); } } class Son extends Father{ } Son.foo(); //子类也继承了父类的静态方法。 这种方式调用和直接通过父类名调用时一样的。
-
原始class实现与语法糖实现
// 原始class实现实际就是组合继承 function Parent(name) { this.name = name; } Parent.prototype.getName = function() { return this.name; } function Child(name, age) { //借用构造函数 Parent.call(this, name); this.age = age; } //实现继承 Child.prototype = new Parent(); Child.prototype.constructor = Child; Child.prototype.getAge = function(){ return this.Age; }; var people = new Child("lily", 20); console.log(people.getName());
// 语法糖形式 class Parent { constructor(name) { this.name = name; } getName() { return this.name; } } class Child extends Parent { constructor(name, age) { super(name); this.age = age; } getAge() { return this.age; } } const people = new Child("lily", 20); console.log(people.getName());
-
-
事件委托
简介:事件委托指的是,不在事件的发生地(直接dom)上设置监听函数,而是在其父元素上设置监听函数,通过事件冒泡,父元素可以监听到子元素上事件的触发,通过判断事件发生元素DOM的类型,来做出不同的响应。
举例:最经典的就是ul和li标签的事件监听,比如我们在添加事件时候,采用事件委托机制,不会在li标签上直接添加,而是在ul父元素上添加。
好处:比较合适动态元素的绑定,新添加的子元素也会有监听函数,也可以有事件触发机制。
事件委托的实现机制就是依靠事件冒泡:
事件冒泡会从当前触发的事件目标一级一级往上传递,依次触发,直到document为止。
事件捕获会从document开始触发,一级一级往下传递,依次触发,直到真正事件目标为止。
● mouseover和mouseenter的区别
mouseover:当鼠标移入元素或其子元素都会触发事件,所以有一个重复触发,冒泡的过程。对应的移除事件是mouseout
mouseenter:当鼠标移入元素本身(不包含元素的子元素)会触发事件,也就是不会冒泡,对应的移除事件是mouseleave
-
bind,apply,call的区别
apply和call改变函数的this指向,他们两个函数的第一个参数都是一样的表示要改变指向的那个对象,第二个参数,apply是数组,而call则是arg1,arg2…这种形式。通过bind改变this作用域会返回一个新的函数,这个函数不会马上执行。
- 异步加载js的方法
defer:只支持IE如果您的脚本不会改变文档的内容,可将defer属性加入到scrip标签中,以便加快处理文档的速度。因为浏览器知道它将能够安全地读取文档的剩余部分而不用执行脚本,它将推迟对脚本的解释,直到文档已经显示给用户为止。
async:HTML5属性仅适用于外部脚本,并且如果在IE中,同时存在defer和async,那么defer的优先级比较高,脚本将在页面完成时执行。
创建script标签,插入到DOM中
-
JS中的垃圾回收机制
由于字符串、对象和数组没有固定大小,所有当他们的大小已知时,才能对他们进行动态的存储分配。JavaScript程序每次创建字符串、数组或对象时,解释器都必须分配内存来存储那个实体。只要像这样动态地分配了内存,最终都要释放这些内存以便他们能够被再用,否则,JavaScript的解释器将会消耗完系统中所有可用的内存,造成系统崩溃。
这段话解释了为什么需要系统需要垃圾回收,JS不像C/C++,他有自己的一套垃圾回收机制(Garbage Collection)。
JavaScript的解释器可以检测到何时程序不再使用一个对象了,当他确定了一个对象是无用的时候,他就知道不再需要这个对象,可以把它所占用的内存释放掉了。例如:
var a="hello world"; var b="world"; var a=b; //这时,会释放掉"hello world",释放内存以便再引用
垃圾回收的方法:标记清除、计数引用。
-
标记清除
这是最常见的垃圾回收方式,当变量进入环境时,就标记这个变量为”进入环境“,从逻辑上讲,永远不能释放进入环境的变量所占的内存,永远不能释放进入环境变量所占用的内存,只要执行流程进入相应的环境,就可能用到他们。当离开环境时,就标记为离开环境。
垃圾回收器在运行的时候会给存储在内存中的变量都加上标记(所有都加),然后去掉环境变量中的变量,以及被环境变量中的变量所引用的变量(条件性去除标记),删除所有被标记的变量,删除的变量无法在环境变量中被访问所以会被删除,最后垃圾回收器,完成了内存的清除工作,并回收他们所占用的内存。
-
引用计数法
另一种不太常见的方法就是引用计数法,引用计数法的意思就是每个值没引用的次数,当声明了一个变量,并用一个引用类型的值赋值给改变量,则这个值的引用次数为1,;相反的,如果包含了对这个值引用的变量又取得了另外一个值,则原先的引用值引用次数就减1,当这个值的引用次数为0的时候,说明没有办法再访问这个值了,因此就把所占的内存给回收进来,这样垃圾收集器再次运行的时候,就会释放引用次数为0的这些值。
用引用计数法会存在内存泄露,下面来看原因:
function problem() { var objA = new Object(); var objB = new Object(); objA.someOtherObject = objB; objB.anotherObject = objA; }
在这个例子里面,objA和objB通过各自的属性相互引用,这样的话,两个对象的引用次数都为2,在采用引用计数的策略中,由于函数执行之后,这两个对象都离开了作用域,函数执行完成之后,因为计数不为0,这样的相互引用如果大量存在就会导致内存泄露。
特别是在DOM对象中,也容易存在这种问题:
var element=document.getElementById(’‘); var myObj=new Object(); myObj.element=element; element.someObject=myObj;
这样就不会有垃圾回收的过程。
内存管理的详细解读:链接
-
-
setTimeout、setInterval和requestAnimationFrame之间的区别
与setTimeout和setInterval不同,requestAnimationFrame不需要设置时间间隔,
大多数电脑显示器的刷新频率是60Hz,大概相当于每秒钟重绘60次。大多数浏览器都会对重绘操作加以限制,不超过显示器的重绘频率,因为即使超过那个频率用户体验也不会有提升。因此,最平滑动画的最佳循环间隔是1000ms/60,约等于16.6ms。
RAF采用的是系统时间间隔,不会因为前面的任务,不会影响RAF,但是如果前面的任务多的话, 会响应setTimeout和setInterval真正运行时的时间间隔。
// 用法 requestID = requestAnimationFrame(callback); //控制台输出1和0 var timer = requestAnimationFrame(function(){ console.log(0); }); console.log(timer);//1 // cancelAnimationFrame方法用于取消定时器 //控制台什么都不输出 var timer = requestAnimationFrame(function(){ console.log(0); }); cancelAnimationFrame(timer); // 也可以直接使用返回值进行取消 var timer = requestAnimationFrame(function(){ console.log(0); }); cancelAnimationFrame(1); // IE9-浏览器不支持该方法 // 简单兼容 if (!window.requestAnimationFrame) { requestAnimationFrame = function(fn) { setTimeout(fn, 17); }; } // 严格兼容 if(!window.requestAnimationFrame){ var lastTime = 0; window.requestAnimationFrame = function(callback){ var currTime = new Date().getTime(); var timeToCall = Math.max(0,16.7-(currTime - lastTime)); var id = window.setTimeout(function(){ callback(currTime + timeToCall); },timeToCall); lastTime = currTime + timeToCall; return id; } }
<!-- 使用setInterval、setTimeout和requestAnimationFrame这三个方法制作一个简单的进制度效果 --> <!-- 1. setInterval --> <div id="myDiv" style="background-color: lightblue;width: 0;height: 20px;line-height: 20px;">0%</div> <button id="btn">run</button> <script> var timer; btn.onclick = function(){ clearInterval(timer); myDiv.style.width = '0'; timer = setInterval(function(){ if(parseInt(myDiv.style.width) < 500){ myDiv.style.width = parseInt(myDiv.style.width) + 5 + 'px'; myDiv.innerHTML = parseInt(myDiv.style.width)/5 + '%'; }else{ clearInterval(timer); } },16); } </script> <!-- 2. setTimeout --> <script> var timer; btn.onclick = function(){ clearTimeout(timer); myDiv.style.width = '0'; timer = setTimeout(function fn(){ if(parseInt(myDiv.style.width) < 500){ myDiv.style.width = parseInt(myDiv.style.width) + 5 + 'px'; myDiv.innerHTML = parseInt(myDiv.style.width)/5 + '%'; timer = setTimeout(fn,16); }else{ clearTimeout(timer); } },16); } </script> <!-- 3. requestAnimationFrame --> <script> btn.onclick = function(){ myDiv.style.width = '0'; cancelAnimationFrame(timer); timer = requestAnimationFrame(function fn(){ if(parseInt(myDiv.style.width) < 500){ myDiv.style.width = parseInt(myDiv.style.width) + 5 + 'px'; myDiv.innerHTML = parseInt(myDiv.style.width)/5 + '%'; timer = requestAnimationFrame(fn); }else{ cancelAnimationFrame(timer); } }); } </script>
特点:
(1)requestAnimationFrame会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率。
(2)在隐藏或不可见的元素中,requestAnimationFrame将不会进行重绘或回流,这当然就意味着更少的CPU、GPU和内存使用量
(3)requestAnimationFrame是由浏览器专门为动画提供的API,在运行时浏览器会自动优化方法的调用,并且如果页面不是激活状态下的话,动画会自动暂停,有效节省了CPU开销。
(4)setInterval的缺陷,使用setInterval()创建的定时器确保了定时器代码规则地插入队列中。这个问题在于:如果定时器代码在代码再次添加到队列之前还没完成执行,结果就会导致定时器代码连续运行好几次。而之间没有间隔。不过幸运的是:javascript引擎足够聪明,能够避免这个问题。当且仅当没有该定时器的代码实例时,才会将定时器代码添加到队列中。这确保了定时器代码加入队列中最小的时间间隔为指定时间。这种重复定时器的规则有两个问题:1.某些间隔会被跳过 2.多个定时器的代码执行时间可能会比预期小。
-
js实现sleep效果
-
while循环
function sleep(ms){ var start=Date.now(),expire=start+ms; while(Date.now()<expire); console.log('1111'); return; } sleep(1000);
-
Promise实现
function sleep(ms){ var temple=new Promise((resolve)=>{ console.log(111); setTimeout(resolve,ms) }); return temple } sleep(500).then(function(){ console.log(222) })
-
通过async封装
function sleep(ms){ return new Promise((resolve)=>setTimeout(resolve,ms)); } // async函数必需返回一个promise async function test(){ // await 后面必需是个异步任务,等待异步任务返回promise结果 var temple=await sleep(1000); console.log(1111) return temple } test();
-
通过generate来实现
function* sleep(ms){ yield new Promise(function(resolve,reject){ console.log(111); setTimeout(resolve,ms); }) } sleep(500).next().value.then(function(){console.log(2222)})
-
-
如何获取对象的属性
for(let I in obj)该方法依次访问一个对象及其原型链中所有可枚举的类型
object.keys:返回一个数组,包括所有可枚举的属性名称
object.getOwnPropertyNames:返回一个数组包含不可枚举的属性
-
为什么 Function instanceof Object 与 Object instanceof Function都为true?
-
Prototype、__proto__与Object、Function关系介绍
Function、Object:都是Js自带的函数对象。
prototype:每一个函数对象都有一个显式的prototype属性(普通对象没有prototype),它代表了对象的原型(Function.prototype是一个对象,有constructor和__proto__两个属性,constructor指向构造函数本身,__proto__指向于它所对应的原型对象)。
proto:每个对象都有一个名为__proto__的内部隐藏属性,指向于它所对应的原型对象(chrome、firefox中名称为__proto__,并且可以被访问到)。原型链正是基于__proto__才得以形成(note:不是基于函数对象的属性prototype)。
var o1 = {}; var o2 =new Object(); function f1(){} var f2 = function(){} var f3 = new Function('str','console.log(str)'); f3('aabb'); // aabb console.log('typeof Object:'+typeof Object); //function console.log('typeof Function:'+typeof Function); //function console.log('typeof o1:'+typeof o1); //object console.log('typeof o2:'+typeof o2); //object console.log('typeof f1:'+typeof f1); //function console.log('typeof f2:'+typeof f2); //function console.log('typeof f3:'+typeof f3); //function
通常我们认为o1、o2是对象,即普通对象;f1、f2、f3为函数。
但是其实函数也是对象,是由Function构造的,
f3这种写法就跟对象的创建的写法一样。f1、f2最终也都像f3一样是有Function这个函数构造出来的
f1、f2、f3为函数对象,Function跟Object本身也是函数对象。
Js中每个对象(null除外)都和另一个对象相关联,通过以下例子跟内存效果图来分析Function、Object、Prototype、__proto__对象间的关系。
function Animal(){ } var anim = new Animal(); console.log('***********Animal anim proto*****************'); console.log('typeof Animal.prototype:' +typeof Animal.prototype); //object console.log('anim.__proto__===Animal.prototype:'+(anim.__proto__===Animal.prototype)); //true console.log('Animal.__proto__===Function.prototype:'+(Animal.__proto__===Function.prototype)); //true console.log('Animal.prototype.__proto__===Object.prototype:'+(Animal.prototype.__proto__===Object.prototype)); //true console.log('***********Function proto*****************'); console.log('typeof Function.prototype:'+typeof Function.prototype); //function console.log('typeof Function.__proto__:'+typeof Function.__proto__); //function console.log('typeof Function.prototype.prototype:'+typeof Function.prototype.prototype); //undefined console.log('typeof Function.prototype.__proto__:'+typeof Function.prototype.__proto__); //object console.log('Function.prototype===Function.__proto__:'+(Function.prototype===Function.__proto__)); //true console.log('***********Object proto*****************'); console.log('typeof Object.prototype:'+typeof Object.prototype); //object console.log('typeof Object.__proto__:'+typeof Object.__proto__); //function console.log('Object.prototype.prototype:'+Object.prototype.prototype); //undefied console.log('Object.prototype.__proto__===null:'+(Object.prototype.__proto__===null)); //null console.log('***********Function Object proto关系*****************'); console.log('Function.prototype===Object.__proto__:'+(Function.prototype===Object.__proto__)); //true console.log('Function.__proto__===Object.__proto__:'+(Function.__proto__===Object.__proto__)); //true console.log('Function.prototype.__proto__===Object.prototype:'+(Function.prototype.__proto__===Object.prototype)); //true /********************* 系统定义的对象Array、Date ****************************/ console.log('**************test Array、Date****************'); var array = new Array(); var date = new Date(); console.log('array.__proto__===Array.prototype:'+(array.__proto__===Array.prototype)); //true console.log('Array.__proto__===Function.prototype:'+(Array.__proto__===Function.prototype)); //true console.log('date.__proto__===Date.prototype:'+(date.__proto__===Date.prototype)); //true console.log('Date.__proto__===Function.prototype:'+(Date.__proto__===Function.prototype)); //true
-
Function、Object、Prototype、proto、constructor关系图
通过上图Function、Object、Prototype关系图中,可以得出一下几点:
所有对象(包括普通对象,函数对象)的原型链(__proto__)最终都指向了Object.prototype,而Object.prototype.__proto__===null,原型链至此结束。
var o1={}; //o1是普通对象
o1.__proto__ == Object.prototype; //true
*.prototype是一个普通对象,包含constructor和__proto__属性(constructor用来说明谁构造的实例,__proto__用来指向构造函数的prototype,继承就是通过他来实现的)。
Object是一个函数对象,也是Function构造的,Object.prototype是一个普通对象。
Object.prototype.__proto__指向null。
- Prototype跟Constructor关系介绍
在 JavaScript 中,每个函数对象都有名为“prototype”的对象属性,该对象属性中有个名为“constructor”的属性,它反过来引用函数本身。这是一种循环引用( Animal.prototype.constructor===Animal)。 通过以下例子以及上图可以看出Prototype、constructor间的关系。
console.log('**************constructor****************');
console.log('anim.constructor===Animal:'+(anim.constructor===Animal)) ; //true
console.log('Animal===Animal.prototype.constructor:'+(Animal===Animal.prototype.constructor)) ; //true
console.log('Animal.constructor===Function.prototype.constructor:'+(Animal.constructor===Function.prototype.constructor)); //true
console.log('Function.prototype.constructor===Function:'+(Function.prototype.constructor===Function)); //true
console.log('Function.constructor===Function.prototype.constructor:'+(Function.constructor===Function.prototype.constructor)); //true
console.log('Object.prototype.constructor===Object:'+(Object.prototype.constructor===Object)); //true
console.log('Object.constructor====Function:'+(Object.constructor===Function)); //true
注意Object.constructor===Function;本身Object就是Function函数构造出来的
如何查找一个对象的constructor,就是在该对象的原型链上寻找碰到的第一个constructor属性所指向的对象。
- 简单例子
// 示例
Function.prototype.a = 1;
Object.prototype.b = 2;
function A() {}
var a = new A();
console.log(a.a, a.b); // undefined, 2
console.log(A.a, A.b); // 1, 2