对未初始化的变量执行typeof操做符会返回undefined
值,而对未声明的变量执行typeof操做符一样也会返回undefined
算法
var message; console.log(typeof message); // => undefined console.log(typeof gaga); // => undefined
各类类型转换成Boolean的规则数组
数据类型 | 转成true的值 | 转成false的值 |
---|---|---|
Boolean | true | false |
String | 任何非空字符串 | ""空字符串 |
Number | 任何非零数字值(包括无穷大) | 0和NaN |
Object | 任何对象 | null |
Undefined | n/a | undefined |
Number类型应该是ECMAScript中最使人关注的数据类型了。浏览器
除了以十进制表示外,整数还能够经过八进制或十六进制表示,其中,八进制字面值的第一位必须是0,而后是八进制数字序列(0 ~ 7)。若是字面值中的数值超出了范围,那么前导0将被忽略,后面的数值将被看成十进制数值解析app
var n = 070; // => 56 var n = 079; // => 79(无效的八进制数值) var n = 08; // => 8(无效的八进制数值)
八进制字面量在严格模式下是无效的,会致使支持的JavaScript引擎抛出错位。函数
十六进制字面值的前两位必须是0x,后边跟着任何十六进制数字(0 ~ 9 及 A ~ F)。其中,字母A ~ F 能够大写,也能够小写。性能
var n = 0xA; // 10 var n = 0x1f; // 31
计算的时候,八进制和十六进制都将转成十进制后再计算。测试
因为保存浮点数值须要的内存空间是保存整数值的两倍,所以ECMAScript会不失时机的将浮点数值转换为整数值。this
永远不要测试某个特定的浮点数值:prototype
if (a + b == 0.3) { alert("You got 0.3"); }
上边的例子中,咱们测试的是两个数的和是否是等于0.3。若是这两个数是0.05和0.25,或者是0.15和0.15都不会有问题。若是这两个数是0.1和0.2,那么测试就没法经过。指针
因为内存的限制,ECMAScript并不能保存世界上全部的数值。若是某次计算的结果获得了一个超出JavaScript数值范围的值,那么这个数值将被自动转换成特殊的Infinity值,若是这个数值是负数,则会转成-Infinity。出现正或负的Infinity值就不能继续计算了。可使用isFinite()函数判断一个数值是否是有穷的。
NaN是Not a Number的缩写,它有两个非同寻常的特色:
isNan()函数的原理是:在接受一个值后,会尝试将这个值转换成数值,成功就返回false,失败则返回true。
有3个函数能够把非数值转换成数值:Number(),parseInt()和parseFloat()。Number函数能够用于任何数据类型,另外两个则专门用于把字符串转换成数值。
Number()函数的转换规则以下:
若是是字符串,遵循下列规则:
若是是对象,则调用对象的valueOf()方法,而后依照前面的规则转换返回的值,若是转换的结果是NaN,则调用对象的toString()方法,而后再一次按照前面的规则转换返回的字符串值。
var n = Number("Hello world"); // NaN var n = Number(""); // 0 var n = Number("000011"); // 11 var n = Number("true"); // 1
parseInt()和parseFloat()在使用的时候须要特别注意进制的问题,parseFloat()只解析十进制。
String()方法内部转换规则:
var n1 = 10; var n2 = true; var n3 = null; var n4; console.log(String(n1)); // => "10" console.log(String(n2)); // => "true" console.log(String(n3)); // => "null" console.log(String(n4)); // => "undefined"
逻辑与(&&)能够应用于任何类型的操做数,而不只仅是布尔值。在有一个操做数不是布尔值的状况下,逻辑与操做就不必定返回布尔值,它遵循下列规则:
逻辑与操做属于短路操做,即若是第一个操做数可以决定结果,那么就不会再对第二个操做数求值,这个跟有些语言不同,所以在条件语句中使用逻辑与的时候要特别注意。
var n = true && NaN; console.log(String(n)); // => NaN var n2 = Boolean(n); console.log(n2); // => false if (!n) { console.log("ok"); // => ok }
打印出了ok,说明在条件语句中可使用&&,可是须要明白返回值的问题。
相等(==)操做符在进行比较以前会对操做数进行转换,咱们要了解这个转换规则:
全等(===)和相等(==)最大的不一样之处是它不会对操做数进行强制转换。
ECMAScript中全部函数的参数都是按值传递的。也就是说,把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另外一个变量同样。基本类型值的传递如同基本类型变量的复制同样,而引用类型值的传递,则如同引用类型变量的复制同样。有很多开发者在这一点上可能会感到困惑,由于访问变量有按值和按引用两种方式,而参数只能按值传递。
在向参数传递基本类型的值时,被传递的值会被复制给一个局部变量(即命名参数,或者用ECMAScript的概念来讲,就是arguments对象中的一个元素)。在向参数传递引用类型的值时,会把这个值在内存中的地址复制给一个局部变量,所以这个局部变量的变化会反映在函数的外部。
先看一个基本类型值传递的例子:
function addTen(num) { num += 10; return num; } var count = 10; var result = addTen(count); console.log(count); // => 10 console.log(result); // => 20
上边的代码中,addTen函数并无改变count的值,按照上边的理论,咱们能够这么看addTen函数:
function addTen(num) { num = count; // 当调用了函数的时候,函数内部作了这一个操做 num += 10; return num; }
再来看看引用类型的值传递的例子:
function setName(obj) { obj = person; // 当调用了函数的时候,函数内部作了这一个操做 obj.name = "James"; obj = new Object(); obj.name = "Bond"; } var person = new Object(); setName(person); console.log(person.name); // => "James"
在函数内部,一样为参数赋值了一个引用类型值的复制数据。在函数内部,obj就是一个指针,当给他从新赋值一个新的对象的时候,他指向了另外一个数据,所以,即便给它的name赋值,也不会影响函数外部的对象的值,说白了,仍是内存地址的问题。
数组的length
属性颇有特色------他不是只读的。所以经过设置这个属性,能够从数组的末尾移除项或向数组中添加新项:
var colors = ["red", "blue", "green"]; colors.length = 2; alert(colors[2]); // => undefined colors.length = 4;
上边的代码给colors设置了length后,最后边的那个数据就变成了undefined,说明经过设置length可以修改数组的值,若是这个值大于数组元素的个数,那么多出来的元素就赋值为undefined。
数组的sort()方法会调用每一个数组项的toString()转型防范,而后比较获得的字符串,以肯定如何排序。即便数组中的每一项都是数值,sort()方法比较的也是字符串。 看个例子:
var array = [1, 4, 5, 10, 15]; array = array.sort(); console.log(array.toString()); // => 1,10,15,4,5
可见,即便例子中值的顺序没有问题,但sort()方法也会根据测试字符串的结果改变原来的顺序。
数组有5种迭代方法:
var numbers = ["1", "2", "3", "4", "5", "6"]; // every() 检测数组中的每一项是否都大于2 var everyResult = numbers.every(function (item, index, array) { return item > 2; }); console.log(everyResult); // => false // some() 检测数组中是否至少有一项大于2 var someResult = numbers.some(function (item, index, array) { return item > 2; }); console.log(someResult); // => true // filter() 过滤数组中大于2的值 var filterResult = numbers.filter(function (item, index, array) { return item > 2; }); console.log(filterResult); // => ["3", "4", "5", "6"] // map() 加工数组中的数据 var maoResult = numbers.map(function (item, index, array) { return item * 2; }); console.log(maoResult); // => [2, 4, 6, 8, 10, 12]
使用函数做为返回值是一件很奇妙的事情,咱们使用一个例子来看看:
function createComparisonFunction(propertyName) { return function (object1, object2) { var value1 = object1[propertyName]; var value2 = object2[propertyName]; if (value1 < value2) { return -1; } else if (value1 > value2) { return 1; } else { return 0; } } } var data = [{ name: "zhangsan", age: 20 }, { name: "lisi", age: 30 }]; data.sort(createComparisonFunction("name")); console.log(data[0]); // => {name: "lisi", age: 30} data.sort(createComparisonFunction("age")); console.log(data[0]); // => {name: "zhangsan", age: 20}
在函数内部,有两个特殊的对象:arguments和this。其中,arguments是一个类数组对象,包含着传入函数中的全部参数。虽然arguments的主要用途是保存函数参数,**但这个对象还有一个名叫callee的属性,该属性是一个指针,指向拥有这个arguments对象的函数,咱们看下边这个很是经典的阶乘函数:
function factorial(num) { if (num < 1) { return 1; } else { return num * factorial(num - 1); } } console.log(factorial(5)); // => 120
定义阶乘函数通常都要用到递归算法,如上边的代码所示,在函数有名字,并且名字之后都不会变的的状况下,这样定义没问题。但问题是这个函数的执行与函数名factorial仅仅耦合在了一块儿。为了消除这种紧密耦合的现象,能够像下面这样是哟很难过arguments.callee:
function factorial(num) { if (num < 1) { return 1; } else { return num * arguments.callee(num - 1); } } console.log(factorial(5)); // => 120
咱们修改factorial函数的实现后:
const anotherFactorial = factorial; factorial = function () { return 0; } console.log(anotherFactorial(5)); // => 120 console.log(factorial(5)); // => 0
使用call()或apply()来扩充做用域的最大好处,就是对象不须要与方法有任何耦合关系。
ECMAScript中有两种属性:数据属性和访问器属性。
数据属性包含一个数据值的位置。在这个位置能够读取和写入值。数据属性有4个描述其行为的特性:
咱们先看几个例子:
const person = { }; Object.defineProperty(person, "name", { writable: false, value: "James" }); console.log(person.name); // => James person.name = "Bond"; console.log(person.name); // => James
上边的代码设置了person中的属性name的特性,把它的writable设置为false,所以当咱们重写它的name属性的时候是不起做用的,使用value能够给属性赋值。咱们再看一个例子:
const person = { }; Object.defineProperty(person, "name", { configurable: false, value: "James" }); console.log(person.name); // => James delete person.name; console.log(person.name); // => James
当咱们把confugurable设置为false的时候,就把name属性的可配置性给锁死了,一旦把confugurable设为false,后续的再次对这个属性设置特性的时候就会出错。下边的代码会报错:
Object.defineProperty(person, "name", { writable: true, value: "JJJJJ" }); console.log(person.name);
访问器属性不含数据值,但能够经过set或get方法来设置或获取值,就像制定了一套这样的规则。我跟喜欢称这个特性为计算属性。
const book = { _year: 2004, edition: 1 }; Object.defineProperty(book, "year", { get: function () { return this._year; }, set: function (newValue) { if (newValue > 2004) { this._year = newValue; this.edition += newValue - 2004; } } }); book.year = 2005; console.log(book.edition);
在这个例子中。_year很想一个私有变量,咱们经过set,get方法来写了一个year属性,固然也可使用这种方式来控制属性是否只读或只写特性。
有一点值得注意,上边说的这些内容算是为对象建立属性的方法,咱们也能够采用person.name这种方式建立属性,只不事后边这种建立的方式给里边的特性赋了默认的值。
在JavaScript中Object的总结这篇文章中,我介绍了多种建立对象的方法:
核心思想是经过函数来建立对象,函数会返回一个根据参数建立的新的对象,这个方法虽然解决了建立多个类似对象的问题,但没有解决对象识别的问题,由于在函数内容,知识把参数赋值给了任何对象的属性
构造函数的使用方法我就不提了,我只说几点须要注意的地方,构造函数的第一个字母要大写,内部使用this来指定属性和方法。在建立对象的时候要加上new关键字。
其实构造函数的本质也是一个函数,若是在调用的时候不加关键字new,那么它内部的属性将会建立为全局变量的属性。**任何加上new关键字的函数都会变成构造函数,而构造函数的本质是:
var a = {}; a.__proto__ = F.prototype; F.call(a);
构造函数可以让咱们经过相似.constructor或instanceof来判断对象的类型,但它的缺点是会为相同的属性或方法建立重复的值,咱们都知道在JavaScript中函数也是对象,这种返回建立统一对象的过程,确定给性能带来了很大的挑战,所以这种模式还须要升级。
原型模式是很是重要的一个概念,咱们会使用很长的篇幅来介绍这方面的内容。
首先咱们应该明白函数名字本质上是一个指向函数对象的指针,所以他能表示这个函数对象,在JavaScript中每一个函数**内部都有一个prototype属性,这个属性是一个指针,指向一个对象,而这个对象的用因而包含属性和方法。所以咱们有这样的启发,若是我给构造函数的prototype赋值属性和方法,那么我在使用构造函数建立对象的时候,是否是就能够继承这些共有的属性呢? 答案是确定的:
function Person() { } Person.prototype.name = "James"; Person.prototype.sayName = function () { console.log(this.name); }; const person1 = new Person(); person1.sayName(); // => James const person2 = new Person(); person2.sayName(); // => James console.log(person1.name == person2.name); // => true
不管何时,只要建立了一个新函数,就会根据一组特定的规则为该函数建立一个prototype属性。这个属性指向函数的原型西乡,在默认状况下,这个prototype又会自动获取一个叫作constructor的属性,这个属性包含一个指向prototype属性所在函数的指针,能够说这是一个回路。
那么建立一个实例的过程是怎么样的呢?
当咱们用构造函数建立一个实例后,该实例内部也会有一个指针指向构造函数的原型对象,通常状况下,这个指针的名字并非prototype,咱们必须记住一点,prototype只是函数内部的一个属性。大部分浏览器的这个指针是__proto__
。咱们看一张图:
上图很好的展现了构造函数和实例对象之间原型的关系。咱们在这里就不一一说明了。虽然咱们经过__proto__
能访问到原型对象,但这绝对不是推荐作法。咱们能够经过isPrototypeOf()
方法来肯定对象之间是否存在这种关系:
console.log(Person.prototype.isPrototypeOf(person1)); // => true
上边的代码很好的演示了这一说法,实例对象person1的原型就是构造函数Person的prototype。,还有一个方法是获取原型对象getPrototypeOf()
:
console.log(Object.getPrototypeOf(person1) == Person.prototype); // => true
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具备给定名字的属性。搜索首先从对象实例自己开始。若是在实例中找到了具备给定名字的属性,则返回该属性,若是没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具备给定名字的属性,若是在原型对象中找到这个属性,则返回该属性。具体的例子咱们就不演示了。
值得注意的是,当给对象的属性赋值时,若是属性的名称与原型对象的属性名称相同,对象内部会建立这个属性,原型中的属性保持不变。,咱们能够这么认为,原型对象大部分时候只提供读取功能,它的目的是共享数据。但若是给引用类型的属性赋值的时候会有不一样的状况,好比修改原型的对象,数组就会致使原型的数据遭到修改。这个在JavaScript中Object的总结这篇文章中我已经详细的给出了解释。
经过上边的距离,咱们大概明白了对象属性与原型之间的关系,那么如今就引出了一个问题。如何区分某个属性是来自对象自己仍是原型呢?为了解决这个问题,咱们引出in
操做符。
有两种方式使用in操做符:单独使用和在for-in循环中使用。在单独使用时,in操做符会在经过对象可以访问给定属性时返回true,不管该属性存在于实例中仍是原型中。咱们看下边这个例子:
function Person() { } Person.prototype.name = "James"; Person.prototype.sayName = function () { console.log(this.name); }; const person1 = new Person(); console.log(person1.hasOwnProperty("name")); // => false console.log("name" in person1); // => true
hasOwnProperty()
方法可以判断对象自己是否存在某个属性,而in可以判断对象是否可以访问某个属性,结合这两种方法,咱们就能判断某个属性的来源,咱们举个简单的例子:
function hasPrototypeProperty(object, name) { return (!object.hasOwnProperty(name)) && (name in object); } console.log(hasPrototypeProperty(person1, "name")); // => true
for-in能够遍历对象中的属性,**可是要依赖属性中的enumerable
这个特性的值,若是这个值为false,那么就没法遍历到属性,跟for-in很类似的方式是Object.keys()
他返回一个字符串数组,若是要想遍历出对象的属性,忽略enumerable
的影响,可使用Object.getOwnPropertyNames()
这个方法,下边是一个简单的例子:
function Person() { } Person.prototype.name = "James"; Person.prototype.sayName = function () { console.log(this.name); }; const person1 = new Person(); console.log(hasPrototypeProperty(person1, "name")); // => true Object.defineProperty(person1, "age", { enumerable: false }); for (const pro in person1) { console.log(pro); } const keys = Object.keys(person1); console.log(keys); const keys1 = Object.getOwnPropertyNames(person1); console.log(keys1);
在上边的内容中,咱们已经明白,JavaScript中寻找属性或方法是经过搜索来实现的,所以咱们能够动态的为原型添加属性和方法。这一方面没什么好说的,但有一点值得注意,若是把原型修改成另外一个对象,就会出现问题。,仍是先看一个实例:
function Person() { } const person = new Person(); Person.prototype = { constructor: Person, name: "James", sayName: function () { console.log(this.name); } }; console.log(person.sayName()); // 会报错
上边的代码会报错,根本缘由是对象的原型对象指向了原型,而不是指向了构造函数,这就比如这样的代码:
var person1 = person; var person2 = person; person1 = son;
上边的代码中,person1换了一个对象,可是person2依然指向了person。用下边这个图开看更直接
这一小节是一个很重要的小结,咱们慢慢的增长了对JavaScript语言的理解。原型模式的重要性不只体如今建立自定义类型方面,就连全部原生的引用类型,都是采用这种模式建立的。全部原生引用类型(Object,Array,String等等)都在器构造函数的原型上定义了方法。
alert(typeof Array.prototype.sort); // => function
所以咱们就经过这种手段为原生的引用类型扩展更多的属性和方法。
String.prototype.startsWith = function (text) { return this,indexOf(text) == 0; }
这种方式很是像面向对象语言中的分类,分类使用好了,可以增长程序的可读性,但在JavaScript中,不建议用这种方法为原生对象作扩展。由于这么作的后果是可能让程序失控。