美文网首页JavaScript
JavaScript高级编程小结

JavaScript高级编程小结

作者: 老马的春天 | 来源:发表于2017-08-10 14:49 被阅读53次

    Undefined

    对未初始化的变量执行typeof操作符会返回undefined值,而对未声明的变量执行typeof操作符同样也会返回undefined

    var message;
    console.log(typeof message); // => undefined
    console.log(typeof gaga); // => undefined
    

    Boolean

    各种类型转换成Boolean的规则

    数据类型 转成true的值 转成false的值
    Boolean true false
    String 任何非空字符串 ""空字符串
    Number 任何非零数字值(包括无穷大) 0和NaN
    Object 任何对象 null
    Undefined n/a undefined

    Number

    Number类型应该是ECMAScript中最令人关注的数据类型了。

    除了以十进制表示外,整数还可以通过八进制或十六进制表示,其中,八进制字面值的第一位必须是0,然后是八进制数字序列(0 ~ 7)。如果字面值中的数值超出了范围,那么前导0将被忽略,后面的数值将被当作十进制数值解析

    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会不失时机的将浮点数值转换为整数值。

    永远不要测试某个特定的浮点数值:

    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的缩写,它有两个非同寻常的特点:

    • 任何涉及NaN的操作都会返回NaN
    • NaN与任何值都不相等,包括NaN本身

    isNan()函数的原理是:在接受一个值后,会尝试将这个值转换成数值,成功就返回false,失败则返回true。

    有3个函数可以把非数值转换成数值:Number(),parseInt()和parseFloat()。Number函数可以用于任何数据类型,另外两个则专门用于把字符串转换成数值。

    Number()函数的转换规则如下:

    • 如果是Boolean值,true和false将分别被转换为1和0

    • 如果是数字值,只是简单的传入和返回

    • 如果是null值,返回0

    • 如果是undefined,返回NaN

    • 如果是字符串,遵循下列规则:

      • 如果字符串中只包含数字(包括前面带正好或负号的情况),则将其转换为十进制数值,即“1”变成1,“123”会变成123,而“011”会变成11(注意:前导的0被忽略了)
      • 如果字符串中包含有效的浮点格式,如“1.1”,则将其转换为对应的浮点数值(同样会忽略前导0)
      • 如果字符串中包含有效的十六进制格式,例如“0xf”,则将其转换为相同大小的十进制整数值
      • 如果字符串是空的(不包含任何字符),则将其转换为0
      • 如果字符串中包含除上述格式之外的字符,则将其转换为NaN
    • 如果是对象,则调用对象的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

    String()方法内部转换规则:

    • 如果值有toString()方法,则调用该方法并返回相应的结果,toString()方法不能处理null和undefined的情况
    • 如果值是null,则返回“null”
    • 如果值是undefined,则返回“undefined”
    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"
    

    逻辑与

    逻辑与(&&)可以应用于任何类型的操作数,而不仅仅是布尔值。在有一个操作数不是布尔值的情况下,逻辑与操作就不一定返回布尔值,它遵循下列规则:

    • 如果第一个操作数是对象,则返回第二个操作数
    • 如果第二个操作数是对象,则只有在第一个操作数的求值结果为true的情况下才会返回该对象
    • 如果两个擦作数都是对象,则返回第二个操作数
    • 如果有一个操作数是null,则返回null
    • 如果有一个操作数是NaN,则返回NaN
    • 如果有一个操作数是undefined,则返回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,说明在条件语句中可以使用&&,但是需要明白返回值的问题。

    相等操作符

    相等(==)操作符在进行比较之前会对操作数进行转换,我们要了解这个转换规则:

    • 如果有一个操作数是布尔值,则在比较相等性之前先将其转换为数值,false转换为0,而true转换为1
    • 如果一个操作数是字符串,另一个操作数是数值,在比较相等性之前先将字符串转换为数值
    • 如果一个操作数是对象,另一个操作数不是,则调用对象的valueOf()方法,用得到的基本类型值按照前面的规则进行比较
    • null和undefined是相等的
    • 要比较相等性之前,不能将null和undefined转换成其他任何值
    • 如果有一个操作数是NaN,则相等操作符返回false,而不相等操作符返回true。重要提示:即使两个操作数都是NaN,相等操作符也返回false
    • 如果两个操作数都是对象,则比较他们是不是同一个对象。如果两个操作数都指向同一对象,则相等操作符返回true,否则,返回false

    全等(===)和相等(==)最大的不同之处是它不会对操作数进行强制转换。

    参数传递

    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赋值,也不会影响函数外部的对象的值,说白了,还是内存地址的问题。

    Array

    数组的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种迭代方法:

    • every(): 对数组中的每一项运行给定函数,如果该函数对每一项都返回true,则返回true,就跟它的名字一样,测试数组中是否每一项都符合函数的条件
    • some(): 对数组中的每一项运行给定函数,如果该函数对任一项返回true,则返回true,同样,就跟它的名字一样,测试数组中是否存在至少一项是符合函数的条件
    • filter(): 对数组中的每一项运行给定的函数,返回该函数会返回true的项组成的数组, 主要用于过滤数据
    • forEach(): 对数组中华的每一项运行给定函数,这个方法没有返回值,就是遍历方法
    • map(): 对数组中的每一项运行给定函数,返回每次函数调用的结果组成的数组,这个算是对数组中的项进行加工
    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

    使用函数作为返回值是一件很奇妙的事情,我们使用一个例子来看看:

    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中有两种属性:数据属性和访问器属性。

    1.数据属性

    数据属性包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有4个描述其行为的特性:

    • configurable
    • enumerable
    • writable
    • value

    我们先看几个例子:

    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);
    

    2.访问器属性

    访问器属性不含数据值,但可以通过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
    

    1.理解原型对象

    无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个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的总结这篇文章中我已经详细的给出了解释

    2.原型与in操作符

    通过上边的距离,我们大概明白了对象属性与原型之间的关系,那么现在就引出了一个问题。如何区分某个属性是来自对象本身还是原型呢?为了解决这个问题,我们引出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);
    

    3.原型的动态性

    在上边的内容中,我们已经明白,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。用下边这个图开看更直接

    4.原生对象的原型

    这一小节是一个很重要的小结,我们慢慢的增加了对JavaScript语言的理解。原型模式的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型,都是采用这种模式创建的。所有原生引用类型(Object,Array,String等等)都在器构造函数的原型上定义了方法。

    alert(typeof Array.prototype.sort); // => function
    

    因此我们就通过这种手段为原生的引用类型扩展更多的属性和方法。

    String.prototype.startsWith = function (text) {
        return this,indexOf(text) == 0;
    }
    

    这种方式非常像面向对象语言中的分类,分类使用好了,能够增加程序的可读性,但在JavaScript中,不建议用这种方法为原生对象做扩展。因为这么做的后果是可能让程序失控。

    相关文章

      网友评论

        本文标题:JavaScript高级编程小结

        本文链接:https://www.haomeiwen.com/subject/pryhrxtx.html