前端工作室专注前端开发
 博客首页

《JavaScript高级程序设计(第3版)》 5-9章总结

2020-10-14 16:35:52
JS基础与深入
38
文章图片

《JavaScript高级程序设计(第3版)》 5-9章是对于学习js具有极其重要的作用,务必深刻理解。

《JavaScript高级程序设计(第3版)》 (作者:Nicholas C.Zakas) 总结5-9章

第五章 引用类型

引用类型的值(对象)是引用类型的一个实例。js中,引用类型是一种数据结构。 对象是某个引用类型的实例。新对象是使用new操作符后跟一个构造函数。

5.1 Object 类型

创建Object实例方式有两种。

  • 使用new操作符后跟Object构造函数
    var person = new Object(); 
    person.name = 'lzw';
    person.age = 18;
    console.log(person) // {name: "lzw", age: 18}
  • 使用对象字面量
    var person ={
      name: 'lzw',
      age: 18
    };
    console.log(person)

属性名也可以使用字符串

var person = {
    'name': 'lzw',
    'age': 18,
    1: true
}
console.log(person.name,person.age,person[1],person['1']) // lzw 18 true true(person.1和person.'1'会报错)

在函数传递参数时,推荐使用对象传递参数

function a(obj){
    console.log('欢迎'+obj.age+ '岁的'+ obj.name)
};
a({name: 'lzw',age: 18})  //输出 '欢迎18岁的lzw'

访问对象属性的方法

  • 点表示法 person.name
  • 方括号语法 person['name']

方括号语法的好处在于可以使用变量和包含会导致语法错误的字符,或者属性名使用的是关键字或保留字。 var name = 'name'; person[name] // 使用变量 person['first name'] // 包含空格

5.2 Array 类型

js数组每一项可以保存任何类型的数据,而且数组大小可以动态调节,自动增长和缩短,以容纳新数据或删除旧数据。 创建数组的两种基本方式是

  • 使用Array构造函数

var colors = new Array(); var colors = new Array(20); // 长度20的数组 var colors = new Array('green','red','blue') // 创建包含三个字符串值的数组 var colors = Array(3) // 不用new操作符,创建长度为3的数组 var colors = Array(green','red','blue') // 不用new操作符,创建长度为3的数组

  • 使用数组字面量表示法

var colors = ['red','green','blue']; // 创建包含3个字符串的数组 var colors = []; // 创建一个空数组 var colors = ['red','green','blue',]; // 不要这样子做,会创建一个三到四项的数组 var colors = [ , , , ]; // 不要这样子做,会创建一个三到四项的数组 读取和设置数据时,使用方括号法。索引从0开始

var colors = ['red','green','blue',];
console.log(colors[0]); // 读取第一项,输出'red'
colors[1] = 'yellow'; // 将第二项改为'yellow'
colors[2] = 'green'; // 将第三项改为'green'
colors[3]= 'blue'; // 新增第四项为'blue'
console.log(colors) // 输出 ["red", "yellow", "green", "blue"]
console.log(colors.length) // 输出 4(自动添加了长度)

数组length属性不只是只读的。可以通过设置这个属性,从数组尾部移除或者新增

var num = [1,2,3];
num.length = 2; 
console.log(num, num[2]) // 输出 [1, 2] undefined(通过设置length,第三项不见了)
var num = [1,2,3];
num.length = 4; 
console.log(num,num[3]) // 输出 [1, 2, 3, empty] undefined (通过设置length,新增了第四项不过没有存下任何值)

利用length在数组末尾添加新项

var num = [1,2,3];
num[num.length] = 4; 
console.log(num,num[3]) // [1, 2, 3, 4] 4(通过设置length下标,新增了第四项)

length会动态更新,例如把一个值放到超出当前数组长度的位置,length会自动适应这一变化。

var num = [1,2,3];
num[100] = 4; 
console.log(num,num.length) // 输出 [1, 2, 3, empty × 97, 4] 101

5.2.1 检测数组

  • 一个网页或者一个全局作用域
    if (value instanceof Array){
      //对数组执行某些操作
    }
  • 存在两个以上全局执行环境
    if (Array.isArray(value)){
      //对数组执行某些操作
    }

5.2.2 转换方法

var colors = ['red','green','blue'];
console.log(colors.toString()) // 输出"red,green,blue"
console.log(colors.toLocaleString()) // 输出"red,green,blue"
console.log(colors.valueOf()) // 输出 ["red", "green", "blue"]
alert(colors.valueOf()) // 输出"red,green,blue" (alert会调用toString()方法)

toLocaleString()、toString()、valueOf()默认情况下使用逗号分隔字符串的形式返回数组项,使用join()方法,可以使用其它分隔符来返回数组项

var colors = ['red','green','blue'];
console.log(colors.join(',')) // 输出 red,green,blue
console.log(colors.join('||')) // 输出 red||green||blue
console.log(colors.join('')) // 输出 redgreenblue
console.log(colors.join()) // 输出 red,green,blue
console.log(colors.join(undefined)) // 输出 red,green,blue(即使你知道这些奇怪的知识点,也不要炫技,免得同事看不懂)
console.log(colors.join(' ')) // red green blue
console.log(colors.join(null)) // rednullgreennullblue(奇怪的知识又增加了)
console.log(colors.join([])) // redgreenblue(奇怪的知识又增加了)
console.log(colors.join({})) // red[object Object]green[object Object]blue(更奇怪的知识又增加了)
var colors = ['red','green','blue'];console.log(colors.join('6666')) // red6666green6666blue
var colors = [[],red','green','blue'];
console.log(colors.join(',')) // 报错 Uncaught SyntaxError: Unexpected string
var colors1 = [{},red','green','blue'];
console.log(colors1.join(',')) // 报错 Uncaught SyntaxError: Unexpected string
var colors2 = [null,undefined,'red','green','blue'];
console.log(colors2.join(',')) // 输出 ,,red,green,blue (null 和undefined 会返回空字符串)

5.2.3 栈方法

尾部推入push和弹出pop push方法

var arr = new Array(); 
var length = arr.push(1,2);
console.log(arr,length) // 输出 [1, 2] 2 (注意返回的是,修改后数组的长度)

注意返回的是,修改后数组的长度 pop方法

var arr = new Array(1,2);
var item = arr.pop();
console.log(item,arr) // 输出 2 [1](数组弹出末尾的项,item返回的是末尾弹出的项)

5.2.4 队列方法

数组前端推入unshift和弹出shift unshift方法

var arr = new Array(3,4);
var length = arr.unshift(1,2);
console.log(length, arr) // 输出 4 [1, 2, 3, 4](注意返回的是修改后数组的长度)

shift方法

var arr = new Array(3,4);
var item = arr.shift();
console.log(item,arr) // 3 [4](数组弹出前端的项,item返回的是前端弹出的项)

5.2.5 重排方法

reverse和sort,返回值是经过排序后的数组 reverse()方法

var arr = [1,2,3,4];
arr.reverse()
console.log(arr) // [4, 3, 2, 1] (实现数组的倒序)

sort()方法 默认小值在前大值在后,调用每个数组项的的toString()转型方法

var arr = [4,3,2,1];
arr.sort();
console.log(arr) // [1, 2, 3, 4]
var arr =['a','A','ab','ac'];
arr.sort();
console.log(arr) // ["A", "a", "ab", "ac"] 
var arr =[
    {toString: function(){return '2'},value: '2'},
    {toString: function(){return 1}}
];
arr.sort();
console.log(arr) //输出 
// [
//    {toString: function(){return 1}}
 //   {toString: function(){return '2'},value: '2'}
// ]
var arr = [1,2,25,5];
arr.sort();
console.log(arr) // 输出 [1, 2, 25, 5](数字也被转换成字符串比较了,我的天!)

所以如何解决这个问题呢? 传递一个比较函数

function compare(value1,value2){
    if(value1-value2 < 0){ 
        return -1
    }else if(value1-value2 > 0){
        return 1
    }else{ return 0 }
}
var arr = [1,2,25,5];
arr.sort(compare) // [1, 2, 5, 25]
function compare(value1,value2){
    if(value1-value2 < 0){ 
        return -1
    }else if(value1-value2 > 0){
        return 1
    }else{
      return 0
       }
}
var arr = [{valueOf:function(){return 2},value:2}, {valueOf:function(){return 1}}];
arr.sort(compare) // [{valueOf:function(){return 1}},{valueOf:function(){return 2},value:2}];(调用valueOf()方法)

对象相减时调用的是valueOf()方法

5.2.6 操作方法

concat()方法

var arr = [1,2];
var arr2 = arr.concat(3,[4,5]);
console.log(arr2); // [1, 2, 3, 4, 5]
arr[1] = 6;
console.log(arr, arr2) // [1, 6] [1, 2, 3, 4, 5](即使arr改变arr2也不会改变,这是个深拷贝)
var arr = [1,2]; 
var arr2 = arr; 
arr[1] = 6;
console.log(arr2) // [1,6](还记得引用类型那藕断丝连的关系么)

concat()方法不会影响原数组。 slice()方法 slice()可以接受两个参数,返回项的起始位置和结束位置,这个位置当然是用索引表示。不会影响原数组

var arr = [1,2,3,4,5];
var arr1 = arr.slice(1,3);
console.log(arr,arr1) // [1, 2, 3, 4, 5] [2, 3](可以看到,该方法也不会影响原数组)

返回的项相当于数学中[1,3),如果你学过初中数学的话,这样子比较好理解,即包含前面却不包含后面之间的全部区域

var arr = [1,2,3,4,5];
var arr1 = arr.slice(1); // 相当于arr.slice(1,arr.length)
console.log(arr,arr1) //[1, 2, 3, 4, 5] [2, 3, 4, 5]

只传递一个参数相当于数学中[1,arr.length) 如果传递的是负数

var arr = [1,2,3,4,5];
var arr1 = arr.slice(-4,-2); // 相当于arr.slice(-4+arr.length, -2+arr.length)
console.log(arr1) // [2, 3]

只传递一个参数相当于数学中[-4+arr.length, -2+arr.length),即[1,3) 如果传递的参数不是按照顺序,返回空数组。

ar arr = [1,2,3,4,5];
var arr1 = arr.slice(3,1); //正确顺序应该为 1 3
console.log(arr1) // []
var arr2 = arr.slice(-2,-4); // 正确顺序为 -4 -2
console.log(arr2) // []

传递相反顺序的参数,相当于数学中[3,1),那当然不包含任何区域。 splice()方法 splice(arg1,arg2,arg3,arg4...): arg1代表要删除的第一项的位置,arg2代表要删除的项数,arg3、arg4...都是要插入的参数。

  • 删除:传递arg1和arg2。
    var arr = [1,2,3,4,5]; 
    var arr1 = arr.splice(1,3); // 从索引1开始后三项,相当于返回[arg1,arg1+arg2)
    console.log(arr,arr1) // [1,5]  [2, 3, 4](会影响原数组)
    相当于返回数学中[1,1+3),即[1,4);总得有个恶人来改变原数组,splice就是那个恶人,原数组变成删除后的组合
  • 插入:可以指定位置插入不定数项。不删除所以arg2为0,arg3、arg4、arg5为要插入的项
    var arr = [1,2,3,4,5]; 
    var arr1 = arr.splice(1,0,2.1,2.2);
    console.log(arr,arr1) // [1, 2.1, 2.2, 2, 3, 4, 5] [](删除项为0所以返回空数组)
  • 替换:实际是前两者混合,先删除再插入
    var arr = [1,2,3,4,5]; 
    var arr1 = arr.splice(1,1,2.1,2.2); // 删除范围为[1,2),删除后插入 2.1 2.2
    console.log(arr,arr1) // [1, 2.1, 2.2, 3, 4, 5] [2]
    项数传递负数会怎么样?
    var arr = [1,2,3,4,5]; 
    var arr1 = arr.splice(1,-1,2.1,2.2);
    console.log(arr,arr1) // [1, 2.1, 2.2, 2, 3, 4, 5] [](不会删除任何项,与项数为0一致)
    实际上就是插入操作。 索引传入负数会怎么样?
    var arr = [1,2,3,4,5]; 
    var arr1 = arr.splice(-1,1,2.1,2.2);  // 相当于arr.splice(-1 + arr.length,1,2.1,2.2);
    console.log(arr,arr1) // [1, 2, 3, 4, 2.1, 2.2] [5]

简而言之,为了不报错,这个方法内部进行某些转换,所以尽管你传负数去刁难它,它依然运行良好

5.2.7 位置方法

indexOf()和lastIndexOf() indexOf(): 从前往后查找索引 lastIndexOf():从后往前查找索引

var arr = [1,2,3,4,5,4]; 
var index = arr.indexOf(4);
console.log(index)  // 3
var index1 = arr.indexOf(6);
console.log(index1) // -1(又一个被吐槽的点,没有就没有返回个-1干嘛)
var index2 = arr.lastIndexOf(4);
console.log(index2) // 5

查找对象会如何?

var obj = {name: 'lzw'};
var arr=[obj];
arr.indexOf(obj) // 0
var obj = {name: 'lzw'};
var arr=[{name: 'lzw'}];
arr.indexOf({name:'lzw'}) // -1(果然还是直接写一样的对象是不行的)

5.2.8 迭代方法

js为数组定义了五个迭代方法。每个方法都接收两个参数:每一项上执行的函数和运行该函数的作用域对象(影响this的值,可不传)。每一项上执行的函数会接受三个参数:数组的项的值、该项在数组中索引和数组对象本身。

  • every():每一项执行函数都返回true时返回true
    var arr = [1,2,3,4,5,6];
    var result = arr.every(function(item,index,arr1){
      return item >= 1; // 每一项都要大于或者等于1
    });
    console.log(result) // true
  • filter(): 执行函数返回true的所有项组成的数组
    var arr = [1,2,3,4,5,6];
    var result = arr.filter(function(item,index,arr1){
      return item >= 4;
    });
    console.log(result) // [4, 5, 6](大于4的集合)
  • forEach():每一项执行特定函数,没有返回值
    var arr = [1,2,3,4,5,6];
    var result = arr.forEach(function(item,index,arr1){
      arr[index] = index
    });
    console.log(result,arr) // [0, 1, 2, 3, 4, 5](遍历改变了自身数组)
  • map():返回每次函数调用结果组成的数组
    var arr = [1,2,3,4,5,6];
    var result = arr.map(function(item,index,arr1){
      return item = item + 1
    });
    console.log(result,arr) //  [2, 3, 4, 5, 6, 7] [1, 2, 3, 4, 5, 6]
  • some():任一项执行函数返回true则返回true
    var arr = [1,2,3,4,5,6];
    var result = arr.some(function(item,index,arr1){
      return item >= 6 
    });
    console.log(result,arr) // true [1, 2, 3, 4, 5, 6]

相同和区别

  1. 这些迭代方法都可以遍历数组,通过某种方法去改变原数组,但是无法通过改变item来改变元素组。 例如:
    var arr = [1,2,3,4,5,6];
    var result = arr.some(function(item,index,arr1){
     arr[index] = 1;
    });
    console.log(result,arr) // false [1, 1, 1, 1, 1, 1](数组项变为全是1)
    但是还是尽量要求程序员严格要求自己按照方法的具体作用来使用它。一般而言,想要返回布尔值值的进行某些逻辑判断的使用every()和some(),过滤数组使用filter(),执行某些操作的不需要任何返回的使用forEach(),对数组每一项进行某些转换并返回的使用map() 如果仅仅通过item改变原数组呢
    var arr = [1,2,3,4,5,6];
    var result = arr.every(function(item,index,arr1){
     item = 1;
     return false
    });
    console.log(result,arr) // false [1, 2, 3, 4, 5, 6](every方法里操作item参数不会影响原数组)
    var arr = [1,2,3,4,5,6];
    var result = arr.filter(function(item,index,arr1){
     item = 1;
     return false
    });
    console.log(result,arr) // []  [1, 2, 3, 4, 5, 6](一样)
    var arr = [1,2,3,4,5,6];
    var result = arr.forEach(function(item,index,arr1){
     item = 1;
    });
    console.log(result,arr) // undefined  [1, 2, 3, 4, 5, 6] (一样)
    var arr = [1,2,3,4,5,6];
    var result = arr.map(function(item,index,arr1){
     return item = 1;
    });
    console.log(result,arr) // [1, 1, 1, 1, 1, 1]  [1, 2, 3, 4, 5, 6](能改变返回的数组,不能改变原数组)
    var arr = [1,2,3,4,5,6];
    var result = arr.some(function(item,index,arr1){
     item = 1;
     return false
    });
    console.log(result,arr) // false [1, 2, 3, 4, 5, 6](some方法里操作item参数不会影响原数组)
    除了每一项遍历使用arr[index]来修改原数组以外,map()方法可以直接把返回数组赋给arr来改变原数组
    var arr = [1,2,3,4,5,6];
    var arr = arr.map(function(item,index,arr1){
     return item = 1;
    });
    console.log(arr) // [1, 1, 1, 1, 1, 1]
    看上去比下面的方法更加专业
    var arr = [1,2,3,4,5,6];
    var result = arr.map(function(item,index,arr1){
     arr[index] = 1
     return item
    });
    console.log(arr) // [1, 1, 1, 1, 1, 1]
  2. every()遇到false后不再执行,就像具有短路效应,其他一律无法跳出循环
    var arr = [1,2,3,4,5,6];
    var result = arr.every(function(item,index,arr1){
     if(index == 2){ return false }
     console.log(index)
     return true
    }); // 输出 0 1(只执行了两项)

利用这个属性有时候可以省下很多不必要的循坏。与es6中的find类似。 最后,迭代方法的第二个参数有什么作用呢?

var arr = [1,2,3,4,5,6];
var obj = {num: 1}
var result = arr.every(function(item,index,arr1){
    if(index == this.num){ 
        console.log(index,this.num);return false 
    }
    return true
},obj); // 输出 1 1(就是让this.num相当于使用obj.num,奇怪的知识又增加了)

5.2.9 归并方法

两个归并方法:reduce()和reduceRight() 接收四个参数:前一个值、当前值、项的索引和数组对象

var numArr = [1,2,3,4,5,6];
var sum = numArr.reduce(function(prev,cur,index,array){
    console.log(prev)
    return prev + cur
})
console.log(sum) 
// 输出 1 3 6 10 15 21(prev并不是前一个值,而是前一个项函数执行结果)
var numArr = [1,2,3,4,5,6];
var sum = numArr.reduceRight(function(prev,cur,index,array){
    console.log(prev)
    return prev + cur
})
console.log(sum)
// 输出 6 11 15 18 20 21(和reduce的区别仅仅是从哪边开始)

5.3 Date类型

这里我们所说的毫秒数都是相对于1970年1月1日0时而言 Date.parse() 方法接收一个表示日期的字符串参数。返回毫秒数

var someDate = new Date(Date.parse("May 25, 2004"));
console.log(someDate) // Tue May 25 2004 00:00:00 GMT+0800 (中国标准时间)
console.log(Date.parse('2/13/2004')) // 1076601600000(月日年形式)
console.log(Date.parse('2-13-2004')) // 1076601600000(月日年形式)
console.log(Date.parse('2004-2-13')) // 1076601600000(年月日形式)
console.log(Date.parse('2004/2/13')) // 1076601600000(年月日形式)
console.log(Date.parse('May, 25 2004')) // 1085414400000(英文月日年)
console.log(Date.parse('may, 25 2004')) // 1085414400000(小写也可以)
console.log(Date.parse('2004,May,25')) // 1085414400000(英文年月日)
console.log(Date.parse('Tue May 25 2004 00:00:00 GMT+0800')) // 1085414400000(星期 月 日 年 时 分 秒 时区)
console.log(Date.parse('May 25 2004 00:00:00 GMT+0800')) // 1085414400000(不传星期也行)
console.log(Date.parse('2004-05-25T00:00:00')) // 1085414400000(ISO拓展格式)
console.log(Date.parse('2004-5-25T00:00:00')) // NaN(ISO拓展格式少个0都不行,无法转换时返回NaN)
var someDate = new Date(Date.parse("nhhg"));
console.log(someDate) // Invalid Date(日期无效时)

相当于使用new Date(),因为Date构造函数也会调用Date.parse()

var someDate = new Date("May 25, 2004");
console.log(someDate) // Tue May 25 2004 00:00:00 GMT+0800 (中国标准时间)

假如给个不存在日期呢

console.log(Date.parse('2020-2-30')) // 1582992000000(年月日形式)
console.log(Date.parse('2020-3-1')) // 1582992000000(相当于31号)

Date.UTC() 方法同样返回表示日期的毫秒数,但它传递的参数与Date.parse()不同,参数分别是年份、月份索引(即从0开始)、天数(1到31)、小时数(0到23)、分钟、秒、毫秒数。

console.log(Date.UTC(2020)) // 1577836800000(年)
console.log(Date.UTC(2020,0)) // 1577836800000(年月)
console.log(Date.UTC(2020,0,1)) // 1577836800000(年月日)
console.log(Date.UTC(2020,0,1,0,0,0,0)) // 1577836800000(年月日时分秒毫秒)

如果给一个错误的不存在的日期例如2020年的2月30号

console.log(Date.UTC(2020,1,30,0,0,0,0)) // 1583020800000
console.log(Date.UTC(2020,2,1,0,0,0,0)) // 1583020800000(相当于31号)

Date()模仿Date.UTC()

console.log(new Date(2020,2,1,0,0,0,0)) // Sun Mar 01 2020 00:00:00 GMT+0800 (中国标准时间)

Date.now() 方法返回调用时的毫秒数

console.log(Date.now()) // 1598923621276

5.3.1 继承的方法

Date类型重写了toLocaleString()、toString()和valueOf()方法 可以使用比较运算符比较两个日期

var date1 = new Date(2020,1,2);
var date2 = new Date(2020,0,2);
console.log(date1>date2) // true
console.log(date1<date2) // false

5.3.2 日期/时间组件方法

console.log(new Date().getTime()) // 1598925990220
console.log(new Date().valueOf()) // 1598925990220
console.log(new Date().getFullYear()) // 2020
console.log(new Date().setFullYear(2021)) // 1630462175410
console.log(new Date().getMonth()) // 8(索引值,即9月)
console.log(new Date().setMonth(9)) // 1601518283722
console.log(new Date().getDate()) // 1(这个不是索引值)
console.log(new Date().setDate(2)) // 1599012799021

其他略

5.4 RegExp 类型

js通过RegExp类型来支持正则表达式。 var expression = / pattern / flags ; flags支持以下一个或多个标志

  • g: 表示全局模式。
  • i: 表示不区分大小写模式
  • m: 表示多行模式

例如:

var pattern1 = /a/g;  //匹配字符串中所有的"a"实例,注意是所有
var pattern2 = /[ab]t/i;  //匹配字符串中第一个bt或者at,不区分大小写,注意是第一个
var pattern3 = /.at/gi //匹配所有以"at"结尾的字符串,.代表一个字符,不区分大小写

需要转义的元字符,之所以需要转义是因为这些字符已经被用于特殊的含义,不能再表述自身 ( [ { \ ^ $ | ) ? * + .]} 例如刚刚的例子,/[ab]/不能指匹配"[ab]"字符串,而是匹配"a"或者"b",/.at/不能指匹配".at"字符串,而是匹配"at"结尾的字符串 如果你必须要匹配"[ab]"字符串,那就需要用\反斜杠来转义,例如

var pattern2 = /\[ab\]/i
var pattern3 = /\.at/gi 

使用字面量模式和构造函数来定义的区别

var pattern2 = /[bc]at/i;
var pattern3 = new RegExp("[bc]at", "i");

上面两种方式貌似是一样的,没什么区别,字面量模式还简介许多。来看看需要转义的情况。

var pattern2 = /\[bc\]at/i;
var pattern3 = new RegExp("\\[bc\\]at", "i"); //(需要两个反斜杠,因为参数是字符串,放斜杠自己也需要反斜杠转义)

所以还是用字面量模式吧,免得某些情况把自己转义转晕了

5.4.1 RegExp实例属性

var pattern = /.at/gi;
console.log(pattern.global) // true(判断是否设置了g标志)
console.log(pattern.ignoreCase) // true(判断是否设置了i标志)
console.log(pattern.lastIndex) // 0(表示搜索的下一个匹配项的字符位置)
console.log(pattern.multiline) // false(表示是否设置了m标志)
console.log(pattern.source) // .at(正则表达式的字符串表示)

5.4.2 RegExp实例方法

exec() 方法,接收要应用模式的字符串,返回一个包含匹配项的数组,没有时返回null 返回的数组还额外包含两个属性: index和input

var str = "Visit W3School, W3School is a place to study web technology."; 
var patt = new RegExp("W3School","g");
var result;
while ((result = patt.exec(str)) != null)  {
  console.log(result[0]);
  console.log(result.index);
  console.log(result.input);
  console.log(patt.lastIndex);
 }
// W3School
// 6
// Visit W3School, W3School is a place to study web technology.
// 14
// W3School
// 16
// Visit W3School, W3School is a place to study web technology.
// 24

全局模式下,即设置了g时,lastIndex可以递增

var text = "cat, bat, sat, fat"; 
var pattern1 = /.at/; 
var matches = pattern1.exec(text); 
console.log(matches.index); //0
console.log(matches[0]); //cat
console.log(pattern1.lastIndex); //0
matches = pattern1.exec(text); 
console.log(matches.index); //0
console.log(matches[0]); //cat
console.log(pattern1.lastIndex); //0

由此可见不设置全局模式,lastIndex不会增长。

var text = "cat, bat, sat, fat"; 
var pattern1 = /.at/g;   
while ((matches = pattern1.exec(text)) != null)  {
    console.log(matches.index); //0
    console.log(matches[0]); //cat
    console.log(pattern1.lastIndex); //0
}
// 0
// cat
// 3
// 5
// bat
// 8
// 10
// sat
// 13
// 15
// fat
// 18

遍历输出每一个匹配的项。 什么时候使用exec()? 需要知道匹配到的项时,例如:你想知道匹配到什么项然后把它替换为空字符串 第二个方法test(),接受一个字符串参数,符合参数匹配情况返回true,否则返回false

var text = "cat, bat, sat, fat"; 
var pattern1 = /.at/g;
console.log(pattern1.test(text)) // true

RegExp 实例继承的 toLocaleString()和 toString()方法都会返回正则表达式的字面量,与创 建正则表达式的方式无关

var pattern = new RegExp("\\.at","gi")
console.log(pattern.toString()) // /\.at/gi
console.log(pattern.toLocaleString()) // /\.at/gi(字符串转义后只是一个反斜杠)

5.4.3 RegExp构造函数属性

5.4.4 模式局限性

5.5 Function 类型

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

function add(a,b){
    return a+b
}

等同于

var add = function(){
    return a + b;
}

不推荐使用构造函数创建函数,会影响性能。

function add(a,b){
    return a+b
}
console.log(add(1,1)) // 2
var anotherAdd = add;
add = null;
anotherAdd(1,1) // 2(存放的是指针,所以anotherAdd不会指向null)

5.5.1 没有重载(深入理解)

function add(a){
    return a+1
}
function add(a){
    return a+2
}
console.log(add(1)) // 3(后面声明的函数会覆盖同名函数)

5.5.2 函数声明和函数表达式

解析器会优先读取函数声明,使其再任何代码之前可用;函数表达式必须等到解析器执行到它所在的代码行,才会真正被解释执行。

console.log(add(1)) // 2(函数声明之前执行依然有效)
function add(a){
    return a+1
}
console.log(typeof add) // undefined
console.log(add(1)) // 报错 Uncaught TypeError: add is not a function
var add = function(a){
    return a+1
}

相当于

var add 
console.log(typeof add) // undefined
console.log(add(1)) // 报错 Uncaught TypeError: add is not a function
add = function(a){
    return a+1
}

5.5.3 作为值的函数

把一个函数执行结果返回

function callSomeFunction(someFunction, someArgument){
    return someFunction(someArgument); 
}

5.5.4 函数内部属性

函数内部,有两个特殊对象:arguments和this。arguments对象有一个callee的属性,是一个指向arguments对象的函数的指针。

function factorial(num){ 
 if (num <=1) { 
 return 1; 
 } else { 
 return num * factorial(num-1) 
 } 
}
factorial(3) // 6

消除与函数名的耦合,因为如果改函数名,内部递归的函数名也必须修改

function factorial(num){ 
 if (num <=1) { 
 return 1; 
 } else { 
 return num * arguments.callee(num-1) 
 } 
}
factorial(3) // 6

严格模式下

"use strict"
function factorial(num){ 
 if (num <=1) { 
 return 1; 
 } else { 
 return num * arguments.callee(num-1) 
 } 
}
factorial(3) // 报错 Uncaught TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them

另一个特殊的对象this 全局作用域调用函数,this对象引用的就是window。对象调用函数时指向对象。

var color = "yellow";
function sayColor(){
    console.log(this.color)
}
sayColor() // 输出 "yellow"(浏览器中全局声明color以后,可以通过window.color访问)
var obj = {
    color: "green",
    sayColor: sayColor
}
obj.sayColor() // 输出 "green"(this指向obj)
var sayColorAgain = obj.sayColor;
sayColorAgain() // 输出 "yellow"(this指向window,因为实际上是函数调用)

函数指向仍是同一个函数,只是this的指向根据执行环境而产生了变化。为什么要这样子做?是为了区别执行环境。 可以窥探代码的函数对象属性:caller

function inner(){ 
 console.log(inner.caller); 
}
inner() // 输出 null(全局作用域中调用当前函数,返回值为null
function outer(){ 
 inner(); 
} 
function inner(){ 
 console.log(inner.caller); 
} 
outer(); //输出 
// ƒ outer(){ 
//  inner(); 
// }

还记得之前说的紧密耦合的事情么,要把上面的函数通过arguments.callee变得更加松散的耦合。arguments.callee指向函数自身;arguments.callee.caller指向调用函数的函数自身

function outer(){ 
 inner(); 
} 
function inner(){ 
 console.log(arguments.callee.caller); 
} 
outer(); //输出 
// ƒ outer(){ 
//  inner(); 
// }

严格模式下

"use strict"
function outer(){ 
 inner(); 
} 
function inner(){ 
 console.log(inner.caller); 
} 
outer(); //报错 Uncaught TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them

使用arguments.callee.caller,肯定也是报错,在此就不举例子了。

5.5.5 函数属性和方法

js中的函数是对象,因此函数也有属性和方法。每个函数都包含两个属性:length和prototype。length表示希望接受的命名参数的个数。

function add(a,b,c){
    console.log(arguments.length) // 输出的是参数个数
    return a + b + c
}
console.log(add.length) // 输出 3(输出的是命名参数的个数)
add(1,2,3,4) //输出 4, 返回 6

prototype属性不可枚举,因此使用for-in无法发现 每个函数都拥有两个方法:apply()和call(),实际上是用于设置函数体内this的指向,否则全局函数调用这个this老是指向window。 apply():接收两个参数,一个是运行函数的作用域,另一个是参数数组,即Array实例或者arguments对象。

function add(a,b){
    console.log(a+b)
}
function callAdd(a,b){
    add.apply(this,arguments) // 传入arguments对象
}
callAdd(1,1); //输出 2
function callAdd1(a,b){
    add.apply(this,[a,b]) // 传入数组
}
callAdd1(1,1); // 输出 2

this没有被用到的时候,作用域可以传null,实际上调用的函数没有用到this时,看起来和直接调用一点区别都没有,甚至代码还多了。 call()方法和apply()方法相同,第一个参数是this值,其余参数是传递给函数的参数。

function add(a,b){
    console.log(a+b)
}
function callAdd(a,b){
    add.call(this,a,b)
}
callAdd(1,1); // 输出 2

如果你不想利用this做点什么,就没必要使用apply和call,例如上面的例子,但是你不知道以后会不会用到this的时候,可以使用。再另外,this也可以当作参数直接传入。总而言之,apply和call的优势是什么?是不知道所调用函数是否需要指定this,保险起见还是绑定this,或者知道一定要绑定this,而且这样绑定this,不需要修改调用函数的代码,不需要任何耦合关系,例如this作为参数传入的话,仍需要改动调用函数的代码,以适应新加的一个指定this的参数。 举个真正用到this的例子行不行呀,马上来。

var color = "yellow";
function sayColor(){
    console.log(this.color)
}
var obj = {
    color: "green",
}
console.log(sayColor.call(this)) // 输出 "yellow"
console.log(sayColor.call(window)) // 输出 "yellow"
console.log(sayColor.call(obj)) // 输出 "green"(this指向了obj)

bind() 方法 该方法会传入一个参数,需要绑定的this值指向,创建一个函数实例,注意不会像apply和call那样会直接执行这个函数。

var color = "yellow";
function sayColor(){
    console.log(this.color)
}
var obj = {
    color: "green",
}
var objSayColor = sayColor.bind(obj);
objSayColor() // 输出 "green"

当你不想立即执行时可以用bind 说到底,使用这些方法都是为了this的指向。

5.6 基本包装类型

js提供了3个特殊的引用类型: Boolean、Number、String。 对于代码

var s1 = "some text";
var s2 = s1.substring(2);

读取字符串时,后台进行了一系列操作

  • 创建String类型的一个实例
  • 在实例上调用指定的方法
  • 销毁这个实例
var s1 = new String("some text");
var s2 = s1.substring(2);
s1 = null;

这样的处理同样适用于Boolean、Number类型对应的布尔值和数字值。 基本包装类型和引用类型的区别:自动创建的基本包类型的对象只存在一瞬间,然后立即被销毁,如上所示;使用new操作符创建引用类型的的实例在执行流离开前一直都被保存在内存中。

var s1 = "some text";  // 创建销毁一次
s1.color = "red";  // 又创建销毁一次 
console.log(s1.color); // undefined (又创建销毁一次,所以输出undefined)

尽量不要使用显示调用Boolean、String、Number来创建基本包装类型 Object构造函数也会像工厂方法一样,根据传入值的类型返回相应基本包装类型的实例。 使用Object构造函数创建基本包装类型的实例

var obj = new Object("some text"); 
console.log(obj instanceof String); //true
var obj1 = new Object(1); 
console.log(obj1 instanceof Number); //true
var obj2 = new Object(true); 
console.log(obj2 instanceof Boolean); //true

直接调用同名转型函数与使用new操作符调用基本包装类型的构造函数不同

var value = "1";
console.log(typeof Number(value)) // "number"(基本类型)
console.log(typeof new Number(value)) // "object"(引用类型)

5.6.1 Boolean类型

创建Boolean对象 var booleanObject = new Boolean(true); valueOf()返回布尔值,toString()返回"true"或者"false" 创建的Boolean对象造成的误解

var falseObject = new Boolean(false); 
var result = falseObject && true; // 因为是对象在布尔表达式中代表true,所以使用&&运算符时,会返回后面的布尔值true
console.log(falseObject.valueOf())  // false
console.log(result); // true
console.log(typeof falseObject.valueOf()) // boolean
console.log(typeof falseObject); // object(falseObject是引用类型)
console.log(falseObject.valueOf() instanceof Boolean)  // false
console.log(falseObject instanceof Boolean); // true(falseObject是Boolean实例)

建议:永远不要使用Boolean对象。 那我们要怎么转换成布尔值? 使用Boolean()

console.log(Boolean('false')) // true
console.log(Boolean(0)) // false
typeof Boolean(0) // false(是基本类型,不是Boolean对象)

或者使用隐式转换!!

!!"false" // true
!!0 // false

5.6.2 Number类型

创建Number对象 var numberObject = new Number(10); valueOf()返回数值,toString()返回字符串形式的数值

var numObject = new Number(10);
console.log(numObject.valueOf()); // 10
console.log(numObject.toString()); // "10"

对于基本类型的数值,使用toString()方法可以传入进制参数

var num = 10; 
console.log(num.toString()); //"10"
console.log(num.toString(2)); //"1010"
console.log(num.toString(8)); //"12"
console.log(num.toString(10)); //"10"
console.log(num.toString(16)); //"a"

toFixed()指定小数点后的位数,超出的四舍五入。

console.log((10.0001).toFixed(3)) // "10.000"

toExponential()返回科学计数法表示方式

console.log((100000).toExponential(2)) // "1.00e+5"

toPrecision()返回数值最合适的格式,参数是总的位数

console.log((10).toPrecision(4)) // "10.00"
console.log((103342324324).toPrecision(4)) // "1.033e+11"

一样不建议创建Number对象

var numberObject = new Number(10); 
var numberValue = 10; 
console.log(typeof numberObject); //"object"(引用类型)
console.log(typeof numberValue); //"number"(基本类型)
console.log(numberObject instanceof Number); //true(是Number类型的实例)
console.log(numberValue instanceof Number); //false(不是Number类型的实例)

真的要转换成数值时要怎么办呢? 使用Number()

Number('10') // 10
typeof Number('10') // "number"

或者隐式转换

+false // 0
+'10' // 10

5.6.3 String类型

创建String对象 var stringObject = new String("hello world"); valueOf()、toString()方法都返回字符串值

var stringObject = new String("hello world");
console.log(stringObject.valueOf()) // "hello world"
console.log(stringObject.toString()) // "hello world"

String类型的length属性

console.log("hello world".length) // 11

String类型的方法: 1、 字符方法 charAt()和charCodeAt(),接收一个位置参数,当然这是个索引位置。返回的一个是char,即字符,一个charCode,即字符编码。

console.log("hello world".charAt(1)) // e
console.log("hello world".charCodeAt(1)) // 101

其实也可以使用方括号访问特定位置字符。

var str = "hello world"
console.log(str[1]) // e
console.log("hello world"[1]) // e

2、字符串的操作方法 contact()拼接符串,不影响原字符串

var stringValue = "hello "; 
var result = stringValue.concat("world", "!"); 
console.log(result); //"hello world!"
console.log(stringValue); //"hello"

使用+操作符更加方便简洁,所以很少用这个方法

var stringValue = "hello "; 
var result = stringValue+"world"+"!"; 
console.log(result); //"hello world!"
console.log(stringValue); //"hello"

三个创建新字符串的方法:slice()、substr()和substring()。slice()和substring()第一个参数指定子字符串开始的位置,第二个参数指示结束的位置。substr()第一个参数指定的是开始的位置,第二个参数是返回个数。 眼见为实

var str = "hello world"; 
console.log(str.slice(2)); //"llo world"(记忆:相当于数学上[2,∞))
console.log(str.substring(2)); //"llo world"(记忆:相当于数学上[2,∞))
console.log(str.substr(2)); //"llo world"(记忆:相当于数学上[2,∞))
console.log(str.slice(2, 8)); //"llo wo"(记忆:相当于数学上[2,8))
console.log(str.substring(2,8)); //"llo wo"(记忆:相当于数学上[2,8))
console.log(str.substr(2, 8)); //"llo worl(记忆:相当于数学上[2,8+2)即[n,n+m))
console.log(str) // "hello world"(原字符串没有发生改变)

[n,m)的意思是包含前面n的不含后面m的区域。即 n≤ x<m 只传一个参数而且参数是正数的情况下,三者的调用返回的结果是一样的。 不过相信我,substr()的第二个参数要传递返回字符串个数,是很难记住的,要用的时候,直接控制台试一下就行了。实在要记忆的话:因为是String类型,所以substring()是正常传递两个索引值,而substr()是变异的特殊的,所以第二个参数传递的是返回字符串的个数。 数组的splice()方法第二个参数也是个数,要删除的个数。 再看几个substr()的例子

var str = "hello world";  
console.log(str.substr(2, 0));  // "" (相当于数学上[2,2),当然返回空字符串)
console.log(str.substr(2, -1)); // ""(相当于数学上[2,1),当然返回空字符串)
console.log(str.substr(2, 1)); // "l"(相当于数学上[2,3))
console.log(str.substr(2, 1000));  // "llo world"(相当于数学上[2,1003))
console.log(str.substr(-2, 1)); // "l"(相当于数学上[-2+11,-2+11+1)即[9,10),也即是[n+str.length, n+str.length+m))

咋看,好像slice()和substring()没啥区别

var str = "hello world";  
console.log(str.slice(-2)); // "ld"(相当于数学上[-2+11 , ∞),即[9,∞),也即是[n+str.length, ∞))
console.log(str.slice(-2, -1)); // "l"(相当于数学上[-2+11 , -1 + 11),即[910),也即是[n+str.length, m+str.length))
console.log(str.slice(-2, 1));  // “”(相当于数学上[-2+11 , 1),即[91),也即是[n+str.length, 1),当然返回空字符串)
console.log(str.substring(-2)); // "hello world"(把所有负数参数转换成0,相当于str.substring(0))
console.log(str.substring(-2, -1)); // ""(把所有负数参数转换成0,相当于str.substring(0 , 0))

弄明白这些以后,怎么记忆这些东西呢? slice()方法Array类型和String类型都具有该方法。所以,记忆:正统排序,slice()>substring()>substr() 越不正统,越怪异。所以slice()总能找到合乎数学理解的方式输出字符串,substring()负数时参数转换为0,substr()第二个参数时个数。 当然这只是为了记忆方便才区分谁更正统而已,js从来没这种说法。 3、字符串位置方法 查找字符串的方法:indexOf()和lastIndexOf()方法

var stringValue = "hello world"; 
console.log(stringValue.indexOf("o")); //4
console.log(stringValue.lastIndexOf("o")); //7

第二个参数指定开始搜索的位置,当然是个索引值

var stringValue = "hello world";
console.log(stringValue.indexOf("o", 6)); //7
console.log(stringValue.lastIndexOf("o", 6)); //4

4、trim()方法 trim()方法删除前缀和后缀的所有空格,注意中间的空格不会删除

var str = " hello world "; 
var trimmedStr = str.trim(); 
console.log(str); //" hello world "
console.log(trimmedStr); //"hello world"

5、字符串大小写转换方法 toLowerCase()、toLocaleLowerCase()、toUpperCase()和 toLocaleUpperCase()

var stringValue = "hello world"; 
console.log(stringValue.toLocaleUpperCase()); //"HELLO WORLD"
console.log(stringValue.toUpperCase()); //"HELLO WORLD"
console.log(stringValue.toLocaleLowerCase()); //"hello world"
console.log(stringValue.toLowerCase()); //"hello world"

6、字符串的模式匹配方法 match()方法:本质上与调用RegExp的exec()方法相同,只接受一个参数,要么是正则表达式,要么是RegExp对象。 记忆:RegExp的exec()都有两个e

var text = "cat, bat, sat, fat"; 
var pattern = /.at/; 
//与 pattern.exec(text)相同
var matches = text.match(pattern); 
console.log(matches.index); //0
console.log(matches); // [0:"cat", index: 0, input: "cat, bat, sat, fat", groups: undefined](返回的内容和RegExp的exec()方法相同,有input属性)
console.log(matches[0]); //"cat"
console.log(pattern.lastIndex); //0

输出所有匹配的项

var text = "cat, bat, sat, fat"; 
var pattern = /.at/g; // 记得设置为全局模式
//与 pattern.exec(text)相同
var matches = text.match(pattern); 
console.log(matches.index); //0
console.log(matches); // ["cat", "bat", "sat", "fat"]
console.log(matches[0]); //"cat"
console.log(pattern.lastIndex); //0

全局匹配的时候只返回一个数组,而RegExp类型调用exec()返回的是一个个对象,必须使用循坏遍历的方法去一个个输出

var text = "cat, bat, sat, fat"; 
var pattern1 = /.at/g;   
while ((matches = pattern1.exec(text)) != null)  {
    console.log(matches.index); //0
    console.log(matches[0]); //cat
    console.log(pattern1.lastIndex); //3
}
// 0
// cat
// 3
// 5
// bat
// 8
// 10
// sat
// 13
// 15
// fat
// 18

所以你想一下子获取全部匹配的项当然还是字符串match()方法比较合适。 search()方法: 接收一个参数,字符串或者RegExp对象指定的一个正则表达式。返回字符串中第一个匹配项的索引

var text = "cat, bat, sat, fat"; 
var index = text.search(/at/); 
var index1 = text.search('at')
var index2 = text.search('atdfafdafds')
console.log(index,index1,index2); // 1 1 -1

replace()方法:第一个参数是RegExp 对象或者一个字符串,第二个参数是一个字符串或者一个函数

var text = "cat, bat, sat, fat"; 
var result = text.replace("at", "ond"); 
console.log(result); //"cond, bat, sat, fat"
result = text.replace(/at/g, "ond"); 
console.log(result); //"cond, bond, sond, fond"

看看第二个参数是函数的情况

var i = 0;
var result = text.replace(/at/g, function(pattern){
    console.log(pattern); // 输出 "at"
    return i++
}); 
console.log(result); // "c0, b1, s2, f3"

可以实现更精准的替换操作,一般用于过滤输入的文本中的危险符号,如:<>

var text = "cat, bat, sat, fat"; 
var result = text.replace(/[bcf]at/g, function(pattern){
    switch(pattern){
        case "bat": return "bad";
        case "cat": return "call";
        case "fat": return "full";
    }
}); 
console.log(result); // call, bad, sat, full

如果第二个参数是字符串,那么就可以使用一些特殊的字符序列

字符序列 替代文本
$$ $
$& 匹配整个模式的子字符串。
$' 匹配子字符串之前的子字符串
$` 匹配子字符串之后的子字符串
$n 匹配第n个捕获的子字符串
$nn 匹配第nn个捕获的子字符串

通过这些特殊的字符序列,可以使用最近一次匹配结果中的内容

var text = "cat, bat, sat, fat"; 
result = text.replace(/(.at)/g, "这个匹配的($1)");  // ()表示匹配并获得该匹配
console.log(result); // "这个匹配的(cat), 这个匹配的(bat), 这个匹配的(sat), 这个匹配的(fat)"

更多的例子

var text = "cat, bat, sat, fat"; 
console.log(text.replace(/(.at)/g, "匹配项($$)"));  // "匹配项($), 匹配项($), 匹配项($), 匹配项($)"($$转换为$)
console.log(text.replace(/(.at)/g, "匹配项($&)")); // "匹配项(cat), 匹配项(bat), 匹配项(sat), 匹配项(fat)"
console.log(text.replace(/(.at)/g, "匹配项($`)"));  // "匹配项(), 匹配项(cat, ), 匹配项(cat, bat, ), 匹配项(cat, bat, sat, )"
console.log(text.replace(/(.at)/g, "匹配项($1)"));  // "匹配项(cat), 匹配项(bat), 匹配项(sat), 匹配项(fat)"
console.log(text.replace(/(.at)(,)/g, "匹配项($2)"));  // "匹配项(,) 匹配项(,) 匹配项(,) fat"(匹配的是第二个括号的内容)

split() 方法:接收两个参数,一个是分隔符,可以是字符串也可以是RegExp对象;第二个参数返回数组的长度。

var colorText = "red,blue,green,yellow"; 
var colors1 = colorText.split(","); 
var colors2 = colorText.split(",", 2); 
var colors3 = colorText.split(/[^\,]+/); 
console.log(colors1) //["red", "blue", "green", "yellow"]
console.log(colors2) //["red", "blue"]
console.log(colors3) //["", ",", ",", ",", ""]

7、lcoaleCompare()方法:比较两个字符串,返回负数一般是-1,返回0,返回正数一般是1

var str = 'abc';
console.log(str.localeCompare('abd')) // -1
console.log(str.localeCompare('abb')) // 1
console.log(str.localeCompare('abc')) // 0

8、fromCharCode()方法

console.log(String.fromCharCode(104, 101, 108, 108, 111)); //"hello"

9、 html方法 略

5.7.1 Global对象

所有在全局作用域中定义的属性和函数,都是 Global 对象的属性,例如:isNaN()、isFinite()、parseInt()以及 parseFloat() 1、URI编码方法 encodeURI()不会对本身属于 URI 的特殊字符进行编码,例如冒号、正斜杠、问号和井字号;而 encodeURIComponent()则会对它发现的任何非标准字符进行编码。

var uri = "http://www.wrox.com/illegal value.htm#start"; 
//"http://www.wrox.com/illegal%20value.htm#start"
console.log(encodeURI(uri)); 
//"http%3A%2F%2Fwww.wrox.com%2Fillegal%20value.htm%23start"
console.log(encodeURIComponent(uri));
var uri = "http%3A%2F%2Fwww.wrox.com%2Fillegal%20value.htm%23start"; 
//http%3A%2F%2Fwww.wrox.com%2Fillegal value.htm%23start
console.log(decodeURI(uri)); 
//http://www.wrox.com/illegal value.htm#start
console.log(decodeURIComponent(uri));

禁止使用escape()和unescape() 一般用于防止中文乱码

var uri = "http://www.wrox.com/illegal value.htm?job=程序员"; 
// "http://www.wrox.com/illegal%20value.htm?job=%E7%A8%8B%E5%BA%8F%E5%91%98"
console.log(encodeURI(uri));  
// "http%3A%2F%2Fwww.wrox.com%2Fillegal%20value.htm%3Fjob%3D%E7%A8%8B%E5%BA%8F%E5%91%98"
console.log(encodeURIComponent(uri)); 
console.log(decodeURI(encodeURI(uri)));   // "http://www.wrox.com/illegal value.htm?job=程序员"
console.log(decodeURIComponent(encodeURIComponent(uri)));  // "http://www.wrox.com/illegal value.htm?job=程序员"

2、eval()方法

eval("console.log('随便说点什么')"); // 输出 "随便说点什么"
eval("var num = 1");
console.log(num) // 1

相当于console.log('随便说点什么') 不会有变量提升

console.log(num) // 报错 Uncaught ReferenceError: num is not defined
eval("var num = 1"); 

有变量提升的情况

console.log(num) // 输出 undefined
var num = 1;

严格模式下可以使用,但是无法访问里面定义的变量

"use strict"
eval("console.log('随便说点什么')"); // 输出 "随便说点什么"
"use strict"
eval("var num = 1");
console.log(num) // 报错 Uncaught ReferenceError: num is not defined

严格模式下对eval()赋值也会报错

"use strict"
eval = 1 // 报错 Uncaught SyntaxError: Unexpected eval or arguments in strict mode

3、Global 对象的属性 书上指出:即使非严格模式下给undefined和NaN和Infinity赋值,也会报错。试了下,并不会。反而给null赋值就会报错

undefined = 1;
NaN = 1;
Infinity = 1;
console.log(undefined,NaN,Infinity) // undefined NaN Infinity
null = 1;
console.log(null) // 报错 Uncaught ReferenceError: Invalid left-hand side in assignment

只是严格模式下,会报错

"use strict"
Infinity = 1;
console.log(Infinity) // 报错 Uncaught TypeError: Cannot assign to read only property 'Infinity' of object '#<Window>'

其他一样就不一样列举了 其他略 4、window对象 Web浏览器全局对象为window对象的一部分。

var color = "green";
function sayColor(){
    console.log(window.color)
}
console.log(window.sayColor()) // "green"

5.7.2 Math对象 1、Math对象的属性 略 2、min()和max()

var max = Math.max(3, 54, 32, 16); 
console.log(max); //54
var min = Math.min(3, 54, 32, 16); 
console.log(min); //3

对于数组可以这样子找出最大值

var values = [1, 2, 3, 4, 5, 6, 7, 8]; 
var max = Math.max.apply(null, values);
console.log(max)

没有涉及this时,可以传null,这里相当于仅仅是通过apply来把参数换成数组 3、Math.ceil()、Math.floor()和 Math.round() Math.ceil():进一法 Math.floor():去尾法 Math.round():标准四舍五入

Math.ceil(25.5) // 26
Math.floor(25.5) // 25
Math.round(25.5) // 26

4、random()方法 值 = Math.floor(Math.random() * 可能值的总数 + 第一个可能的值) 例如10-15的一个可能的值

var value = Math.floor(Math.random() * 6 + 10);
console.log(value)

5、其他方法 略

第六章 面向对象的程序设计

6.1 理解对象

创建对象 方法一:创建Object实例

var person = new Object();
person.name = "lzw";
person.sayName = function (){
    console.log(this.name)
}
console.log(person) // {name: "lzw", sayName: ƒ}

方法二: 对象字面量

var person = {
    name: 'lzw',
    sayName: function(){
        console.log(this.name)
    }
}
console.log(person) // {name: "lzw", sayName: ƒ}

6.1.1 属性类型

1、数据属性 属性的默认特性: [[Configurable]]:是否可以delete删除,是否可以修改属性的特性,能否把属性修改为访问器属性 [[Enumerable]]:是否可以通过for-in枚举 [[Writable]]:是否可以修改属性 [[Value]]:包含这个属性的数据值 var person = { name: 'lzw' } [[Configurable]]、[[Enumerable]]和[[Writable]]特性都被设置为 true,而[[Value]]特性被设置为指定的值。 要修改属性的默认特性,必须要用Object.defineProperty()方法,接收三个参数:属性所在的对象、属性的名字和一个描述符对象(即

configurable、enumerable、writable 和 value)
var person = {
    name: 'lzw'
}
Object.defineProperty(person,"name",{
    configurable: false,
    value: 'leezw'
})
delete person.name
console.log(person.name) // 输出 "leezw"(输出的是Object.defineProperty里面的定义的value值,且不可删除)
var person = {
    name: 'lzw'
}
Object.defineProperty(person,"name",{
    configurable: false,
    value: 'leezw'
})
person.name = "lizw"
console.log(person.name) // 输出 "lizw"

但是却可以修改 严格模式下,设置configurable为false后delete报错

"use strict"
var person = {
    name: 'lzw'
}
Object.defineProperty(person,"name",{
    configurable: false,
    value: 'leezw'
})
delete person.name // 报错 Uncaught TypeError: Cannot delete property 'name' of #<Object>
console.log(person.name) 

设置configurable为false后不可逆转,再设置为true会报错

var person = {
    name: 'lzw'
}
Object.defineProperty(person,"name",{
    configurable: false,
    value: 'leezw'
})
Object.defineProperty(person,"name",{
    configurable: true,
    value: 'leezw'
}) 
// 报错 Uncaught TypeError: Cannot redefine property: name

下面看看writable特性

var person = {
    name: 'lzw'
}
Object.defineProperty(person,"name",{
    writable: false,
    value: 'leezw'
})
person.name = "lizw"
console.log(person.name) // 输出 'leezw'(无法修改属性值了)

2、访问器属性 包含setter和getter函数 [[Get]]:在读取属性时调用的函数。默认值为 undefined。 [[Set]]:在写入属性时调用的函数。默认值为 undefined。 同样必须使用 Object.defineProperty()来定义

var person = {
    height: 1.52
}
Object.defineProperty(person,"height",{
    get: function(){
        console.log('再怎么看都是一米五的啦')
        return 1.5
    },
    set: function(newValue){
        console.log('别改了长不高的啦')
        return
    }
})
console.log(person.height) // 输出 '再怎么看都是一米五的啦' 1.5
person.height = 1.8 // 输出 '别改了长不高的啦'
console.log(person.height) // 输出 '再怎么看都是一米五的啦' 1.5

6.1.2 定义多个属性

上面提到的定义单个属性使用Object.defineProperty(),但是有多个属性时,岂不是要调用多次这个函数? 所以有了Object.defineProperties() 方法

var person = {
    name: 'lzw',
    age: 18,
    _hobby: 'play'
}
Object.defineProperties(person,{
    name: {
        value: 'leezw'
    },
    age: {
        writable: false
    },
    hobby: {
        get: function(){
            console.log('啊哈就是喜欢玩')
            return this._hobby // 直接访问this.hobby会导致死循环,所以使用_hobby
        },
        set: function(){
            console.log('不要试图改变我,还是喜欢玩')
            return 
        }
    }
})
console.log(person.name) // "leezw'(使用的是Object.defineProperty()定义的value)
person.age = 17; 
console.log(person.age) // 18(无法修改属性值)
person.hobby = 'work'; 
console.log(person.hobby) // '不要试图改变我,还是喜欢玩' 'play'(访问该属性值会调用get函数)

6.1.3 读取属性的特性

Object.getOwnPropertyDescriptor()方法接收两个参数:属性所在的对象和要读取其描述符的属性名称

var person = {
    age: 18,
}
Object.defineProperties(person,{
    age: {
        writable: false
    },
})
var descriptor = Object.getOwnPropertyDescriptor(person,'age')
console.log(descriptor.value,descriptor.writable,descriptor.get,descriptor.set,descriptor.configurable,descriptor.enumerable)
// 18 false undefined undefined true true

6.2 创建对象

6.2.1 工厂模式

js无法创建类,开发人员就发明了一种封装以特定接口创建对象的函数。

function createPerson(name, age, job){ 
 var o = new Object(); 
 o.name = name; 
 o.age = age; 
 o.job = job; 
 o.sayName = function(){ 
     console.log(this.name); 
 }; 
 return o; 
}
var person1 = createPerson('lzw',18,'worker')
var person2 = createPerson('mht',18,'boss')
console.log(person1,person2) 
// {name: "lzw", age: 18, job: "worker", sayName: ƒ} {name: "mht", age: 18, job: "boss", sayName: ƒ}
console.log(person1 instanceof createPerson) // false

调用工厂函数创建了多个对象。工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。 它们的constructor都指向Object(),而且不是createPerson的实例。

6.2.2 构造函数模式(最常用)

function Person(name, age, job){ 
 this.name = name; 
 this.age = age; 
 this.job = job; 
 this.sayName = function(){ 
     console.log(this.name); 
 }; 
}
var person1 = new Person('lzw',18,'worker')
var person2 = new Person('mht',18,'boss')
console.log(person1,person2) 
// {name: "lzw", age: 18, job: "worker", sayName: ƒ} {name: "mht", age: 18, job: "boss", sayName: ƒ}(可以创建两个类似的对象)
console.log(person1.constructor) // ƒ Person(name, age, job){...}(可以找到构造对象的函数)
console.log(person1 instanceof Person) // true(可以找到实例)

优点:没有显式创建对象,方法和属性赋给了对象,没有return语句 构造函数首字母大写是惯例。 先回到最根本的问题,为什么使用new操作符可以创建一个新对象?

var b,c
function a (){
    b = 1;
    c = 2;
};
var obj = new a();
console.log(obj) // {}
console.log(b,c) // 1 2(函数里面的代码被执行了)

随便一个函数通过new 操作符都能返回一个对象。在代码底层上,它是经历了下面这些步骤: (1) 创建一个新对象; (2) 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象); (3) 执行构造函数中的代码(为这个新对象添加属性); (4) 返回新对象。 构造函数创建对象的强大生命力来自于new操作符,new操作符使this指向正确得以添加属性和方法,使一个被修改的对象得以返回和使用。

function Person(name, age, job){ 
 this.name = name; 
 this.age = age; 
 this.job = job; 
 this.sayName = function(){ 
     console.log(this.name); 
 }; 
}
var person1 = Person('lzw',18,'worker')
var person2 = Person('mht',18,'boss')
console.log(person1,person2) // undefined undefined(失去new操作符的构造函数直接调用不会返回对象)

1、将构造函数当作函数

function Person(name, age, job){ 
 this.name = name; 
 this.age = age; 
 this.job = job; 
 this.sayName = function(){ 
     console.log(this.name); 
 }; 
}
var person1 = Person('lzw',18,'worker')
console.log(name) // 'lzw'(实际上函数直接执行this指向window)
var person2 = Person('mht',18,'boss')
console.log(name) // 'mht'

new操作符和构造函数搭配,是为了新建对象,将this正确指向后修改并返回这个对象,万一我原本就有一个对象,只是想修改,不用新建呢?

var obj = {hobby: 'play'}
function Person(name, age, job){ 
 this.name = name; 
 this.age = age; 
 this.job = job; 
 this.sayName = function(){ 
     console.log(this.name); 
 }; 
}
Person.call(obj, 'lzw',18,'worker')
console.log(obj.name,obj.hobby) // 'lzw' 'play'

实际上就是obj代替了new新建的对象, 但是构造函数是为了创建出大量类似的对象,用来修饰原有对象是不是有点 “不务正业”。 2.构造函数的问题

function Person(name, age, job){ 
 this.name = name; 
 this.age = age; 
 this.job = job; 
 this.sayName = function(){ 
     console.log(this.name); 
 }; 
}
var person1 = new Person('lzw',18,'worker')
var person2 = new Person('mht',18,'boss')
console.log(person1.sayName == person2.sayName) // false

person1 和 person2 都有一个名为 sayName()的方法,但那两个方法不是同一个 Function 的实例。即一直在重复创建新的 Function 实例

function Person(name, age, job){ 
 this.name = name; 
 this.age = age; 
 this.job = job; 
 this.sayName = sayName 
}
function sayName(){ //方法定义在外部
     console.log(this.name); 
 };
var person1 = new Person('lzw',18,'worker')
var person2 = new Person('mht',18,'boss')
console.log(person1.sayName == person2.sayName) // true

需要把方法定义在外部,但是这样又导致对象调用的方法,老是定义在了全局作用域中,每多一个方法,就要在全局作用域中创建多一个函数 缺点: 要么一直重复创建新的Function实例,要么一直在全局作用域新写一个或多个函数导致没有封装性可言,所以有了原型模式!

6.2.3 原型模式

function Person(){ 
} 
Person.prototype.name = "lzw"; 
Person.prototype.age = 18; 
Person.prototype.job = "worker"; 
Person.prototype.sayName = function(){  // 优点:方法写进了prototype中,看起来封装性好多了
 console.log(this.name); 
}; 
var person1 = new Person(); 
person1.sayName(); //"lzw"
var person2 = new Person();
person2.sayName(); //"lzw"(缺点:可是共享了同样的属性,且不可传参设定属性)
console.log(person1.sayName == person2.sayName); //true(优点:共享了同样的方法)

1、理解原型对象 我们把上面的person1打印出来看看,是这个样子

Person {}
    __proto__:
        age: 18
        job: "worker"
        name: "lzw"
        sayName: ƒ ()
        constructor: ƒ Person()
        __proto__: Object

person1是一个空对象,但是prototype中含有添加的属性和方法(谷歌浏览器中可以通过proto__属性查看 )。 可以看到生成的空对象即**person1.__proto.constructor指向Person函数person1.__ proto__.__proto__指向Object函数** 而person1的__proto__属性(实例的的__proto__属性)指向Person函数的prototype(构造函数的原型) 初看真的是非常绕,还是眼见为实 首先看看prototype是怎么来的

function a(){};
console.log(a.prototype) 
// 输出一个对象{constructor: ƒ}
// constructor: ƒ a()
// __proto__: Object

所以只要新建一个函数,就会自动的添加一个prototype属性,它的值是一个对象,里面有个constructor指向函数本身 其次每个实例上都有一个[[Prototype]]指针,谷歌浏览器可以通过.__proto__访问,它指向构造函数的原型

function Person(){ 
} 
Person.prototype.name = "lzw"; 
Person.prototype.age = 18; 
Person.prototype.job = "worker"; 
Person.prototype.sayName = function(){ 
 console.log(this.name); 
}; 
var person1 = new Person(); 
person1.sayName(); //"lzw"
console.log(person1.__proto__ === Person.prototype); // true(实例person1的__proto__属性指向Person构造函数的prototype即构造函数的原型上)
console.log(person1.__proto__.constructor == Person) // true
console.log(Person.prototype.constructor == Person) // true(函数的prototype对象的constructor属性指向Person)
console.log(person1.__proto__.__proto__ == Object.prototype) // true
console.log(Person.prototype.__proto__ == Object.prototype) // true(Person.prototype是Object构造函数的实例)
console.log(Person.__proto__.__proto__ == Object.prototype) // true
console.log(Person.__proto__.__proto__.constructor == Object) // true
console.log(Person.__proto__.__proto__.__proto__) // null(沿着原型链一直往上溯源直到指向null)

上面的例子一开始非常难懂,但是只要记住person1.__proto__ === Person.prototype,在控制台打印一下person1,同时了解Person.prototype作为一个对象是Object构造函数的实例,然后其他都可以推导出来 再次强调记住两个要点:

  1. 每个实例上都有一个[[Prototype]]指针,谷歌浏览器可以通过.__proto__访问,它指向构造函数的原型,对应例子中:person1.__proto__ === Person.prototype
  2. 只要新建一个函数,就会自动的添加一个prototype属性,它的值是一个对象,里面有个constructor指向函数本身,对应例子中·: Person.prototype.constructor == Person 但是__proto__属性并不是每个浏览器都一定支持,所以可以通过isPrototypeOf()和Object.getPrototypeOf()方法来更加规范判断
    console.log(Person.prototype.isPrototypeOf(person1)) // true
    console.log(Object.getPrototypeOf(person1) === Person.prototype) // true

重点:每次查找一个属性都会在原型链上一级一级查找,找到即返回,直至最后指向null,所以把属性设置在原型上时,即使空对象也可以找到这个属性 实例中创建的属性会覆盖原型链上,因为已经在实例自身找到了就不一级一级往上查找了。

function Person(){ 
} 
Person.prototype.name = "lzw"; 
Person.prototype.age = 18; 
Person.prototype.job = "worker"; 
Person.prototype.sayName = function(){ 
 console.log(this.name); 
}; 
var person1 = new Person(); 
var person2 = new Person();
person1.name = 'leezw'
console.log(person1.name) // "leezw"(来自实例)
console.log(person1.__proto__.name) // "lzw"(原型上的值是这个)
console.log(person2.name) // "lzw"(来自原型的值)
console.log(person1.__proto__.name) // "lzw"
console.log(person1.hasOwnProperty('name')) // true(存在实例中)
console.log(person2.hasOwnProperty('name')) // false(存在原型中)
console.log(Object.getOwnPropertyDescriptor(person1,'name')) // {value: "leezw", writable: true, enumerable: true, configurable: true}
console.log(Object.getOwnPropertyDescriptor(person2,'name')) // undefined(无法对原型上的使用)

使用 hasOwnProperty()方法可以检测一个属性是存在于实例中,还是存在于原型中 Object.getOwnPropertyDescriptor()方法只能用于实例属性,看名字就知道 2、原型与in操作符 in操作符的两种方式

  1. 单独使用
  2. for-in搭配使用
function Person(){ 
} 
Person.prototype.name = "lzw"; 
Person.prototype.age = 18; 
Person.prototype.job = "worker"; 
Person.prototype.sayName = function(){ 
 console.log(this.name); 
}; 
var person1 = new Person(); 
var person2 = new Person();
console.log(person1.hasOwnProperty('name')) // false(原型中有name值)
console.log("name" in person1) // true
person1.name = "leezw"
console.log(person1.hasOwnProperty('name')) // true(实例中有name值)
console.log("name" in person1) // true
console.log("hobby" in person1) // false(实力和原型上都找不到,返回false)

无论是实例中还是原型中,都in操作符都会返回true,不存在当然返回false 所以如果原型上没有但是in操作符又返回true,就可以断定一定是原型上的属性。

function hasPrototypeProperty(object, name){
    return !object.hasOwnProperty(name) && (name in object);
}
hasPrototypeProperty(person2,"name") // true(原型上的属性)

for-in循环包括在实例中的属性和原型上的属性,毕竟无论是实例中还是原型中,都in操作符都会返回true,但是屏蔽了[[Enumerable]]标记为 false 的属性

function Person(){ 
} 
Person.prototype.name = "lzw"; 
Person.prototype.age = 18; 
var person1 = new Person(); 
person1.hobby = 'play';
person1.height = 1.8;
for(var prop in person1){
    console.log(person1[prop]) // play 1.8 lzw 18(原型和实例上的属性都被输出)
}
Object.defineProperty(person1,'height',{enumerable: false})
for(var prop in person1){
    console.log(person1[prop]) // play lzw 18(不可枚举的height已经不见了)
}
Object.defineProperty(person1,'name',{enumerable: false})
for(var prop in person1){
    console.log(person1[prop]) // play 18(原型上不可枚举的name已经不见了)
}

使用Object.keys()方法来取得对象上的可枚举实例属性

function Person(){ 
} 
Person.prototype.name = "lzw"; 
Person.prototype.age = 18; 
Person.prototype.job = "worker"; 
Person.prototype.sayName = function(){ 
 console.log(this.name); 
}; 
var person1 = new Person(); 
person1.hobby = 'play';
var keys = Object.keys(person1)
console.log(keys) // ["hobby"] (只包含该实例的可枚举key)
var Anotherkeys = Object.keys(Person.prototype) 
console.log(Anotherkeys) // ["name", "age", "job", "sayName"](只包含该实例的可枚举key)
Object.defineProperties(person1,{hobby:{enumerable: false}})
keys = Object.keys(person1) 
console.log(keys) // [](hobby不可枚举后去除了)
keys = Object.getOwnPropertyNames(person1)
console.log(keys) // ["hobby"] (不可枚举的属性又出现了,Object.getOwnPropertyNames()返回所有实例属性,不论是否可以枚举)

吐槽:这也太难记忆了吧,一般只获取实例上的东西加个ownproperty比较好吧 有没有可以返回全部属性的呢,有,使用Object.getOwnPropertyNames() Object.keys()和 Object.getOwnPropertyNames()方法都可以用来替代 for-in 循环 3、更简单的原型语法

function Person(){ 
} 
Person.prototype = { 
 name : "lzw", 
 age : 18, 
 job: "worker", 
 sayName : function () { 
     console.log(this.name); 
 } 
};
var person = new Person();
console.log(person instanceof Object) // true
console.log(person instanceof Person) // true
console.log(person.constructor == Person) // false(缺点:因为赋值的原因,constructor不再指向Person)
console.log(person.constructor == Object) // true

如果 constructor 的值真的很重要,可以像下面这样特意将它设置回适当的值。设 constructor 属性会导致它的[[Enumerable]]特性被设置为 true,所以手动设置为false

function Person(){ 
} 
Person.prototype = { 
 constructor: Person, //设置constructor的指向正确
 name : "lzw", 
 age : 18, 
 job: "worker", 
 sayName : function () { 
     console.log(this.name); 
 } 
};
// 手动设置[[Enumerable]]特性为false
Object.defineProperty(Person.prototype,'constructor',{
    enumerable: false,
    value: Person,
})
var person = new Person();

简直麻烦得不要不要的。 4、原型的动态性 实例中的指针仅指向原型,而不指向构造函数。 先创建实例在修改原型,原型上的修改也会马上反映到原型对象中。因为实例与原型的连接仅仅是一个指针。

function Person(){ 
} 
var person = new Person()
Person.prototype.sayHi = function(){
    console.log("hi")
}
person.sayHi() // 'hi'

所以可以随便的动态添加相关的属性和方法 重写原型带来的危害:失去动态性

function Person(){ 
} 
var person = new Person()
Person.prototype = {
    sayHi: function(){
        console.log("hi")
    }
}
console.log(person.__proto__) // {constructor: ƒ}(并没有sayHi方法)
person.sayHi() // 报错

因为实例中的指针并没有改变,还是指向旧的对象,那应该怎么改呢?在谷歌浏览器下

function Person(){ 
} 
var person = new Person()
Person.prototype = {
    sayHi: function(){
        console.log("hi")
    }
}
person.__proto__ = Person.prototype //设置实例指针的正确指向
person.sayHi() // "hi"

重写原型的问题太多了,要正确指向contructor,要再实例前完成构造函数的原型的重写,如果可以的话还是使用一开始添加属性的方式吧 5、原生对象的原型 所有原生的引用类型(Object、Array、String)都采用原型模式创建的

console.log(Array) // ƒ Array() { [native code] }(这里明明没有看见contact等方法)
console.log(Array.prototype) // [constructor: ƒ, concat: ƒ, copyWithin: ƒ, fill: ƒ, find: ƒ, …](原来是在原型上)

你也可以自己添加一些方法

Array.prototype.first = function(){// 添加first组件
    return this[0]
}
var arr = [1,2,3,4] 
console.log(arr.first()) // 1

再例如旧版IE不支持find方法,可以判断有没有该方法,没有就添加。其他情况不建议修改原生对象的原型 6、原型对象的问题 缺点: 没有传参初始化,所有实例共享

function Person(){ 
} 
Person.prototype.name = "lzw"; 
Person.prototype.age = 18; 
Person.prototype.job = "worker"; 
Person.prototype.sayName = function(){  // 优点:方法写进了prototype中,看起来封装性好多了
 console.log(this.name); 
}; 
var person1 = new Person(); 
person1.sayName(); //"lzw"
var person2 = new Person();
person2.sayName(); //"lzw"(缺点:可是共享了同样的属性,且不可传参设定属性)
console.log(person1.sayName == person2.sayName); //true(优点:共享了同样的方法)
console.log(person1.name == person2.name) // true(缺点:共享了所有属性)

所以要解决几个问题:

  1. 可传参初始化
  2. 共享方法,但不共享属性

6.2.4 组合使用构造函数模式和原型模式

把前面的Person函数换成可传参的构造函数,用于存储属性,而方法依然放在原型上。 为什么方法需要共享?因为一般而言,说出一个人名字这个事情都是同样的操作,没必要去每次重复实例化Function类型。当然你想让每个新建的对象都有不一样的方法,也可以采用传参方式写入构造函数中。

function Person(name,age,job){// 通过new操作符this指向要返回的对象,传入的参数得以写入新对象,实现传参功能
    this.name = name;
    this.age = age;
    this.job = job;
}
Person.prototype = {// 一定要在实例化前完成原型重写,否则会导致实例指向出现错误,访问属性和方法报错
    constructor: Person, // 记得重新赋予constructor属性,以及属性值,否则指向Object,当然如果你不使用这个属性,看起来貌似是人畜无害的
    sayName: function(){ // 方法保存在原型上公用,就如同引用类型的方法一样
        console.log(this.name)
    }
}
var person1 = new Person('lzw',18,'worker'); //这时候再实例化,指向就正确了
var person2 = new Person('my',40,'boss')
console.log(person1.name == person2.name) // false(属性不共享了)
console.log(person1.sayName == person2.sayName) // true(方法共享了)
Person.prototype.sayHi = function(){
    console.log('hi,' + this.name)
}
person1.sayHi() // "hi,lzw"(可以继续添加属性)

构造函数较为常用,而组合使用构造函数模式和原型模式几乎是最优解。

6.2.5 动态原型模式

function Person(name, age, job){ // 可传参
     //属性(存储在对象中)
     this.name = name; 
     this.age = age; 
     this.job = job;//方法
     if (typeof this.sayName != "function"){ // 只在 sayName()方法不存在的情况下,才会将它添加到原型中,只在第一次执行
         Person.prototype.sayName = function(){ //方法存储在原型上
            console.log(this.name); 
         }; 
     } 
} 
var person = new Person("lzw", 18, "worker"); 
person.sayName(); // "lzw"

这里对原型所做的修改,能够立即在所有实例中得到反映。同时只执行了一次。之所以能够这样是因为,只是在原型上添加了属性,而不是使用字面量重写属性,一旦重写属性就失去了动态性。参考6.2.3 原型模式的第四点,因为实例还是指向旧的内存地址。 头铁再试一下

function Person(name, age, job){ // 可传参
     //属性(存储在对象中)
     this.name = name; 
     this.age = age; 
     this.job = job;//方法
     if (typeof this.sayName != "function"){ // 只在 sayName()方法不存在的情况下,才会将它添加到原型中,只在第一次实例化执行
        Person.prototype = {
            sayName: function(){ //方法存储在原型上
                console.log(this.name); 
             }
        }
     } 
} 
var person = new Person("lzw", 18, "worker"); 
person.sayName(); // 报错

如何头铁的去修复它,在支持__proto__的浏览器下

打印person发现它的__proto__指向的是Object
function Person(name, age, job){ // 可传参
     //属性(存储在对象中)
     this.name = name; 
     this.age = age; 
     this.job = job;//方法
     if (typeof this.sayName != "function"){ // 只在 sayName()方法不存在的情况下,才会将它添加到原型中,只在第一次执行
        Person.prototype = {
            sayName: function(){ //方法存储在原型上
                console.log(this.name); 
             }
        }
     } 
} 
var person = new Person("lzw", 18, "worker"); 
person.__proto__ = Person.prototype; //实例指向不会自动更新,在实例化的时候已经确定了指向,所以需要手动更新
person.sayName(); // 'lzw'

所以还是不要用字面量重写原型,自己挖坑自己填坑的,而且这个坑还必须要支持__proto__的属性才能填

6.2.6 寄生构造函数模式

function Person(name, age, job){ 
    var o = new Object(); 
    o.name = name; 
    o.age = age; 
    o.job = job; 
    o.sayName = function(){ 
        console.log(this.name); 
    }; 
    return o; 
} 
var person = new Person("lzw", 18, "worker"); 
person.sayName(); //"lzw"
console.log(person instanceof Person) // false(无法确定是谁的实例)

new操作符本来会自动创建和返回一个对象的,但是在这里,直接自己创建并返回了对象,使用return语句使new操作符没来得及返回新创建的对象,就停止执行函数了。

function Person(name, age, job){ 
    var o = new Object(); 
    o.name = name; 
    o.age = age; 
    o.job = job; 
    o.sayName = function(){ 
        console.log(this.name); 
    }; 
    //return o; // 去掉return看看
} 
var person = new Person("lzw", 18, "worker"); 
console.log(person) // {}(这次返回了new操作符创建的函数)

下面我们看看去掉new操作符看看

function Person(name, age, job){ 
    var o = new Object(); 
    o.name = name; 
    o.age = age; 
    o.job = job; 
    o.sayName = function(){ 
        console.log(this.name); 
    }; 
    return o; 
} 
var person = Person("lzw", 18, "worker"); 
console.log(person) // {name: "lzw", age: 18, job: "worker", sayName: ƒ}
person.sayName() // "lzw"

既然你选择了自己返回一个对象,为什么还要使用new操作符? 经我实践发现,new操作符完全可以去掉。尽量不要使用这种模式,因为无法使用instanceof确定实例的类型

6.2.7 稳妥构造函数模式

function Person(name, age, job){
    //创建要返回的对象
    var o = new Object();

    //可以在这里定义私有变量和函数
    //添加方法
    o.sayName = function(){
        console.log(name);
    };
    //返回对象
    return o;
}
var person = Person("lzw", 18, "worker")
person.sayName() // "lzw"

除了使用 sayName()方法之外,没有其他办法访问 name 的值,所以非常的稳妥。这不就是6.2.6寄生构造函数模式去掉new操作符么?

6.3 继承

js继承是依靠原型链来实现的。

6.3.1 原型链

构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。如果原型对象等于另一个实例(另一个对象)呢?上述关系依然成立,层层递进。

function Person(name){
    this.name = name
}
var person = new Person('lzw')
console.log(person.__proto__) // {constructor: ƒ Person(name),__proto__: Object}
console.log(person.__proto__.__proto__.constructor) // ƒ Object() { [native code] }
console.log(Object.prototype) // {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}

Person构造函数的原型其实是Object()构造函数的实例,所以根据原型链继承都有着Object()原型上的方法,例如valueOf(),平时使用的每一个对象就原型链最好的例子。

function SuperType(){ 
 this.property = true; 
}
SuperType.prototype.getSuperValue = function(){ 
 return this.property; 
}; 
function SubType(){ 
 this.subproperty = false; 
} 
//继承了 SuperType
SubType.prototype = new SuperType(); 
SubType.prototype.getSubValue = function (){ // 须先继承再添加
 return this.subproperty; 
}; 
var instance = new SubType(); 
console.log(instance.getSuperValue()); //true

我们打印出来查看一下原型链,当然还是看__proto__这个属性

console.log(instance .__proto__)
//{
//  getSubValue: ƒ (),//SubType函数原型上添加的方法
//   property: true,// SubType函数原型赋值时得到SuperType实例的属性
//  __proto__: // 继承的SuperType函数原型
//    {
//      getSuperValue: ƒ (),// 继承的SuperType函数原型上的方法
//      constructor: ƒ SuperType(),
//      __proto__: Object // SuperType函数原型就是Object的实例
//    }
//  }

我的天,差点把自己看晕。

console.log(instance.__proto__.__proto__.__proto__) // {constructor: ƒ Object()} //最后一个是Object全局函数的实例
console.log(instance.__proto__.__proto__.__proto__.__proto__)  // null(原型链末端最后指向null)

1、别忘了默认原型 所有函数的默认原型都是 Object 的实例,因此默认原型都会包含一个内部指针,指向 Object.prototype,所以都可以使用 constructor、hasOwnProperty()、isPrototypeOf()、propertyIsEnumerable()toLocaleString()、toString()、valueOf() 2、确定原型和实例的关系 第一种方式是使用 instanceof 操作符

console.log(instance instanceof Object); //true
console.log(instance instanceof SuperType); //true
console.log(instance instanceof SubType); //true

第二种方式是使用 isPrototypeOf()方法

console.log(Object.prototype.isPrototypeOf(instance)); //true
console.log(SuperType.prototype.isPrototypeOf(instance)); //true
console.log(SubType.prototype.isPrototypeOf(instance)); //true

3、谨慎地定义方法

function SuperType(){ 
 this.property = true; 
} 
SuperType.prototype.getSuperValue = function(){ 
 return this.property; 
}; 
function SubType(){ 
 this.subproperty = false; 
} 
//继承了 SuperType
SubType.prototype = new SuperType(); 
//添加新方法
SubType.prototype.getSubValue = function (){ 
 return this.subproperty; 
}; 
//重写超类型中的方法
SubType.prototype.getSuperValue = function (){ 
 return false; 
}; 
var instance = new SubType(); 
console.log(instance.getSuperValue()); //false

重写并覆盖了原来的getSuperValue()方法 同样的使用字面量会出现某些麻烦

function SuperType(){ 
 this.property = true; 
} 
SuperType.prototype.getSuperValue = function(){ 
 return this.property; 
}; 
function SubType(){ 
 this.subproperty = false; 
} 
//继承了 SuperType
SubType.prototype = new SuperType(); 
//使用字面量添加新方法,会导致上一行代码无效
SubType.prototype = { 
 getSubValue : function (){ 
 return this.subproperty; 
 }, 
 someOtherMethod : function (){ 
 return false; 
 } 
}; 
var instance = new SubType(); 
console.log(instance.getSuperValue()); // 报错

4、原型链的问题

function SuperType(){ 
 this.colors = ["red", "blue", "green"];
} 
function SubType(){ 
} 
//继承了 SuperType
SubType.prototype = new SuperType(); 
var instance1 = new SubType(); 
instance1.colors.push("black"); 
console.log(instance1.colors); //["red,blue,green,black"]
var instance2 = new SubType(); 
console.log(instance2.colors); //["red,blue,green,black"](SubType 的所有实例都会共享这一个 colors 属性)

方法共享没毛病,关键是属性共享就很有问题,所以 问题一:所有实例共享原型链上的属性,一个实例修改这个属性,其他实例都会得到反映 问题二: 不能传参自定义属性

6.3.2 借用构造函数

function SuperType(){ 
 this.colors = ["red", "blue", "green"]; 
} 
function SubType(){ 
 //继承了 SuperType
 SuperType.call(this); 
} 
var instance1 = new SubType(); 
instance1.colors.push("black"); 
console.log(instance1.colors); //["red,blue,green,black"]
var instance2 = new SubType(); 
console.log(instance2.colors); //["red,blue,green"]

通过call()改变this的指向,实际上相当于直接执行以下代码

function SuperType(){ 
 this.colors = ["red", "blue", "green"]; 
} 
function SubType(){ 
 //继承了 SuperType
 this.colors = ["red", "blue", "green"];   // SuperType函数内容直接放在这里,this在使用new操作符时,指向新建的对象
} 
var instance1 = new SubType(); 
instance1.colors.push("black"); 
console.log(instance1.colors); //["red,blue,green,black"]
var instance2 = new SubType(); 
console.log(instance2.colors); //["red,blue,green"]

1、传递参数 可以在子类型构造函数中向超类型构造函数传递参数。

function SuperType(name){ 
 this.name = name; 
} 
function SubType(){ 
 //继承了 SuperType,同时还传递了参数
 SuperType.call(this, "lzw"); // 这行代码必须要放在最前面,否则父类的属性会覆盖子类的属性

 //实例属性
 this.age = 18; 
} 
var instance = new SubType(); 
console.log(instance.name); //"lzw";
console.log(instance.age); //18

2、借用构造函数的问题 问题一: 方法无法复用,构造函数每次使用new操作符,里面定义的方法都会重新创建一个新的Function实例,参考###6.2.2构造函数模式 问题二: 在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式

6.3.3 组合继承

指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。是不是感觉和###6.2.4组合使用构造函数模式和原型模式很相似?

function SuperType(name){ 
     this.name = name; 
    this.colors = ["red","green"]
} 
SuperType.prototype.sayName = function(){
    console.log(this.name)
}
function SubType(name,age){ // 要自定义属性所以要传参
     //继承了 SuperType,同时还传递了参数
     SuperType.call(this, name); 
     //实例属性
     this.age = age; 
} 
SubType.prototype = new SuperType()//仅仅是进行原型链的连接不用传参
SubType.prototype.constructor = SubType;
SuperType.prototype.sayAge = function(){
    console.log(this.age)
}
var instance1 = new SubType('lzw',18); 
instance1.sayName(); //"lzw";
instance1.sayAge(); //18
instance1.colors.push('white')
console.log(instance1.colors) // ["red", "green", "white"]
var instance2 = new SubType('my',40); 
instance2.sayName(); //"my";(共享了方法)
instance2.sayAge(); //40
console.log(instance2.colors) // ["red", "green"](不会共享属性)

SubType.prototype.constructor = SubType; 为什么需要上面这一行代码呢? 去掉这一行代码console.log(instance1.__proto__)打印出来发现没有constructor这个属性,因为直接把一个实例对象赋值给了SubType.prototype,导致该属性丢失。 你要是不使用该属性,理论上是可以不用管的,但是当别人想直接从constructor这个属性构建一个类似的对象时就会出现问题。例如现在我依然可以使用这个属性创建多一个实例

var instance3 = new SubType.prototype.constructor('mht',40);
instance3.sayName() // "mht"

你也许会想谁会干这种事情呢,这样子不是多此一举么?new SubType.prototype.constructor('mht',40)相当于new SubType('mht',40)。那再看看下面的例子

var instance4 = new instance1.__proto__.constructor('lj',35); //一个实例通过__proto__.constructor获得构造函数然后创建新实例
instance4.sayName() // "lj"

可能真的有人做这种事情。所以还是把它指向正确的内存地址吧组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式。而且,instanceof 和 isPrototypeOf()也能够用于识别基于组合继承创建的对象。

console.log(instance4 instanceof SubType) // true
console.log(instance4 instanceof SuperType) // true
console.log(SubType.prototype.isPrototypeOf(instance4)) // true
console.log(SuperType.prototype.isPrototypeOf(instance4)) // true

6.3.4 原型式继承

function object(o){
    function F(){}
    F.prototype = o;
    return new F();
}

object()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。 注意传入参数是对象,是根据已有的对象创建新对象。

function object(o){
    function F(){}
    F.prototype = o;
    return new F();
}
var person = { 
    name: "lzw", 
    friends: ["a", "b", "c"] 
}; 
var person1 = object(person); 
person1.name = "my"; 
person1.friends.push("d"); 
var person2 = object(person); 
person2.name = "mht"; 
person2.friends.push("e"); 
console.log(person.friends); // ["a", "b", "c", "d", "e"](读取操作的依然是原型上的friends数组)
console.log(person1.name,person2.name); // "my" "mht"(被实例上的name覆盖了原型上的name

实际上原型上的属性被共享了,仅仅是对传入其中的对象执行一次浅拷贝。 js已经新增了Object.create()来规范化原型式继承,接收两个参数:用作新对象原型的对象和(可选)一个为新对象定义额外属性的对象。

var person = { 
    name: "lzw", 
    friends: ["a", "b", "c"] 
}; 
var person1 = Object.create(person); 
person1.name = "my"; 
person1.friends.push("d"); 
var person2 = Object.create(person); 
person2.name = "mht"; 
person2.friends.push("e"); 
console.log(person.friends);

把person1打印出来是这个样子的

{
    name: "my", // 实例本身的属性
    __proto__:{ // 原型上的属性
        friends: (5) ["a""b""c""d""e"],
        name: "lzw",
        __proto__: Object
    }
}

第二个参数的作用

var person = { 
    name: "lzw", 
    friends: ["a", "b", "c"] 
}; 
var person1 = Object.create(person,{
    name:{value: 'leezw',enumerable: false} //name不可枚举了
}); 
var person2 = Object.create(person); 
for(var key in person1){
    console.log(key) // friends
}
for(var key in person2){
    console.log(key) // name friends
}

这种继承方式十分简单方便。

6.3.5 寄生式继承

function createAnother(original){ 
    var clone = object(original); //通过调用函数创建一个新对象
    clone.sayHi = function(){ //以某种方式来增强这个对象
        console.log("hi"); 
    }; 
    return clone; //返回这个对象
}

实际上就是在原型式继承的基础上添加自身的方法和属性

function object(o){
    function F(){}
    F.prototype = o;
    return new F();
}
function createAnother(original){ 
    var clone = object(original); //通过调用函数创建一个新对象
    clone.sayHi = function(){ //以某种方式来增强这个对象
        console.log("hi"); 
    }; 
    return clone; //返回这个对象
}
var person = { 
    name: "lzw", 
    friends: ["a", "b", "c"] 
};
var anotherPerson = createAnother(person)
anotherPerson.sayHi(); // "hi"

6.3.6 寄生组合式继承

组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。

function SuperType(name){
    this.name = name;
    this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
    alert(this.name);
};
function SubType(name, age){
    SuperType.call(this, name); //第二次调用 SuperType()
    this.age = age;
}
SubType.prototype = new SuperType(); //第一次调用 SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
    alert(this.age);
};

寄生组合式继承的基本模式如下所示。

function object(o){
    function F(){}
    F.prototype = o;
    return new F();
}
function inheritPrototype(subType, superType){ 
    var prototype = object(superType.prototype); //创建对象
    prototype.constructor = subType; //增强对象
    subType.prototype = prototype; //指定对象
}

只需要把想要继承的superType.prototype上的方法直接浅拷贝下来,而不是去重新实例化一次 寄生组合式继承全部代码如下

function object(o){
    function F(){}
    F.prototype = o;
    return new F();
}
function inheritPrototype(subType, superType){ 
    var prototype = object(superType.prototype); //创建对象
    prototype.constructor = subType; //增强对象
    subType.prototype = prototype; //指定对象
}
function SuperType(name){ 
     this.name = name; 
    this.colors = ["red","green"]
} 
SuperType.prototype.sayName = function(){
    console.log(this.name)
}
function SubType(name,age){ // 要自定义属性所以要传参
     //继承了 SuperType,同时还传递了参数
     SuperType.call(this, name); 
     //实例属性
     this.age = age; 
} 
inheritPrototype(SubType, SuperType);
SuperType.prototype.sayAge = function(){
    console.log(this.age)
}
var instance1 = new SubType('lzw',18); 
instance1.sayName(); //"lzw";
instance1.sayAge(); //18
instance1.colors.push('white')
console.log(instance1.colors) // ["red", "green", "white"]
var instance2 = new SubType('my',40); 
instance2.sayName(); //"my";(共享了方法)
instance2.sayAge(); //40

相对于组合继承实际上就是使用浅拷贝代替SuperType函数实例化

第七章 函数表达式

定义函数的方式有两种:一种式函数声明,另一种是函数表达式。 函数声明语法:

function functionName(arg0, arg1, arg2) {
//函数体
}

函数声明的重要特征:函数声明提升,在执行代码之前会先读取函数声明

sayHi() // hi(不报错,函数声明被提升了)
function sayHi(){
    console.log('hi')
}

函数表达式语法:

var functionName = function(arg0, arg1, arg2){
//函数体
};

这种情况下创建的函数叫做匿名函数(anonymous function),因为 function 关键字后面没有标识符

sayHi() // 报错 Uncaught TypeError: sayHi is not a function
var sayHi = function (){
    console.log('hi')
}

理解函数提升的关键,就是理解函数声明与函数表达式之间的区别

// 不要这样子做
if(condition){ 
 function sayHi(){ 
     console.log("Hi!"); 
 } 
} else { 
 function sayHi(){ 
     console.log("Yo!"); 
 } 
}

实际上,这在 ECMAScript 中属于无效语法,JavaScript 引擎会尝试修正错误,将其转换为合理的状态。但问题是浏览器尝试修正错误的做法并不一致

//可以这样做
var sayHi;
if(condition){
    sayHi = function(){
        console.log("Hi!");
    };
} else {
    sayHi = function(){
        console.log("Yo!");
    };
}

7.1 递归

递归:函数通过自身函数名调用自身

function factorial(num){
    if (num <= 1){
        return 1;
    } else {
        return num * factorial(num-1);
    }
}

但是可能造成某些错误。

function factorial(num){ 
     if (num <= 1){ 
        return 1; 
     } else { 
        return num * factorial(num-1); 
     } 
}
var anotherFactorial = factorial; 
factorial = null; 
console.log(anotherFactorial(4)); //报错 Uncaught TypeError: factorial is not a function

归根结底,还是因为耦合性。使用arguments.callee可以解决这个问题。

function factorial(num){ 
     if (num <= 1){ 
        return 1; 
     } else { 
        return num * arguments.callee(num-1); 
     } 
}
var anotherFactorial = factorial; 
factorial = null; 
console.log(anotherFactorial(4)); // 24

但是严格模式下,报错

"use strict"
function factorial(num){ 
     if (num <= 1){ 
        return 1; 
     } else { 
        return num * arguments.callee(num-1);  // 报错  Uncaught TypeError: 'caller', 'callee', and 'arguments' properties ...
     } 
}
var anotherFactorial = factorial; 
factorial = null; 
console.log(anotherFactorial(4));  

使用命名函数来修复

"use strict"
var factorial = (function f(num){ 
    if (num <= 1){ 
        return 1; 
    } else { 
        return num * f(num-1); 
    } 
});
var anotherFactorial = factorial; 
factorial = null; 
console.log(anotherFactorial(4)); 

虽然看上去是简单的问题复杂化,但是的确解决了问题。

7.2 闭包

闭包是指有权访问里一个函数作用域中的变量的函数。常见方式为,函数内部再创建一个函数。

function add(num1){
    return function(num2){ // 被返回的这个函数一直可以访问到num1,因为可以访问外部函数的参数值
        return num1 + num2
    }
}
var addTen = add(10);
console.log(addTen(1)) // 11(一直可以访问到num1,也就是10)
console.log(addTen(10)) // 20(一直可以访问到num1)

实际上就是返回了一个可以一直访问add参数的函数。 当某个函数被调用时,会创建一个执行环境(execution context)及相应的作用域链。然后,使用 arguments 和其他命名参数的值来初始化函数的活动对象(activation object)。但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,……直至作为作用域链终点的全局执行环境。 函数执行过程中,为了读取和写入变量值,就需要再作用域中查找变量。 作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。 无论什么时候在函数中访问一个变量时,就会从作用域链中搜索具有相应名字的变量。一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象)。但是,闭包的情况又有所不同。 上面的匿名函数返回后,作用域链包含了add()函数的活动对象和全局变量对象匿名函数返回后,add()的执行环境的作用域链会被销毁。但它的活动对象依然会保留再内存中直到匿名函数被销毁后,createComparisonFunction()的活动对象才会被销毁 解除对匿名函数的引用以释放内存,不然会导致内存泄漏。

function add(num1){
    return function(num2){
        return num1 + num2
    }
}
var addTen = add(10);
console.log(addTen(1))
console.log(addTen(10))
addTen = null

7.2.1 闭包与变量

即闭包只能取得包含函数中任何变量的最后一个值。别忘了闭包所保存的是整个变量对象,而不是某个特殊的变量。

function createFunctions(){ 
    var result = new Array(); 
    for (var i=0; i < 10; i++){ 
         result[i] = function(){ 
            return i; 
         }; 
    } 
    return result; 
}
var result = createFunctions();
console.log(result[0]); // function(){ return i; }
console.log(result[9]); // function(){ return i; }
console.log(result[0]()); // 10(i的值已经变成了10)
console.log(result[9]()); // 10

所以问题的关键在于,当你想要调用的时候,i已经变成了10,i和循环的从0到9变化中的i,没有建立联系。所以应该立即执行一下并使用i,使其返回确定的数值。

function createFunctions(){ 
    var result = new Array(); 
    for (var i=0; i < 10; i++){ 
        result[i] = function(num){ 
            return function(){ 
                return num; 
            }; 
        }(i);//这里会自动执行,于是num的值会分别绑定0-9
    } 
    return result; 
}
var result = createFunctions();
console.log(result[0]); // function(){ return num; }
console.log(result[9]); // function(){ return num; } 
console.log(result[0]());  // 0
console.log(result[9]()); // 9

7.2.2 关于this对象

函数调用this指向window,严格模式下指向undefined,对象调用this指向对象。

"use strict"
var name = 'lzw'
function a(){console.log(this.name)};
a();  // 报错 Uncaught TypeError: Cannot read property 'name' of undefined

在闭包中

var name = "The Window"; 
var object = { 
    name : "My Object", 
    getNameFunc : function(){ 
        return function(){ 
            return this.name; 
        }; 
    } 
}; 
console.log(object.getNameFunc()()); //"The Window"(在非严格模式下)

object.getNameFunc()()相当var a = object.getNameFunc(); a();即函数调用

7.2.3 内存泄漏

function assignHandler(){
    var element = document.getElementById("someElement");
    var id = element.id;
    element.onclick = function(){
        alert(id);
    };
    element = null;
}

7.3 模仿块级作用域

function outputNumbers(count){ 
 for (var i=0; i < count; i++){ 
     console.log(i);  //输出 0 1 2
 } 
 console.log(i); //输出 3
}
outputNumbers(3);

for循环外部依然可以访问到i,而且JavaScript 从来不会告诉你是否多次声明了同一个变量;遇到这种情况,它只会对后续的声明视而不见

function outputNumbers(count){ 
 for (var i=0; i < count; i++){ 
     console.log(i); //输出 0 1 2
 } 
 var i;
 console.log(i);  //输出 3(重新声明变量视而不见)
}
outputNumbers(3);

块级作用域语法:

(function(){
//这里是块级作用域
})();

下面的语法错误

function(){
//这里是块级作用域
}(); //出错!
function outputNumbers(count){ 
    (function () { 
        for (var i=0; i < count; i++){ 
            console.log(i);  // 0 1 2
        } 
    })();
    console.log(i); //报错 Uncaught ReferenceError: i is not defined
}
outputNumbers(3)

有了块级作用域,就不能在大括号外部使用i了。事实上,在ES6中有更简单的方法实现块级作用域。但在这里不详细介绍。 这种技术经常在全局作用域中被用在函数外部,从而限制向全局作用域中添加过多的变量和函数,减少内存占用,同时避免命名冲突。一般来说,我们都应该尽量少向全局作用域中添加变量和函数。

7.4 私有变量

我们把有权访问私有变量和私有函数的公有方法称为特权方法(privileged method)。

function MyObject(){
    //私有变量和私有函数
    var privateVariable = 10;
    function privateFunction(){
        return false;
    }
    //特权方法
    this.publicMethod = function (){
        privateVariable++;
        return privateFunction();
    };
}

只能通过特权方法来访问私有变量和私有函数。 隐藏不应该被直接修改的数据:

function Person(name){ 
    this.getName = function(){ 
        return name; 
    }; 
    this.setName = function (value) { 
        name = value; 
    }; 
} 
var person = new Person("lzw"); 
console.log(person.getName()); //"lzw"
person.setName("leezw"); 
console.log(person.getName()); //"leezw"

缺点: 必须使用构造函数,针对每个实例都会创建同样一组新方法

7.4.1 静态私有变量

通过私有作用域定于私有变量或函数

(function(){
    //私有变量和私有函数
    var privateVariable = 10;
    function privateFunction(){
        return false;
    }
    //构造函数
    MyObject = function(){
    };
    //公有/特权方法
    MyObject.prototype.publicMethod = function(){
        privateVariable++;
        return privateFunction();
    };
})();

注意:

  1. 定义构造函数没有使用函数声明,因为使用表达式后,没有使用var, MyObject会提升为全局变量

  2. 严格模式下,不使用var声明会报错。

    (function(){ 
    
    var name = ""; 
    
    Person = function(value){ 
      name = value; 
    }; 
    
    Person.prototype.getName = function(){ 
      return name; 
    }; 
    
    Person.prototype.setName = function (value){ 
     name = value; 
    }; 
    })(); 
    var person1 = new Person("lzw"); 
    console.log(person1.getName()); //"lzw"
    person1.setName("leezw"); 
    console.log(person1.getName()); //"leezw"
    var person2 = new Person("my"); 
    console.log(person1.getName()); //"my"(糟糕person1也跟着改变了)
    console.log(person2.getName()); //"my"

    每个实例都会共享属性,真的是糟糕。 改进一下

    (function(){ 
    
    Person = function(value){ 
      this.name = value; 
    }; 
    
    Person.prototype.getName = function(){ // 方法存储在原型上
      return this.name; 
    }; 
    
    Person.prototype.setName = function (value){ 
     this.name = value; 
    }; 
    })(); 
    var person1 = new Person("lzw"); 
    console.log(person1.getName()); //"lzw"
    person1.setName("leezw"); 
    console.log(person1.getName()); //"leezw"
    var person2 = new Person("my"); 
    console.log(person1.getName()); //"leezw"(这下子属性不会被共用了)
    console.log(person2.getName()); //"my"

7.4.2 模块模式

单例(singleton),指的就是只有一个实例的对象。

var singleton = { 
     name : value, 
     method : function () { 
         //这里是方法的代码
     } 
};

模块模式通过为单例添加私有变量和特权方法能够使其得到增强

var singleton = function(){
    //私有变量和私有函数
    var privateVariable = 10;
    function privateFunction(){
        return false;
    }
    //特权/公有方法和属性
    return {
        publicProperty: true,
        publicMethod : function(){
            privateVariable++;
            return privateFunction();
        }
    };
}();

在这个匿名函数内部,首先定义了私有变量和函数。然后,将一个对象字面量作为函数的值返回。返回的对象字面量中只包含可以公开的属性和方法。

7.4.3 增强的模块模式

有人进一步改进了模块模式,即在返回对象之前加入对其增强的代码。这种增强的模块模式适合那些单例必须是某种类型的实例,同时还必须添加某些属性和(或)方法对其加以增强的情况。

var singleton = function(){
    //私有变量和私有函数
    var privateVariable = 10;
    function privateFunction(){
        return false;
    }
    //创建对象
    var object = new CustomType();
    //添加特权/公有属性和方法
    object.publicProperty = true;
    object.publicMethod = function(){
        privateVariable++;
        return privateFunction();
    };
    //返回这个对象
    return object;
}();

第八章 BOM

8.1 window 对象

BOM 的核心对象是 window,它表示浏览器的一个实例。 在浏览器中,window 对象有双重角色,它既是通过 JavaScript 访问浏览器窗口的一个接口,又是 ECMAScript 规定的 Global 对象。

8.1.1 全局作用域

所有在全局作用域中声明的变量、函数都会变成 window 对象的属性和方法。

var age = 18; 
function sayAge(){ 
 console.log(this.age); 
} 
console.log(window.age); //18
sayAge(); //18
window.sayAge(); //18(this.age相当于window.age,即18)

区别:

var age = 18;
window.color = 'red';
console.log(color) // "red"
delete window.color;
console.log(window.color) // undefined(直接定义在window对象上可以删除)
delete window.age;
console.log(window.age) // 18(定义在全局上不可以删除)
Object.getOwnPropertyDescriptor(window,'age') // {value: 18, writable: true, enumerable: true, configurable: false}

因为使用var声明的时候,configurable特性设置为了fasle,所以不可以删除,奇怪的知识又增长了。

//这里会抛出错误,因为 oldValue 未定义
var newValue = oldValue;  // 报错  Uncaught ReferenceError: oldValue is not defined
//这里不会抛出错误,因为这是一次属性查询
//newValue 的值是 undefined
var newValue = window.oldValue;
console.log(newValue) // undefined

8.1.2 窗口关系及框架

<html>
    <head>
        <title>Frameset Example</title>
    </head>
    <frameset rows="160,*">
        <frame src="frame.htm" name="topFrame">
        <frameset cols="50%,50%">
            <frame src="anotherframe.htm" name="leftFrame">
            <frame src="yetanotherframe.htm" name="rightFrame">
        </frameset>
    </frameset>
</html>

可以通过window.frames[0]或者 window.frames["topFrame"]来引用上方的框架。不过,恐怕你最好使用top 而非 window 来引用这些框架(例如,通过 top.frames[0]、top.frames["topFrame"])。 我们知道,top 对象始终指向最高(最外)层的框架,也就是浏览器窗口。使用它可以确保在一个框架中正确地访问另一个框架。因为对于在一个框架中编写的任何代码来说,其中的 window 对象指向的都是那个框架的特定实例,而非最高层的框架。 与框架有关的最后一个对象是 self,它始终指向 window;实际上,self 和 window 对象可以互换使用。引入 self 对象的目的只是为了与 top 和 parent 对象对应起来,因此它不格外包含其他值。 所有这些对象都是 window 对象的属性,可以通过 window.parent、window.top 等形式来访问。同时,这也意味着可以将不同层次的window 对象连缀起来,例如**window.parent.parent.frames[0]**。

8.1.3 窗口位置

var leftPos = (typeof window.screenLeft == "number") ?
window.screenLeft : window.screenX;  //获取窗口距离屏幕左边的距离
var topPos = (typeof window.screenTop == "number") ?
window.screenTop : window.screenY; //获取窗口距离屏幕上边的距离

8.1.4 窗口大小

获取视口大小

var pageWidth = window.innerWidth,
pageHeight = window.innerHeight;
if (typeof pageWidth != "number"){
    if (document.compatMode == "CSS1Compat"){
        pageWidth = document.documentElement.clientWidth;
        pageHeight = document.documentElement.clientHeight;
    } else {
        pageWidth = document.body.clientWidth;
        pageHeight = document.body.clientHeight;
    }
}

8.1.5 导航和打开窗口

window.open()方法接收四个参数:要加载的 URL、窗口目标、一个特性字符串以及一个表示新页面是否取代浏览器历史记录中当前加载页面的布尔值。

//等同于< a href="https://www.baidu.com" target="topFrame"></a>
window.open("https://www.baidu.com", "topFrame");

如果有一个名叫"topFrame"的窗口或者框架,就会在该窗口或框架加载这个 URL;否则,就会创建一个新窗口并将其命名为"topFrame"。 此外,第二个参数也可以是下列任何一个特殊的窗口名称:_self、_parent、_top 或_blank。空白时传_'blank' 利用第三个参数弹出窗口:

window.open("http://www.baidu.com/","_blank", "height=400,width=400,top=10,left=10,resizable=yes");
var baiduWin = window.open("http://www.baidu.com/","_blank", 
 "height=400,width=400,top=10,left=10,resizable=yes"); 
//调整大小
baiduWin.resizeTo(500,500); 
//移动位置
baiduWin.moveTo(100,100); 

检测窗口是否被屏蔽

var wroxWin = window.open("http://www.baidu.com", "_blank","height=400,width=400"); 
if (wroxWin == null){ 
 console.log("The popup was blocked!"); 
}

8.1.6 间歇调用和超时调用

超时调用需要使用 window 对象的 setTimeout()方法,它接受两个参数:要执行的代码和以毫秒表示的时间(即在执行代码前需要等待多少毫秒)。 其中,第一个参数可以是一个包含 JavaScript 代码的字符串(就和在 eval()函数中使用的字符串一样),也可以是一个函数。

//不建议传递字符串!
setTimeout("console.log('Hello world!') ", 1000); // 报错 VM1835:2 Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in

经本人测试发现会报错,所以还是老老实实传函数吧

//推荐的调用方式
setTimeout(function() { 
 console.log("Hello world!"); 
}, 1000);
function sayHello() { 
 console.log("Hello world!"); 
}
setTimeout(sayHello, 1000);

第二个参数执行时间不一定准确,一般要延后一些。因为JavaScript 是一个单线程序的解释器,第二个参数仅仅是控制什么时候把当前任务加入队列,至于队列前面还有多少,还要执行多久,它可不管不了。简而言之,后面的排好队,一个一个来。 头铁的可以尝试以下代码

setTimeout(function() { 
    console.log("Hello world!"); 
}, 2000);
for(var i = 0; i< 70000;i++){
    console.log(i)
}
// 输出 1-70000
// 输出 "Hello world!"(远超过2秒后)

调用 setTimeout()之后,该方法会返回一个数值 ID,表示超时调用。 要取消尚未执行的超时调用计划,可以调用clearTimeout()方法并将相应的超时调用 ID 作为参数传递给它

//设置超时调用
var timeoutId = setTimeout(function() { 
 console.log("Hello world!"); 
}, 10000); 
//注意:把它取消
clearTimeout(timeoutId); // 取消了什么都没有输出
//设置超时调用
var timeoutId = setTimeout(function() { 
    clearTimeout(timeoutId); // 这里面也可以取消
    console.log("Hello world!"); 
}, 10000); 

在单页面项目,例如react项目和vue项目,当页面发生跳转后但由于前一个页面请求还没有结束,或者事件 定时器没有结束,但是页面跳转到下一个页面了,此时就可能会报错并影响性能。例如,react:

import React,{
  Component
} from 'react';

export default class Hello extends Component {
  componentDidMount() {
    this.timer = setTimeout(
      () => { console.log('把一个定时器的引用挂在this上'); },
      500
    );
  }
  componentWillUnmount() {
    // 如果存在this.timer,则使用clearTimeout清空。
    // 如果你使用多个timer,那么用多个变量,或者用个数组来保存引用,然后逐个clear
    this.timer && clearTimeout(this.timer);
  }};

间歇调用:它会按照指定的时间间隔重复执行代码,直至间歇调用被取消或者页面被卸载。

//不建议传递字符串!
setInterval ("console.log('Hello world!') ", 10000);  // 报错 Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the...
//推荐的调用方式
setInterval (function() {
    console.log("Hello world!");
}, 10000);
var i = 0
var intervalId = setInterval (function() {
    i++
    console.log("Hello world!");
    if(i > 10){
        clearInterval(intervalId) // 达到指定数量后清除
    }
}, 1000);

用超时调用重写上面例子

var i = 0;
function a() {
    i++;
    console.log("Hello world!");
    if(i < 10){
        setTimeout(a,100) // 达到指定数量后清除
    }else{
        clearTimeout(timer)
    }
}
var timer = setTimeout (a, 1000);

8.1.7 系统对话框

alert('这是个对话框')
console.log(confirm('确定么?')) // 点击确定返回true,点击取消返回false
console.log(prompt('你的名字')) //  输出你输入的文本,取消时返回null
window.print() // 打印

经本人测试,window.find()无效

8.2 location

location 对象是很特别的一个对象,因为它既是 window 对象的属性,也是document 对象的属性;换句话说,window.location 和 document.location 引用的是同一个对象。

console.log(location) // 输出
hash: ""
host: "local-ntp"
hostname: "local-ntp"
href: "chrome-search://local-ntp/local-ntp.html"
origin: "chrome-search://local-ntp"
pathname: "/local-ntp.html"
port: ""
protocol: "chrome-search:"
reload: ƒ reload()
replace: ƒ ()
search: ""
toString: ƒ toString()

不要去记住这些东西,要用的时候打印一下就知道了。

8.2.1 查询字符串参数

尽管 location.search 返回从问号到 URL 末尾的所有内容,但却没有办法逐个访问其中的每个查询字符串参数。这也太值得吐槽了!

function getQueryStringArgs(){ 
    //取得查询字符串并去掉开头的问号
    var qs = (location.search.length > 0 ? location.search.substring(1) : ""), 

    //保存数据的对象
    args = {}, 

    //取得每一项
    items = qs.length ? qs.split("&") : [], 
    item = null, 
    name = null,
    value = null, 
    //在 for 循环中使用
    i = 0, 
    len = items.length; 
    //逐个将每一项添加到 args 对象中
    for (i=0; i < len; i++){ 
        item = items[i].split("="); 
        name = decodeURIComponent(item[0]); 
        value = decodeURIComponent(item[1]); 
        if (name.length) { 
        args[name] = value; 
        } 
    } 

    return args; 
}

https://www.baidu.com/?tn=48020221_28_hao_pg&job=worker&name=lzw页面下执行

console.log(getQueryStringArgs())
// {tn: "48020221_28_hao_pg", job: "worker", name: "lzw"}

8.2.2 位置操作

使用 location 对象可以通过很多方式来改变浏览器的位置。

location.assign("http://www.baidu.com"); // 跳转到该页面

相当于

window.location = "http://www.baidu.com"
window.location.href = "http://www.baidu.com"

将hash、search、hostname、pathname 和 port 属性设置为新值来改变 URL。用户可以回到前一个页面

location.hash= "#lllll" // 变为"https://www.baidu.com/#lllll"
location.search = "?job=worker" // 变为 "https://www.baidu.com/?job=worker#lllll"
location.hostname = "www.qq.com" // 变为 "https://www.qq.com/?job=worker#lllll"

replace()方法:跳转后用户无法回到前一个页面,但是可以回到前一个页面再之前的页面,简而言之就是被替代了

location.replace('https://www.baidu.com')

location.reload():页面重新加载

location.reload(); //重新加载(有可能从缓存中加载)
location.reload(true); //重新加载(从服务器重新加载)

8.3 navigator 对象

console.log(navigator)

8.3.1 检测插件

console.log(navigator.plugins)  // 打印出来看就好,别记

8.4 screen 对象

console.log(screen) // 打印看看就好,千万别记

8.5 history 对象

//后退一页
history.go(-1);
//前进一页
history.go(1);
//前进两页
history.go(2);
history.go('baidu.com') //跳转到最近的 baidu.com 页面
//后退一页
history.back();
//前进一页
history.forward();

第九章 客户端检测