JS高级程序笔记

248 阅读9分钟

JS高级程序笔记(一)——Js中的valueOf函数

在类型转换中,经常用到方法`valueOf()`和`toString()`,所有对象(包括基本包装类型)都拥有这两个方法。这里我记录一下`valueOf()`方法的重载。</br>`valueOf()`方法会将对象转换为基本类型,如果无法转换为基本类型,则返回原对象。

昨天在看《JavaScript高级程序设计(第三版)》时发现,第一次接触js函数的重写

        var s1 = "2";
        var s2 = "z";
        var b = false;
        var f = 1.1;
        var o = { 
            valueOf: function() {
                return -1;
            },

        };
        var m = "mystring";
        var m = {
            toString:function () {
                return "3212312";
            }
        }
        
        s1++;   //value becomes numeric 3
        s2++;   //value becomes NaN
        b++;    //value becomes numeric 1
        f--;    //value becomes 0.10000000000000009
        o--;    //value becomes numeric -2  

        alert(s1);   //3
        alert(s2);  //NaN
        alert(b);   //1
        alert(f);   //0.10000000000000009
        alert(o);   //-2
        alert(m);   //3212312

这里可以看出,对变量o自带的valueOf函数进行了重写,而原有的函数定义是

function 函数名(参数,···){
    ······
}

就目前而言好像我只接触到了valueOf方法的重写以及toString方法的重写。

再继续看后面的章节,我发现,

    toString:function () {
        return "3212312";
    }

这样其实是字面量语法的写法,将原来的toString方法覆盖了, 入一下这段

    var person = new Object();
    person.name = "Nicholas";
    person.age = 29;
    person.job = "Software Engineer";
    person.sayName = function(){
        alert(this.name);
    };
    
    person.sayName();

而字面量语法创建一个自定义对象的方法如下,一看便明白了

    var person = {
        name : "Nicholas",
        age : "29",
        job : "Software Enginner",
        sayName : function(){
            alert(this.name);
        }
    }

后续如果有补充会继续添加内容

JS高级程序笔记(二)——JavaScript执行环境与作用域的概念

JavaScript中没有块级作用域经常会导致理解上的困惑,在其他类C的语言中,由花括号封闭的代码都有自己的作用域,因而支持根据条件来定义变量。但是在JavaScript中下面的代码并不会得到想象中的结果:

if(true){
    var a = 'balck';
}
console.log(a);//black

上文在if语句中定义了一个变量a,如果是在C、C++或Java语言中,a会在if语句执行完毕后被销毁。但在JS中,if语句里的变量声明会将变量添加到当前的执行环境,而JS中的执行环境通常只有两种——全局执行环境和局部执行环境,局部执行环境一般就是函数,所以这里的a是定义在全局执行环境中,也就是作为window对象的属性存在的,如下图:

所以根据作用域链机制会正常打印black。 了解到在ES6中加上了块级作用域的概念,现在将《JS高级》看完后继续研究ES6。

JS高级程序笔记(三)——indexOf 和 lastIndexOf 是什么?

indexOf 和 lastIndexOf 都是索引文件

indexOf 是查某个指定的字符串在字符串首次出现的位置(索引值)(从左往右)

lastIndexOf 是查某个指定的字符串在字符串最后一次出现的位置(索引值)(从右往左)

    var a = "eddiepeng";
    
    alert(a.indexOf("i"));  // 3
    
    alert(a.lastIndexOf("i"));  // 3

可见上面的例子中,indexOf和lastIndexOf输出的结果都是3,看不出它们的差别。

接下来再看一个例子

    var a = "eddiepeng";

    alert(a.indexOf("e"));      // 0

    alert(a.lastIndexOf("e"));      // 6

在这个例子中发现,字母e出现了3次,indexOf查询的结果是字母e第一次出现的位置0,而lastIndexOf查询出的结果是字母e最后一次出现的位置6。

两次的查询结果明显不一致。但是中间又还有一个字母e的位置位于4。

由此就可以得出结论:

  • 当数组(字符串)中所要查询的数(字符串/字符)在字符串(数组)中只出现一次的时候 二者返回的索引值相同

  • 当数组(字符串)中所要查询的数(字符串/字符)在字符串(数组)中出现两次及以上的时候

  • indexOf 返回的是 valuesearch 第一次在数组(字符串)出现的位置(从左往右)

  • lastIndexOf 返回的是 valuesearch 最后一次在数组(字符串)出现的位置(从右往左)

最后再做一下说明:

lastIndexOf() 方法定义:可返回一个指定的元素在数组中最后出现的位置,从该字符串的后面向前查找。

lastIndexOf()方法虽然是从后往前搜索,但返回的位置是从前开始数的。

JS高级程序笔记(四)—— Function类型的相关知识(将持续补充···)

1、函数定义

函数名实际上就是一个指向函数对象的指针,不会与某个函数绑定。

函数的定义方式主要有两种:

1、通过函数声明语法定义

    function sun(num1, num2){
    	return num1 + num2;
    }

通过这种方式定义的函数,有一个中要的特征就是函数声明提升,意思就是在执行代码之前会先读取函数声明,这就以为着可以把函数声明放在调用它的语句的后面。

    sayHi();
    function sayHi(){
        alert("Hi!");
    }

这个例子中不会抛出错误,因为在代码执行之前会先读取函数声明。

而下面这种方式就不能这样了。

2、通过函数表达式定义函数

    var sum =function(num1, num2){
        return num1 +  num2;
    };    //(注意结尾还有一个分号,就像声明其他变量一样)

这种情况下创建的函数叫做匿名函数,因为function关键字后面没有标识符。匿名函数的name属性是空字符串

    sayHi();        //出现错误:函数不存在
    var sayHi = function(){
        alert("Hi!");
    }

上面简单介绍了函数定义,下面就根据函数名与匿名函数展开一定的讨论:

    //这个function(){}就是匿名函数
    $(function(){		
        //执行代码
    });
    
    //这个function(){}也是匿名函数
    //显然这也是通过函数表达式定义函数
    var fun1 = function(){		
        //执行代码
    }
     
    $("body").click(function(){
        //jquery最基本的绑定click事件的一种
    });
    //上面这里用的就是匿名函数,可以用下面的代码代替
    function clickHandle(){
         
    }
    $("body").click(clickHandle);
 

但是好像并不是所有的匿名函数都能被取代,暂时我还不知道下面这个匿名函数可以用哪个命名函数来取代它的作用


2、JavaScript数组的sort方法排序原理详解,以及对数组对象的某个属性进行排序的方法说明

2.1 sort方法排序原理

定义与用法:

sort() 方法用于对数组的元素进行排序。

语法:

arrayObject.sort(sortby)

注意:sortby必须是函数,规定排序顺序。可选参数

返回值:

对数组的引用。请注意,数组在原数组上进行排序,不生成副本。

说明及原理:

如果调用该方法时没有使用参数,将按字母顺序对数组中的元素进行排序,说得更精确点,是按照字符编码ASCII编码的顺序进行排序。要实现这一点,首先应把数组的元素都转换成字符串(如有必要),以便进行比较。

如果想按照其他标准进行排序,就需要提供比较函数,该函数要比较两个值,然后返回一个用于说明这两个值的相对顺序的数字。比较函数应该具有两个参数 a 和 b,其返回值如下:

若 a 小于 b,在排序后的数组中 a 应该出现在 b 之前,则返回一个小于 0 的值。 若 a 等于 b,则返回 0。 若 a 大于 b,则返回一个大于 0 的值。

实例:

例子一:在本例中,我们将创建一个数组,并按字母顺序进行排序

var arr = new Array(6);
arr[0] = "George";
arr[1] = "John";
arr[2] = "Thomas";
arr[3] = "James";
arr[4] = "Adrew";
arr[5] = "Martin";

console.log(arr + " ");    // George,John,Thomas,James,Adrew,Martin

arr.sort();

console.log(arr + " ");    // Adrew,George,James,John,Martin,Thomas

例子二:在本例中,我们将创建一个数字数组,并查看经过sort后的排列顺序

var arr = new Array(6);
arr[0] = "8";
arr[1] = "16";
arr[2] = "50";
arr[3] = "6";
arr[4] = "100";
arr[5] = "1";

console.log(arr + " ");    // 8,16,50,6,100,1

arr.sort();

console.log(arr + " ");    // 1,100,16,50,6,8

可以注意到,上面的代码没有按照数值的大小对数字进行排序,就是因为依照每个数的第一位的ASCII码进行了排序,要实现真正的排序,就必须使用一个排序函数:

以下再看一个代码:

var array = [12,10,7,5,2];

array.sort(function(a,b){

	var arr = a-b;
	console.log(a+'-'+ b + '=' +arr);  //这里对a-b进行了输出
	return arr;

});

console.log(array + "");

/*
* 10-12=-2
* 7-10=-3
* 5-7=-2
* 2-5=-3
* 2,5,7,10,12
* 
* */

我们会发现该方法进行了4次运算。(可能在不同的浏览器中会进行不同次数的运算)

这其实应用到了冒泡排序,我们一步步来分析他的排序过程

第一步: 10-12=-2; 这里return的值是-2,为负数。说明10小于12,所以10被提前了,该数组变为[10,12,7,5,2]

第二步 : 7-10=-3; 这里return的值是-3,为负数。这里说明7小于10,7又被提前到了10的前面。 这时该数组变为[7,10,12,5,2]

第三步:5-7=-2; 这里return的值是-2,为负数。这里说明5小于7,5又被提前到了7的前面。 这时该数组变为[5,7,10,12,2]

第四步:2-5=-3; 这里return的值是-3,为负数。这里说明2小于5,2又被提前到了5的前面。 这时该数组变为[2,5,7,10,12]

可见每一次都有最前面的那个数进行比较,大家可以对不同的数组进行试验,这里就不一一展示了。

2.2 对数组对象的某个属性进行排序的方法说明

下面来看这一段代码:

var arr = [
    {name:'a',age:9},
    {name:'b',age:12},
    {name:'c',age:8}
];

function compare(property){
    return function(a,b){
        var value1 = a[property];
        var value2 = b[property];
        return value1 - value2;
    };
}

console.log(arr.sort(compare('age')));

输出结果如图:

这里还不是很清楚要如何用命名函数来取代这个匿名函数;后续知道会继续补充······

2.3最后再看看自运行函数

//匿名函数
(function(){

})();
//上面的自运行函数(闭包操作)可以改成下面这样
function.tempFun(){

}
tempFun();

3、前面提到了匿名函数的概念,这里要提出一个新的黑洞【闭包】

3.1 作用域与词法作用域

在学习闭包之前,必须先掌握作用域和词法作用域的概念,这些可以在我的收藏夹里的与闭包相关知识里面看其他大佬的文章,可以快速了解。这里我再对词法作用域的概念做一个例子上的说明。

    var s = 3;
    
    function a() {            //这里是函数a()的被定义的作用域
        console.log(s);
    }
    
    function b() {         
        var s = 6;
        a();
    }
    
    b(); // 3,不是6

结合上述例子说明一下几点:

所谓的词法作用域就是在你写代码时将变量和块作用域写在哪里来决定,也就是词法作用域是静态的作用域,在你书写代码时就确定了。

JS采用了词法作用域(静态作用域),JS的函数运行在他们执行的作用域中,而不是他们被被定义的作用域

(这一点在ES6的箭头函数中又不同,因为箭头函数中的this是静态的,它的this始终指向它被定义时所在的作用域)——看了《ES6入门教程后的补充》

这也就是为什么打印出来的不是6而是3的原因,js的函数运行是在执行的作用域中,而不是他们被被定义的作用域。

再看下面这个例子,结合起来看就会更加清楚词法作用域的概念。

    var s1 = 3;
    
    function a() {
        console.log(s1);
    }
    
    function b1() {
        var s1 = 6;
        
        // 这里就是函数a1被定义的作用域,可以发现这个作用域在函数b1的作用域里面
        // 而且a1被定义的作用域正好和a1的执行作用域都在b1被定义的作用域里面
        (function a1() {             
            console.log(s1);
        })();                   //在之前提到过的自运行,可以自行再查资料了解构造
    }
    
    b1(); // 这里打印的就是6了

3.2 下面就解释一下《高级》中闭包与变量一节中的案例

案例如下:

    function create(){
        var result =new Array();
        for(var i=0;i<10;i++){
            result[i]=function(){
            return i;
            }
        }
        return result;
    }
    var funcs = create();
    
    //every function outputs 10
    for (var i=0; i < funcs.length; i++){
        document.write(funcs[i]() + "<br />");
    }

因为这一节看了很多有关于闭包的文章,最后在这个例子上还是有点云里雾里。

知道看到了这个说法,才感觉茅塞顿开。以下来说说我的看法。

上面的案例中会在页面输出10个10,而我们第一次接触肯定会觉得是输出0~10。

create函数内部的闭包可以通过作用域访问到外部函数create中的变量i,当函数create执行完以后i最后的值是10。

而当执行 document.write(funcs[i]() + "<br />");的时候 本质上是形成了下面这种情况

    result = [function () {
        return i;
    }, function () {
        return i;
    }, .....function()
    {
        return i;
    }
    ]
    
    //下面这个形式可以看得更清楚一点,
    for 循环执行完后的结果:

    result[0] = function() {return i;};
    result[1] = function() {return i;};
    ~~~
    result[9] = function() {return i;};

我们可以执行下面这段,在每次循环的尾部添加一句console.log(result[i]);即在如下位置:

    for(var i = 0; i < 10; i++) {
    	result[i] = function() {
    		return i;
    	}
    	console.log(result[i]);
    }

再看看打印出来是不是这么一回事:

    "function () {
        return i;
    }"	
    "function () {
        return i;
    }"
    "function () {
        return i;
    }"
    "function () {
        return i;
    }"	
    "function () {
        return i;
    }"	
    "function () {
        return i;
    }"
    "function () {
        return i;
    }"
    "function () {
        return i;
    }"
    "function () {
        return i;
    "function () {
        return i;
    }"

chorme浏览器在控制台的输出如下:

数组中10个匿名函数的结果,因为是引用类型,只能访问到create执行以后i的值。因此是10

*补充说明1:

在看《锋利的jQuery(第二版)》一书中我看到作者是这样解释闭包的也是ECMAScript对闭包的解释:允许使用内部函数(即函数定义和函数表达式位于另一个函数体内),而且,这些内部函数可以访问他们所在的外部函数中声明的所有局部变量、参数和声明的其他内部函数,当其中一个这样的内部函数再包含他们的外部函数之外被调用时,就会形成闭包

JS高级程序笔记(五)——正则表达式与RegExp与支持正则表达式的 String 对象的方法的记录

返向引用

假如我们有这样一个需求:把日期'2015-12-25'替换成'12/25/2015',如果是你,你现在会怎么做呢?

你可能会这样写:

	var reg = /\d{4}-\d{2}-\d{2}/g;
	var text = '2015-12-25'
	var result = text.replace(reg,'12/25/2015');
	console.log(result);//12/25/2015

但是上面这样的写法,你只能够匹配到'2015-12-25'了,不能再匹配别的日期了,'2015-12-25'是会变的,这样就达不到需求了。

这时候,正则的反向引用就可以取到作用了。表达式在匹配时,表达式引擎会将小括号 "( )" 包含的表达式所匹配到的字符串记录(分组捕获)下来。在获取匹配结果的时候,小括号包含的表达式所匹配到的字符串可以单独获取。

在js中正则匹配成功的字符串可以用1表示第一次匹配成功,1表示第一次匹配成功,3表示第三次匹配成功的字符,以此类推至$99)。于是,上面的例子就可以这样写了:

	var reg = /(\d{4})-(\d{2})-(\d{2})/g;
	var text = '2015-12-25'
	var result = text.replace(reg,'$2/$3/$1');
	console.log(result);//12/25/2015

忽略分组

在上面的反向引用中,我们默认是根据'()'全部捕获记录为1 1~99的,倘若我们想忽略某个捕获要怎么办呢?

不希望捕获某些分组,只需要在分组内加上'?:'就可以了。

	var reg = /(?:Byron)(\d{4})-(\d{2})-(\d{2})/g;
	var text = 'Byron2016-12-05'
	var result = text.replace(reg,'$2/$3/$1');
	console.log(result);//12/05/2016

前瞻

正则表达式从文本头部向尾部开始解析,文本尾部方向称为“前”,前瞻就是在正则表达式匹配到规则的时候,向前检查是否符合断言,后顾/后瞻方向相反。

符合和不符合断言称为肯定/正向匹配和否定/负向匹配。

上面是前瞻的概念,是不是看完有点晕?我看完也有点晕...我来解释一下:假如你需要匹配一个名字叫“张三”的人,以前我们可能是从一堆人中找出名字是“张三”的揪出来就行了,但是前瞻就是要求你,揪出的人的名字叫“张三”还不够,还必须“张三”的父亲必须叫“张二”或者其它特定条件,这样就是前瞻了,类似的,后瞻就是名字是张三还不够,儿子还必须叫“小张”等。

是不是有点明白了?不过需要注意的是,在javascript中是不支持后顾/后瞻的,所以我们也不需要关心了。(这里纠正一下,在 ES2018(ES9) 中已经支持后瞻和命名分组了)

至于符合/不符合断言,可以解释为:比如匹配要求名字叫“张三”,并且呢他父亲不叫“张二”,对于符合的我们就叫正向/肯定匹配,不符合的就叫负向/否定匹配。

我们再用表格说明一下:

名称正则含义
正向前瞻exp(?=assert)我们匹配符合了exp部分的表达式,然后还不算完,必须也匹配断言部分('()'内部,'='后面的正则),才算成功
负向前瞻exp(?!assert)我们匹配符合了exp部分的表达式,然后还不算完,必须也匹配断言部分('()'内部,'!'后面的正则),才算成功
负向前瞻exp(?<=assert)javascript不支持
负向前瞻exp(?<!assert)javascript不支持

现在是不是清楚了?如果还不够明白,没事,我们再举个栗子:

	var reg = /\w(?=\d)/g;
	var text = 'a2*3';
	var result = text.replace(reg,'X');
	console.log(result);//X2*3

需要注意,我们断言里面内容只是作为匹配的条件之一,也是必须的条件,但是匹配的本质只匹配"()"前面的正则,所以上面的结果为:'X23',而不是'X3'。如果要匹配结果为后者,我们按原来的写法就行了var reg = /\w\d/g;,不是吗?

原作者以及举了一个例子,我再举一个例子加强理解:

    var reg = /\w(?=\s)/g;
    var text = 'a2*3';
    var result = text.replace(reg,'X');
    console.log(result);//a2*3

这个例子中,正则表达式要求匹配一个word单词字符(字母、数字、下划线)和一个空白符组成的字符串,上一个例子中将"()"中的正则表达式作为一个附加条件,但是并不代表它可有可无,它是必须要满足的,但是在后面进行匹配替换操作时没有替换"()"中正则匹配到的内容,这就是前瞻,仅仅将其作为一个必要条件来查找,形象一点就是我需要你女朋友同意你才能去网吧,当时并不代表你女朋友也要去网吧,只是一个必要条件,就像下面这个例子一样,女朋友不同意,你就不能去。如果你需要女朋友和你一起去,那就像原作者说的那样写成var reg = /\w\d/g;,不是吗?

String对象的函数支持正则的有4个,我们只说其中的2个

1、match函数

用法:stringObject.match(searchvalue | regexp),这里我们只说regexp模式

返回值:存放匹配结果的数组。该数组的内容依赖于 regexp 是否具有全局标志 g。

描述

match() 方法将检索字符串stringObject,以找到一个或多个与regexp匹配的文本。这个方法的行为在很大程度上有赖于 regexp 是否具有标志 g。

如果 regexp 没有标志 g,那么match()方法就只能在stringObject中执行一次匹配。如果没有找到任何匹配的文本, match() 将返回null。否则,它将返回一个数组,其中存放了与它找到的匹配文本有关的信息。该数组的第 0 个元素存放的是匹配文本,而其余的元素存放的是与正则表达式的子表达式匹配的文本。(这里就设计到了刚才说的方向引用的知识点了) 除了这些常规的数组元素之外,返回的数组还含有两个对象属性。index属性声明的是匹配文本的起始字符在 stringObject 中的位置,input 属性声明的是对 stringObject 的引用。

如果 regexp 具有标志 g,则match()方法将执行全局检索,找到stringObject中的所有匹配子字符串。若没有找到任何匹配的子串,则返回null。如果找到了一个或多个匹配子串,则返回一个数组。不过全局匹配返回的数组的内容与前者大不相同,它的数组元素中存放的是 stringObject 中所有的匹配子串,而且也没有 index 属性或 input 属性。

示例:

不带修饰符g

    var url = 'http://www.baidu.com?a=1&b=2&c=3';
    var reg = /([^?&=]+)=([^?&=])/;
    //使用字符串匹配法match
    // var result = url.match(reg);

    //使用RegExp的实例方法exec
    var result = reg.exec(url);

    // 该数组的第 0 个元素存放的是匹配文本,而其余的元素存放的是与正则表达式的子表达式匹配的文本。
    // index 属性声明的是匹配文本的起始字符在 stringObject 中的位置,input 属性声明的是对 stringObject 的引用。
    for (var i = 0; i < result.length; i++) {
        console.log(result[i]);
    }
    console.log(result);    //["a=1", "a", "1", groups:undefined ,index: 21, input: "http://www.baidu.com?a=1&b=2&c=3", length: 3]
    console.log(result.index);  //21
    console.log(reg.lastIndex);
    console.log(result.input);  //http://www.baidu.com?a=1&b=2&c=3

带修饰符g

    /*为真正表达式设置了全局标志g*/
    var url = 'http://www.baidu.com?a=1&b=2&c=3';
    var reg = /([^?&=]+)=([^?&=])/g;

    //使用字符串匹配法match
    // var result = url.match(reg);

    //使用RegExp的实例方法exec
    var result = reg.exec(url);

    for (var i = 0; i < result.length; i++) {
        console.log(result[i]);
    }
    console.log(result);    //["a=1", "b=2", "c=3", length: 3]
    console.log(result.index);  //undefined
    console.log(reg.lastIndex);  //24
    console.log(result.input);  //undefined

在这里字符串的模式匹配方法带了修饰符g后,match方法返回了一个匹配到的所有符合正则表达式的数组,这一点上和使用RegExp的实例方法exec也设置全局修饰符g得出的结果大不相同,可以运行代码看出差异。使用RegExp的实例方法exec得出的结果只是为对象reg的属性lastIndex赋值,记录下一次开始匹配的位置。

2、replace函数

replace() 方法用于在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串。

语法为stringObject.replace(regexp/substr,replacement),结果返回一个新的字符串,是用replacement 替换了 regexp 的第一次匹配或所有匹配之后得到的。

对于 replace() 方法,它有三种使用方式:

1、String.prototype.replace(str,replaceStr); 2、String.prototype.replace(reg,replaceStr); 3、String.prototype.replace(reg,function);

1和2两种的使用我就不再多举例了,相信如果你认真看完前面的文章,肯定最熟悉的就是 replace() 方法了的一、二两种用法了。 这里就提一下第3种使用方法。

先说一下String.prototype.replace(reg,function);中 function 的参数含义,function 会在每次匹配替换的时候调用,有四个参数(第二个参数不固定):

匹配字符串 正则表达式的分组内容,没有分组则没有该参数、 匹配项在字符串中的 index 原字符串

照例来举两个栗子看看:

    //栗子1
    var str = 'a1b2c3d4e5';
    var reg = /\d/g;
    var arr = str.replace(reg,function(match, index, origin){
      console.log(index);// 1 3 5 7 9
      console.log(origin);//a1b2c3d4e5 a1b2c3d4e5 a1b2c3d4e5 a1b2c3d4e5 a1b2c3d4e5
      return parseInt(match) + 1;
    })
    console.log(arr);//a2b3c4d5e6 把每次匹配到的结果+1替换
        
        
    //栗子2
    var str = 'a1b2c3d4e5';
    var reg = /(\d)(\w)(\d)/g;
    var arr = str.replace(reg,function(match, group1, group2, group3, index, origin){
        console.log("match=="+match);// match==1b2      3d4
        console.log("group1=="+group1);//group1==1      group1==3
        console.log("group2=="+group2);//group2==b      group2==d
        console.log("group3=="+group3);//group3==2      group3==4
        console.log("group1+group2+group3=="+group1+group2+group3);  //group1+group2+group3==1b2        group1+group2+group3==3d4
        console.log("group1+group3=="+group1+group3);//group1+group3==12     group1+group3==34
        return group1 + group3;
    });
    console.log(arr);//a12c34e5  去除了每次匹配到的group2

栗子2中利用正则表达式(\d)(\w)(\d)第一次匹配到的是1b2,第二次匹配到的是3d4

第一次用group1+group3==12替换第一次匹配到的1b2,第二次用返回的group1+group3==34替换第二次匹配到的3d4

文章内容转载至

JS高级程序笔记(六)——面向对象程序设计(创建对象与继承)

本章最主要的就是标题的两大块内容,创建对象和继承。

在这里做一个总结:

下面给出两个详细带注释的栗子:

栗子1:(解释创建对象利用到的【组合使用构造函数模式和原型模式】)

    // 构造函数模式定义实例属性
    function Person(name, age, job) {
        this.name = name;
        this.age = age;
        this.job = job;
        this.friends = ["Shelby", "Court"];
    }
    
    // 原型模式定义共享的属性和方法
    Person.prototype = {
    
        // 由于Person.prototype的constructor属性被重写了,
        // 所以这里要再次修改回来
        constructor: Person,
        colors: ["yellow","green"],
        sayName: function () {
            console.log(this.name);
    
        },
        sayColor: function () {
            console.log(this.colors);
    
        }
    
    };
    
    var person1 = new Person("Nicholas", 29, "Software Engineer");
    var person2 = new Person("Greg", 27, "Doctor");
    
    person1.friends.push("Van");
    
    // 由于实例属性都是在构造函数中定义的,而由所有实例共享的constructor
    // 和方法sayName()则是在原型中定义的,所以修改了person1.friends(向其中添加一个新字符串),
    // 并不会影响到person2.friends,因为题目引用了不同的数组。
    console.log(person1.friends);    //"Shelby,Court,Van"
    console.log(person2.friends);    //"Shelby,Court"
    person1.sayName();              //Nicholas
    person2.sayName();              //Greg
    person1.sayColor();             //["yellow", "green"]
    person2.sayColor();             //["yellow", "green"]
    
    // 这里也可以看出来他们引用了不同的数组,都是共享一个sayName方法
    console.log(person1.friends === person2.friends);  //false
    console.log(person1.sayName === person2.sayName);  //true
    
    //而这个colors数组是两个实例对象所共享的,在Person原型对象中定义的
    person1.colors.push("black");
    person1.sayColor();             //["yellow", "green", "black"]
    person2.sayColor();             //["yellow", "green", "black"]

栗子2:(解释寄生组合式继承)

    // 寄生组合式继承,即通过借助【构造函数继承】来继承属性,
    // eg:通过【原型链原型链继承】和【寄生式继承】的混成形式来继承方法
    // 背后的基本思路就是:不必为了指定子类型SubType的原型而调用超类型SuperType的构造函数,
    // 需要的就是超类型SuperType原型的一个副本而已
    // 本质上,就是1、使用寄生式继承来继承超类型的原型
    // 2、让后再将结果指定给子类型的原型【原型链继承】
    // 折旧是为什么eg:叫混合形式的继承方法
    function SuperType(name) {
        this.name = name;
        this.colors = ["red", "blue", "green"];
    }
    
    SuperType.prototype.sayName = function () {
        console.log("my name is " + this.name);
        return this.name;
    }
    
    SuperType.prototype.sayColors = function () {
        console.log("these are my colors " + this.colors);
    }
    
    function SubType(name, age) {
        SuperType.call(this, name);
        this.age = age;
    }
    
    // 混合式继承的最简单形式
    // 第一步、创建超类型原型的一个副本
    // 第二步、为创建的副本添加constructor属性,从而弥补因重写原型而失去的默认的constructor属性
    // 第三步、将新建的对象(副本)赋值给子类型的原型
    function inheritPrototype(superType, subType) {
        var prototype = Object.create(superType.prototype);     //创建对象
        prototype.constructor = subType;                        //增强对象
        subType.prototype = prototype;                          //指定对象
    }
    
    inheritPrototype(SuperType, SubType);
    
    SubType.prototype.sayAge = function () {
        console.log("my age is " + this.age);
        return this.age;
    }
    
    
    var parent = new SuperType("parent");
    var person = new SubType("Nicholas", 29);
    var person2 = new SubType("Nicholas2", 30);
    
    console.log(parent);
    parent.sayName();
    parent.sayColors();
    console.log("-------------------------------------");
    
    console.log(person2);
    person.sayName();
    person.sayAge();
    person.sayColors();
    person.colors.push("black");
    person.sayColors();
    
    console.log("-------------------------------------");
    
    console.log(person2);
    person2.sayName();
    person2.sayAge();
    person2.sayColors();
    person2.colors.push("black2");
    person2.sayColors();

JS高级程序笔记(七)——通过Object.prototype.toString方法精确判断对象的类型

在 JavaScript 里使用 typeof 来判断数据类型,只能区分基本类型,即 “number”,”string”,”undefined”,”boolean”,”object”,“function”,“symbol” (ES6新增)七种。

对于数组、null、对象来说,其关系错综复杂,使用 typeof 都会统一返回 “object” 字符串。
再比如,instanceof操作符存在多个全局作用域(像一个页面中包含多个frame)的情况下,也会是问题多多。

要想区别对象、数组、函数单纯使用 typeof 是不行的,JavaScript中,通过Object.prototype.toString方法,判断某个对象值属于哪种内置类型。

在介绍Object.prototype.toString方法之前,我们先把toString()方法和Object.prototype.toString.call()方法进行对比。

var arr=[1,2];

//直接对一个数组调用toString()
arr.toString();// "1,2"

//通过call指定arr数组为Object.prototype对象中的toString方法的上下文
Object.prototype.toString.call(arr); //"[object Array]"

Object.prototype中的toString方法是确实被继承下来了,但是很多东西总不会一层不变,作为儿子的数组重写了toString方法,所以直接调用数组对象上面的toString方法调用到的实际是重写后的方法,并不是Object.prototype中的toString方法。

应用场景:如果没有应用场景讲这个也没啥用了,那到底有啥用呢?

Object.prototype对象上的toString方法可以用来判断数据类型

Object.prototype.toString.call(arr); //"[object Array]"  判断是否是数组

而重写后的toString方法可以把对象转换成字符串,还可以把数值转换成不同进制的数

var arr=[1,2,3];
console.log(Object.prototype.toString.call(arr));
console.log(Array.prototype.toString.call(arr));

来看一下效果:

看到这里都应该明白了,其实只有Object.prototype上的toString才能用来进行复杂数据类型的判断。

简单解释一些原型链的概念:

我们都知道js中的对象都继承自Object,所以当我们在某个对象上调用一个方法时,会先在该对象上进行查找,如果没找到则会进入对象的原型(也就是.prototype)进行查找,如果没找到,同样的也会进入对象原型的原型进行查找,直到找到或者进入原型链的顶端Object.prototype才会停止。

所以,当我们使用arr.toString()时,不能进行复杂数据类型的判断,因为它调用的是Array.prototype.toString,虽然Array也继承自Object,但js在Array.prototype上重写了toString,而我们通过toString.call(arr)实际上是通过原型链调用了Object.prototype.toString。