JS高级

1,javascript 基础知识

Array对象
属性 描述
length 设置或返回数组中的元素的数目

Array对象属性

属性 描述
length 设置或返回数组中的元素的数目
方法 描述
concat() 链接两个或更多的数组并返回结果
join() 把数组的所有的元素放入一个字符串,元素通过指定的分隔符进行分割
pop() 删除并返回数组的最后一个元素
reverse() 颠倒数组中的元素的顺序
shift 删除并返回数组的第一个元素
slice() 从某个已有的数组返回选定的元素
sort() 对数组的元素进行排序
splice() 删除元素,冰箱数组添加新元素
unshift() 向数组的开头添加一个或更多元素,并返回新的长度

Arrray对象方法

方法 描述
concat() 链接两个或更多的数组并返回结果
join() 把数组的所有的元素放入一个字符串,元素通过指定的分隔符进行分割
pop() 删除并返回数组的最后一个元素
reverse() 颠倒数组中的元素的顺序
shift 删除并返回数组的第一个元素
slice() 从某个已有的数组返回选定的元素
sort() 对数组的元素进行排序
splice() 删除元素,冰箱数组添加新元素
unshift() 向数组的开头添加一个或更多元素,并返回新的长度
Date对象
方法 描述
Date() 当日的日期和时间
getDate() 从Date对象返回一个月中的某一天(1-31).
getDay() 从Date对象返回一周中的某一天(0-6)
getMonth() 从Date对象返回月份(0-11)
getFullYear() 从Date对象以四位数字返回年份
getYear() 请使用getFullYear()方法代替
getHours() 返回Date对象的小时(0-23)
getMiunute s() 返回Date对象的分钟(0-59)
getSeconds() 返回Date对象的秒数(0-59)
getMiliseconds() 返回Date的毫秒(0-999)
getTime() 返回1970年1月1日至今的毫秒数
parse() 返回1970年1月1日午夜到指定日期(字符串)的毫秒数)
setDate() 设置Date对象中月的某一天(1-31)
setMonth() 设置Date对象中月份(0-11)
setFullYear() 设置Date对象中的年份(四位数字)
setYear() 请使用setFullYear()方法代替
setHours() 设置Date对象中的小时(0-23)
setMinutes() 设置Date()对象a的分钟(0-59)
setSeconds() 设置Date对象中的秒钟(0-59)
setMiliseconds() 设置Date对象中的毫秒(0-999)
setTime() 以毫秒设置Date对象
toString() 把Date对象转换为字符串
toTimeString() 把Date对象的时间部分转换为字符串
toDateString() 把Date对象的日期部分转换为字符串

Date对象方法

方法 描述
Date() 当日的日期和时间
getDate() 从Date对象返回一个月中的某一天(1-31).
getDay() 从Date对象返回一周中的某一天(0-6)
getMonth() 从Date对象返回月份(0-11)
getFullYear() 从Date对象以四位数字返回年份
getYear() 请使用getFullYear()方法代替
getHours() 返回Date对象的小时(0-23)
getMiunute s() 返回Date对象的分钟(0-59)
getSeconds() 返回Date对象的秒数(0-59)
getMiliseconds() 返回Date的毫秒(0-999)
getTime() 返回1970年1月1日至今的毫秒数
parse() 返回1970年1月1日午夜到指定日期(字符串)的毫秒数)
setDate() 设置Date对象中月的某一天(1-31)
setMonth() 设置Date对象中月份(0-11)
setFullYear() 设置Date对象中的年份(四位数字)
setYear() 请使用setFullYear()方法代替
setHours() 设置Date对象中的小时(0-23)
setMinutes() 设置Date()对象a的分钟(0-59)
setSeconds() 设置Date对象中的秒钟(0-59)
setMiliseconds() 设置Date对象中的毫秒(0-999)
setTime() 以毫秒设置Date对象
toString() 把Date对象转换为字符串
toTimeString() 把Date对象的时间部分转换为字符串
toDateString() 把Date对象的日期部分转换为字符串
Number对象
属性 描述
MAX_VALUE b可表示的最大的数
MIN_VALUE 可表示的最小的数
NaN 非数字,相当于NaN
NEGATIVE_INFINITY 负无穷大,溢出时返回该值,相当于-Infinity
POSITIVE_INFINITY 正无穷大,溢出时返回该值,相当于Infinity

number对象的属性

属性 描述
MAX_VALUE b可表示的最大的数
MIN_VALUE 可表示的最小的数
NaN 非数字,相当于NaN
NEGATIVE_INFINITY 负无穷大,溢出时返回该值,相当于-Infinity
POSITIVE_INFINITY 正无穷大,溢出时返回该值,相当于Infinity
方法 描述
toString 把数字转换为字符串,使用指定的基数
toFixed 把数字字符串,结果的小数点后有指定位数的数字
toExponential 把对象的值转换为指数计数法
toPrecision 把数字格式化为指定的长度
valueOf 返回一个Number对象的基本数字值

Number对象方法

方法 描述
toString 把数字转换为字符串,使用指定的基数
toFixed 把数字字符串,结果的小数点后有指定位数的数字
toExponential 把对象的值转换为指数计数法
toPrecision 把数字格式化为指定的长度
valueOf 返回一个Number对象的基本数字值
Boolean对象
方法 描述
toString() 把逻辑值转换为字符串,并返回结果
valueOf() 返回Boolean对象的原始值

Boolean对象方法

方法 描述
toString() 把逻辑值转换为字符串,并返回结果
valueOf() 返回Boolean对象的原始值
String对象
属性 描述
length 字符串的长度

String对象属性

属性 描述
length 字符串的长度
方法 描述
anchor() 创建HTML锚
big() 用大号字体显示字符串
blink() 显示闪动字符串
bold() 使用粗体显示字符串
fontcolor() 使用指定的颜色来显示字符串
fontsize() 使用指定的尺寸来显示字符串
italics() 使用斜体显示字符串
link() 将字符串显示为链接
small() 使用小号来显示字符串
strike() 使用删除线来显示字符串
sub() 把字符串显示为下标
fixed() 以打字及文本显示字符串
sup() 把字符串显示为上标
charAt() 返回在指定未指定的字符串
charCodeAt() 返回在指定位置的字符的Unicode
concat() 链接字符串
formCharCode() 从字符编码创建一个字符串
indexOf() 检索字符串
lastIndexOf() 从后向前搜索字符串
match() 找到一个或多个正字表达式的匹配
replace() 替换与正则表达式匹配的字符串
search() 检索与正则表达式相匹配的值
slice() 提取字符串的片段,并在新的字符串中返回被提取的部分
split() 把字符串分隔为字符串数组
substr() 从起始索引号提取字符串中指定书目的字符
substring() 提取字符串中两个指定的索引号之间的字符
toLocaleLowerCase() 把字符串转换为小写
toLocalUpperCase() 把字符串转换为大写
toLowerCase() 把字符串转换为小写
toUpperCase() 把字符串转换为大写
toSource() 代表对象的源代码
toString() 返回字符串
valueOf() 返回某个字符串对象的原始值

String对象方法

方法 描述
anchor() 创建HTML锚
big() 用大号字体显示字符串
blink() 显示闪动字符串
bold() 使用粗体显示字符串
fontcolor() 使用指定的颜色来显示字符串
fontsize() 使用指定的尺寸来显示字符串
italics() 使用斜体显示字符串
link() 将字符串显示为链接
small() 使用小号来显示字符串
strike() 使用删除线来显示字符串
sub() 把字符串显示为下标
fixed() 以打字及文本显示字符串
sup() 把字符串显示为上标
charAt() 返回在指定未指定的字符串
charCodeAt() 返回在指定位置的字符的Unicode
concat() 链接字符串
formCharCode() 从字符编码创建一个字符串
indexOf() 检索字符串
lastIndexOf() 从后向前搜索字符串
match() 找到一个或多个正字表达式的匹配
replace() 替换与正则表达式匹配的字符串
search() 检索与正则表达式相匹配的值
slice() 提取字符串的片段,并在新的字符串中返回被提取的部分
split() 把字符串分隔为字符串数组
substr() 从起始索引号提取字符串中指定书目的字符
substring() 提取字符串中两个指定的索引号之间的字符
toLocaleLowerCase() 把字符串转换为小写
toLocalUpperCase() 把字符串转换为大写
toLowerCase() 把字符串转换为小写
toUpperCase() 把字符串转换为大写
toSource() 代表对象的源代码
toString() 返回字符串
valueOf() 返回某个字符串对象的原始值
Math对象
属性 描述
E 安徽算数常量e,即自然对数的底数(约等于2.718)
LN2 返回2的自然对数(约等于0.693)
LN10 返回10的自然对数(约等于2.302)
LOG2E 返回以2为底的e的对数(约等于0.434)
LOG10E 返回以10为底的e的对数(约等于0.434)
PI 返回圆周率(约等于3014159)
SQTR1_2 返回2的平方根的倒数(约等于0.707)
SQRT2 返回2的平方个(约等于1.414)

Math对象的属性

属性 描述
E 安徽算数常量e,即自然对数的底数(约等于2.718)
LN2 返回2的自然对数(约等于0.693)
LN10 返回10的自然对数(约等于2.302)
LOG2E 返回以2为底的e的对数(约等于0.434)
LOG10E 返回以10为底的e的对数(约等于0.434)
PI 返回圆周率(约等于3014159)
SQTR1_2 返回2的平方根的倒数(约等于0.707)
SQRT2 返回2的平方个(约等于1.414)
属性 方法
abs(x) 返回数的绝对值
acos(x) 返回数的反余弦值
asin(x) 返回数的反正弦值
atan(x) 以介于-PI/2与PI/2弧度之间的数值来返回x的反正切值
atan2(y,x) 返回从 x 轴到点 (x,y) 的角度(介于 -PI/2 与 PI/2 弧度之间)。
ceil(x) 对数进行上舍入。
cos(x) 返回数的余弦。
exp(x) 返回 e 的指数。
floor(x) 对数进行下舍入。
log(x) 返回数的自然对数(底为e)。
max(x,y) 返回 x 和 y 中的最高值。
min(x,y) 返回 x 和 y 中的最低值。
pow(x,y) 返回 x 的 y 次幂。
random() 返回 0 ~ 1 之间的随机数。
round(x) 把数四舍五入为最接近的整数。
sin(x) 返回数的正弦。
sqrt(x) 返回数的平方根。
tan(x) 返回角的正切。
toSource() 返回该对象的源代码。
valueOf() 返回 Math 对象的原始值。

Math对象的方法

属性 方法
abs(x) 返回数的绝对值
acos(x) 返回数的反余弦值
asin(x) 返回数的反正弦值
atan(x) 以介于-PI/2与PI/2弧度之间的数值来返回x的反正切值
atan2(y,x) 返回从 x 轴到点 (x,y) 的角度(介于 -PI/2 与 PI/2 弧度之间)。
ceil(x) 对数进行上舍入。
cos(x) 返回数的余弦。
exp(x) 返回 e 的指数。
floor(x) 对数进行下舍入。
log(x) 返回数的自然对数(底为e)。
max(x,y) 返回 x 和 y 中的最高值。
min(x,y) 返回 x 和 y 中的最低值。
pow(x,y) 返回 x 的 y 次幂。
random() 返回 0 ~ 1 之间的随机数。
round(x) 把数四舍五入为最接近的整数。
sin(x) 返回数的正弦。
sqrt(x) 返回数的平方根。
tan(x) 返回角的正切。
toSource() 返回该对象的源代码。
valueOf() 返回 Math 对象的原始值。
值类型和引用类型

值类型(基本数据类型)

  • 数值类型
  • 布尔类型
  • undefined
  • null
  • 字符串
    值类型是存储在栈(stack)中的简单数据,也就是说,他们的值直接存储在变量访问的位置
var num = 10;
var str = 'hello js";
var flag = true;
var un = undefined;
var nu = null;

上面定义的这些值类型的数据在内村中的存储如下

值类型.png

引用类型(符合数据类型)

  • 对象
  • 数组
  • 函数
    存储在堆(heap)中的对象 也就是说存储在变量出的值是一个指针(point),指向存储对象的内存处
var arr = [1,2,3];
var p1 = {name:"张三",age:18};
var p2 = {
      name:"李四",
      age:50,
      son:{
               name:"李晓艺,"
               age:18,
              }
var p3 = {
      name:"王五",
      age:50,
      children:[
           {
               name:"王小艺,"
               age:20,
              },
          {
               name:"王小二,"
               age:15,
              },
               {
               name:"王小三,"
               age:12,
              }
             ]
}

上面定义的这些引用类型的数据在内村中的存储如下:

引用类型.jpg
值类型和引用类型的特征

值类型和引用类型的赋值

  1. 值类型赋值,直接将值赋值一份
var num1 = 10;
var num2 = num1;

在内存的存储

值类型存储.jpg
  • var num1 = 10;表示变量num1存储的是数字10;
  • 将数据拷贝一份,也就是将10拷贝一份,这个时候内村中有两个10;
  • 将拷贝的10赋值给num2
  1. 引用类型赋值,是将地址复制一份
var p ={name:"张三",age:19};
var p1= p;

在内存中的体现为:

引用类型存储.jpg
  • var p = {name:"张三",age:19};p 中存储的是对象的地址
  • 赋值就是将变量p中存储的数据,也就是地址拷贝一份,然后将该数据赋值给p1
  • 此时内村中只有一个对象,变量pp1同时指向这个对象

总结

  • 在调用函数的时候,传参的过程其实就是用实参给形参赋值的过程
  • 当参数为值类型的时候,函数内和函数外的两个变量完全不同,仅仅只是存的值一样而已,修改时互不影响
  • 当参数为引用类型的时候,函数内和函数外的两个变量不同,但是共同指向同一个对象,在函数内修改对象数据时会影响外部
对象的动态特性

给对象动态添加属性
当一个对象需要某个属性的时候,可以用两种方式添加属性

  • 直接使用 对象名 = 值这种形式,为对象添加对应的属性
  • 使用关联数组语法对象名["属性名"] = 值这种形式,为对象添加对应的属性
var o = {};//o是一个没有任何自定义属性的对象
//现在想让他拥有name age gender 等属性
//直接使用 对象名.属性名 = 值
o.name = "张三";
//使用  对象名["属性名"] = 值
o["age"] = 18;

注意
当要动态的为一个对象添加属性的时候,必须使用关联数组的方式

var str = prompt("请输入属性名");
o = {};
//o.str = "这是一个新属性";这样写是不对的,会给对象新增一个str属性,正确的写法如下
o[str]=''这是一个新属性";

对象属性的访问形式

  • 点语法 :对象名.属性名
  • 关联数组:对象名[属性名]
var o = {
 name:"zhangsan",
sayHello:function(){
console.log('hello,my name is"+this.name);
}
};
//点语法
console.log(o.name);
//关联数组语法
console.log(o["name"]);
//这两种用法同样适用于方法
o.sayHello();
o["sayHello"]();
//可以对这个对象的属性进行遍历,如果是值,就打印,如果是方法,就调用
for(var k in o)
{
if(typeof o[k] == 'function'){
o[k]();
}else{
console.log('log:'+o[k]);
}
}
常用的DOM操作

四字总结:增删改查

  • 获取元素
    getElementById getElementsByTagName getElementsByClassName
  • 元素节点操作
    appendChild insertBefore removeChid replaceChild cloneNode createElement creatTextNode(创建文本节点)
  • 属性节点操作
    getAttribute setAttribute removeAttribute
  • 常用DOM属性
    className innerHTML innerText.textContent value children
异常处理

常见的异常分类

  • 运行环境的多样性导致的异常(浏览器)
  • 语法错误 代码错误
    异常捕获
    捕获异常,使用try-catch语句
try{
    //这里写可能出现异常的代码
}catch(e){
    //这里的e就是捕获的异常对象
    //可以在这里写,出现异常后的处理代码
}

异常捕获语句执行的过程为:

1 代码正常运行, 如果在try中出现了错误, try 里面出现错误的语句后面的代码都不再执行, 直接跳转到 catch 中

2 catch中处理错误信息

3 然后继续执行后面的代码

4 如果 try 中没有出现错误, 那么不走 catch 直接执行后面的代码

通过try-catch语句进行异常捕获之后,代码将会继续执行,而不会中断。

注意:

  • 语法错误异常用try-catch语句无法捕获,因为在预解析阶段,语法错误会直接检测出来,而不会等到运行的时候才报错。
  • try-catch在一般日常开发中基本用不到,但是如果要写框架什么的,用的会非常多。因为这个会让框架变得健壮

抛出异常
如何手动抛出异常呢

自己写一个函数需要一个参数如果用户不传参数,此时直接给用户抛出异常,就需要了解如何抛出异常

抛出异常使用throw关键字,语法如下:
throw 异常对象;
异常对象一般是用new Error("异常消息"), 也可以使用任意对象

function test(para){
    if(para == undefined){
        throw new Error("请传递参数");
        //这里也可以使用自定义的对象
        throw {"id":1, msg:"参数未传递"};
    }
}

try{
    test();
}catch(e){
    console.log(e);
}
异常的传递机制

function f1 () {
    f2(); // f1 称为调用者, 或主调函数, f2 称为被调用者, 或被调函数
}

function f2 () {
    f3();
}

function f3() {
    throw new Error( 'error' );
}
f1();

当在被调函数内发生异常的时候,异常会一级一级往上抛出。
异常捕获语句的完整模式
异常捕获语句的完整模式为try-catch-finally

try {
    //可能出现错误的代码
} catch ( e ) {
    //如果出现错误就执行
} finally {
    //结束 try 这个代码块之前执行, 即最后执行
}

finally中的代码,不管有没有发生异常,都会执行。一般用在后端语言中,用来释放资源,JavaScript中很少会用到

调试工具的使用

调试窗口介绍

  • 指针: 选择页面中的元素

  • 手机: 使用移动端界面调试

  • Elements: 查看页面 DOM 树

  • Console: 控制台(注意, 控制台与该页面是一个整体, 在控制台中的任何操作, 会影响到页面)

  • Source: 代码调试

调试工具的使用

  • 逐过程运行, 一次运行一个函数

  • 单步运行(逐步运行), 一次运行一句, 如果是函数, 进入函数体内运行

  • 继续运行. 从当前状态运行下去, 直到出现断点, 如果没有断点则运行结束
    设置断点技巧

  • 逐步与逐过程混合

  • 断点加继续运行

  • 条件断点(右键添加 add contitional breakpoint)

2,面向对象

构造函数是干什么用的?
在javascript中,构造函数是给对象添加属性,初始化属性用的.
对象的创建过程
var p = new Person();
以上面这个p对象创建为例:

  1. 首先使用new关键字创建对象,类似于使用{},这个时候创建出来的对象是一个"没有任何成员"的对象。这里需要注意两点:
  • 使用new关键字创建的对象,对象的类型就是创建这个对象使用的构造函数的函数名
  • 使用{}创建对象,对象的类型一定是Object,相当于使用了new Object()

2 使用构造函数为其初始化成员

  • 在构造函数调用开始的时候,有一个赋值操作,也就是让this = 刚创建出来的对象
  • 在构造函数中,this就代表刚创建出来的对象
  1. 在构造函数中,利用对象的动态特性,为对象添加成员
面向对象的特性

封装
对象是将数据与功能组合到一起, 即封装

1.js 对象就是 键值对的集合

 键值如果是数据( 基本数据, 复合数据, 空数据 ), 就称为属性
 如果键值是函数, 那么就称为方法

2.对象就是将属性与方法封装起来

3.方法是将过程封装起来
继承
传统继承基于模板
子类可以使用从父类继承的属性和方法

class Person {
 string name;
 int age;
}

class Student : Person {
}
var stu = new Student();
stu.name

即:让某个类型的对象获得另一个类型的对象的属性的方法

js继承基于对象
在JavaScript中,继承就是当前对象可以使用其他对象的方法和属性。

js继承实现举例:混入(mix)

    for ( var k in o2 ) {
        o1[ k ] = o2[ k ];
    }
}

多态

把不同的子类对象都当作父类来看,可以屏蔽不同子类对象之间的差异,写出通用的代码,做出通用的编程,以适应需求的不断变化。

动物 animal = new 子类(); // 子类:麻雀、狗、猫、猪、狐狸...
动物 animal = new 狗();
animal.叫();
传统构造函数存在的问题

发现问题
现有构造函数:

function Person(name, age){
    this.name = name;
    this.age = age;
    this.sayHi = function(){
        console.log("你好");
    }
}

调用该构造函数创建对象,并对比创建出来的对象的sayHi方法:

var p = new Person("张三", 18);
var p1 = new Person("李四", 19);
console.log(p.sayHi == p1.sayHi); //输出结果为false

由于每个对象都是由new Person创建出来的,因此每创建一个对象,函数sayHi都会被重新创建一次,这个时候,每个对象都拥有一个独立的,但是功能完全相同的方法。

构造函数.jpg

功能相同的函数,完全没有必要再内存中存在这么多份。所以就造成了资源浪费。
解决问题
这里最好的办法就是将函数体放在构造函数之外. 在构造函数中只需要引用该函数即可。

function sayHello(){
    console.log("你好");
}

function Person(name, age){
    this.name = name;
    this.age = age;
    this.sayHi = sayHello;
}

//调用该构造函数创建对象,并对比创建出来的对象的sayHi方法
var p = new Person("张三", 18);
var p1 = new Person("李四", 19);
console.log(p.sayHi == p1.sayHi); //输出结果为true

这样写依然存在问题:

  • 全局变量增多,会增加引入框架命名冲突的风险

  • 代码结构混乱,会变得难以维护

使用原型解决构造函数的问题

关键点
  • 每一个函数在定义的时候,都会有跟它关联的一个对象被创建出来
  • 每一个由构造函数创建出来的对象,都会默认的和构造函数的神秘对象关联
  • 当使用一个方法进行属性或者方法访问的时候,会先在当前对象内查找该属性和方法
  • 如果当前对象内未找到,就回去跟它关联的神秘对象内进行查找
function Person(name, age){
    this.name = name;
    this.age = age;
    this.sayHi = function(){
        console.log("Hello!");
    };
}
var p = new Person("张三", 18);
p.sayHi(); //当前对象内有这个方法,所以不会去神秘对象内进行查找
var p1 = new Person("李四", 19);
p1.sayHello(); //当前对象没没有找到这个方法,所以去神秘对象内进行查找

如何访问到这个神秘的对象?

//可以通过构造函数.prototype访问这个神秘对象
console.log(Person.prototype);

当尝试给这个对象新增一个方法之后

Person.prototype.sayHello = function(){
    console.log("我是神秘对象中的方法");
};

使用p,p1都可以访问这个方法:

p.sayHello();
p1.sayHello();

总结
所有对象共享神秘对象(构造函数.prototype)内的属性和方法.

结局方案

既然所有对象共享神秘对象(构造函数.prototype)内的属性和方法。我们只需要将需要共享的东西,也就是重复占用内存的东西,全部都放到 神秘对象(构造函数.prototype)中,那么所有对象就都可以使用,并且内存里面也只有一份了。

改造函数

function Person(name, age){
    this.name = name;
    this.age = age;
}

Person.prototype.sayHi = function(){
    console.log("你好");
};

//测试
var p = new Person("张三", 18);
var p1 = new Person("李四", 19);

console.log(p.sayHi == p1.sayHi); //输出true
常见的错误

将属性写在神秘对象(构造函数.prototype)

function Car(name){
     this.name = name;
}

function Person() {}

Person.prototype.name = '张三'; //基本类型的属性影响不大

Person.prototype.car = new Car("法拉利"); //引用类型的属性,会被所有的对象共享

var p = new Person();

赋值的错误

function Person() {}

Person.prototype.name = '张三';

var p1 = new Person();

var p2 = new Person();

p1.name = '李四';

console.log( p1.name );

console.log( p2.name );

// 如果是访问数据, 当前对象中如果没有该数据就到构造函数的原型属性中去找

// 如果是写数据, 当对象中有该数据的时候, 就是修改值; 如果对象没有该数据, 那么就添加值
原型的相关概念

神秘对象与构造函数

  • 神秘对象就是构造函数的'原型属性'
  • 简称原型(构造函数的原型)
    神秘对象与构造函数所创建出来的对象
  • 神秘对象针对构造函数创建出来的对象成为'原型对象'
  • 简称原型(对象的原型)
原型与构造函数.jpg
原型继承

[1] 构造函数创建的对象 继承自 构造函数的原型属性
[2] 构造函数创建的对象 继承自 该对象的原型对象

  • 原型中的成员, 可以直接被实例对象所使用
  • 实例对象直接 "含有" 原型中的成员
  • 因此实例对象 继承自 原型
  • 这样的继承就是 "原型继承"
原型的使用

使用对象的动态特性

function Person () { }
Person.prototype.func = function () {
 console.log( 'something' );
};

var p = new Person();
p.func();

直接替换原型对象

function Person () { };
Person.prototype = {
    func: function () {
        console.log( '22222' );
    }
};

var p = new Person();
p.func();

直接替换原型会出现的问题

function Person () { }

Person.prototype.func = function () {
    console.log( 'something' );
};

var p = new Person();

Person.prototype.func = function () {
    console.log( 'something' );
};

var p1 = new Person();

p.func();

p1.func();

替换原型之后,在替换前创建出来的对象和替换后创建出来的对象的原型对象不一致

替换.jpg




对象的proto属性

标识符命名规则

  • 区分大小写,Name和name是两个不同的变量

  • 标识符可以以下划线_,美元符$或者字母开头,但是不能是数字

  • 标识符可以由下划线_,美元符$,字母,数字组成

神秘对象的访问
构造函数的prototype属性

之前我们访问神秘对象的时候,使用的是原型属性 prototype

function Person(){}

//通过构造函数的原型属性prototype可以直接访问原型

Person.prototype;

在之前是无法通过构造函数创建出来的对象访问原型的

function Person(){}

var p = new Person();

//以前不能直接通过p来访问神秘对象

**实例对象的proto属性
__proto__属性最早是火狐浏览器引入的,用以通过实例对象来访问原型,这个属性在早期是非标准的属性

有了__proto__属性,就可以通过构造函数创建出来的对象直接访问神秘对象

function Person(){}

var p = new Person();

//实例对象的__proto__属性可以方便的访问到原型对象

p.__proto__;


//既然使用构造函数的`prototype`和实例对象的`__proto__`属性
//都可以访问原型对象
//就有如下结论
p.__proto__ === Person.prototype;

proto属性的用途

  • 可以用来访问原型

  • 在实际开发中除非有特殊的需求,不要轻易的使用实例对象的proto属性去修改原型的成员,

  • 在调试过程中,可以轻易的查看原型的成员

早期是使用实例对象访问构造函数属性constuctor

var p = new Person();
p.constructor.prototype;

给实例继承自原型的属性赋值时需要注意的问题

function Person(){};
Person.prototype.name = "周华健";
var o1 = new Person();
var o2 = new Person();
o1.name = "李宗盛"; //这里修改的不是原型对象的name属性,而是给o1自己新增了一个name属性,进行了赋值
继承的实现方式

最简单的继承实现
直接遍历父对象的属性,将所有的属性加到当前对象上

var animal = {
    name:"Animal",
    sex:"male",
    age:5,
    bark:function(){
        console.log("Animal bark");
    }
};

var dog = {};

for (var k in animal){

    dog[k]= animal[k];

}

原型继承
每一个构造函数都有prototype原型属性,通过构造函数创建出来的对象都继承自该原型属性。所以可以通过更改构造函数的原型属性来实现继承。

function Dog(){
    this.type = "yellow Dog";
}

function extend(obj1, obj2){
    for (var k in obj2){
        obj1[k] = obj2[k];    
    }
};

//使用混入的方式,将属性和方法添加到构造函数的原型属性上,构造函数所创建出来的实例就都有了这些属性和方法。
extend(Dog.prototype, {
    name:"",
    age:"",
    sex:"",
    bark:function(){}

})

//使用面向对象的思想把extend方法重新封装
//extend是扩展的意思,谁要扩展就主动调用extend这个方法
//所以extend应该是对象的方法,那现在我们要扩展的是构造函数的原型对象
//所以给构造函数的原型对象添加一个extend方法

//如下:

Dog.prototype.extend = function(obj){
    for (var k in obj){
        this[k]=obj[k];
    }
}

//调用方式就变成了下面这种形式

Dog.prototype.extend({
    name:"",
    age:"",
    sex:"",
    bark:function(){}
});
原型三角形的绘制

请尝试绘制如下三种情况的原型三角形图:

  • 第一种
function Person() {
    this.name = '张三';
    this.sayHello = function () {
    }
}

var p = new Person();
  • 第二种
function Person() {
    this.name = '张三';
}

Person.prototype.sayHello = function () {
}
var p = new Person();
  • 第三种
function Person() {
    this.name = '张三';
}

Person.prototype = {
    sayHello: function () {
    }    
};

var p = new Person();
属性搜索原则

访问一个对象的成员的时候,首先是在实例中找,没有找到, 就去原型中找, 但是原型中没有怎么办?
原型链
每一个对象都有原型属性,那么对象的原型属性也会有原型属性,所以这样就形成了一个链式结构,我们称之为原型链。
属性搜索原则
所谓的属性搜索原则,也就是属性的查找顺序,在访问对象的成员的时候,会遵循如下的原则:

1 首先在当前对象中查找,如果找到,停止查找,直接使用,如果没有找到,继续下一步

2 在该对象的原型中查找,如果找到,停止查找,直接使用,如果没有找到,继续下一步

3 在该对象的原型的原型中查找,如果找到,停止查找,直接使用,如果没有找到,继续下一步。

4 继续往上查找,直到查找到Object.prototype还没有, 那么是属性就返回 undefied,是方法,就报错xxx is not a function。

原型链结构

凡是对象就有原型, 原型又是对象, 因此凡是给定义一个对象, 那么就可以找到他的原型, 原型还有原型. 那么如此下去, 就构成一个对象的序列. 称该结构为原型链.

function Person() {
}

var p = new Person();
// p 具有默认的原型链

默认的原型链结构就是:

当前对象 -> 构造函数.prototype -> Object.prototype -> null

在实现继承的时候, 有时会利用替换原型链结构的方式实现原型继承, 那么原型链结构就会发生改变

使用构造函数创建出对象, 并且没有利用赋值的方式修改原型, 就说该对象保留默认的原型链.

默认原型链结构是什么样子呢?

function ItcastCollection () {
}
ItcastCollection.prototype = [];
var arr = new ItcastCollection();
// arr -> [] -> Array.prototype -> Object.prototype -> null
// var arr = new Array();

//我们可以对比一下o1和o2的name值

原型式继承

观察:DOM对象的原型链
原型式继承就是利用修改原型链的结构( 增加一个节点, 删除一个节点, 修改节点中的成员 ), 来使得实例对象可以使用整条链中的所有成员.
绘制原型链结构
注意:函数也有proto属性,暂时不考虑这个!

观察如下代码,绘制相应的原型链结构图:

function Person(){};
var p = new Person();
原型链.jpg

练习:

  1. {}的原型链结构图
  2. 绘制[]的原型链结构图
    注意:

在 js 中, 所有的对象字面量在解析以后, 就是一个具体的对象了. 那么可以理解为 调用的 对应的构造方法.

  • 例如在代码中写上 {}, 就相当于new Object()
  • 例如代码中有 [], 就相当于new Array()
  • 例如代码中有 /./, 就相当于new RegExp( '.' )
    注意: 在底层理论执行的过程中, 是否有调用构造函数, 不一定. 和浏览器的版本有关.

练习:

绘制如下代码的原型链结构:


var o = {
    appendTo: function ( dom ) {
    }

};

function DivTag() {}

DivTag.prototype = o;
var div = new DivTag();

Object.prototype成员介绍
成员 描述
Object.prototype.proto 指向当对象被实例化的时候,用作原型的对象。
Object.prototype.hasOwnProperty() 返回一个布尔值 ,表示某个对象是否含有指定的属性,而且此属性非原型链继承的。
Object.prototype.isPrototypeOf() 返回一个布尔值,表示指定的对象是否在本对象的原型链中。
Object.prototype.toString() 返回对象的字符串表示。
Object.prototype.valueOf() 返回指定对象的原始值。

Object.prototype常用成员()

成员 描述
Object.prototype.proto 指向当对象被实例化的时候,用作原型的对象。
Object.prototype.hasOwnProperty() 返回一个布尔值 ,表示某个对象是否含有指定的属性,而且此属性非原型链继承的。
Object.prototype.isPrototypeOf() 返回一个布尔值,表示指定的对象是否在本对象的原型链中。
Object.prototype.toString() 返回对象的字符串表示。
Object.prototype.valueOf() 返回指定对象的原始值。
函数的构造函数Function

在 js 中 使用Function可以实例化函数对象。也就是说在 js 中函数与普通对象一样, 也是一个对象类型. 函数是 js 中的一等公民.

1 函数是对象, 就可以使用对象的动态特性

2 函数是对象, 就有构造函数创建函数

3 函数是函数, 可以创建其他对象

4 函数是唯一可以限定变量作用域的结构

要解决的问题

1 Function 如何使用

2 Function 与函数的关系

3 函数的原型链结构

Function的使用

语法

//Function函数所有的参数全都是字符串
//Function函数的作用就是将所有的参数组合起来,变成一个函数
//1、如果只传一个参数,那么这个函数必然是函数体
//2、如果传多个参数,那么最后一个参数表示函数体,前面的参数代表将要创建的函数的参数
//3、如果不传参数,表示创建一个空函数
new Function(arg1, arg2, arg3, ..., argN, body);

创建一个打印一句话的函数

//传统的方式
function foo(){
    console.log("你好");
}

//使用Function
var func = new Function("console.log('你好');");

这里两种方式创建出来的函数功能是一样的。
创建一个空函数

//传统的方式
function foo(){}

//Function
var func = new Function();

创建一个有参数的函数

//传统的方式
function foo(num){
    console.log(num);
}

//Function

var func = new Function(){"num", "console.log(num);"};

练习: 利用 Function 创建一个函数, 要求传入两个数字, 打印其和

var func = new Function(
    'num1',
    'num2',
    'console.log( num1 + num2 );'
);

练习: 利用 Function 创建一个函数, 要求允许函数调用时传入任意个数参数, 并且函数返回这些数字中最大的数字. 练习: 利用 Function 创建一个求三个数中最大数的函数.

// 传统
function foo ( a, b, c ){
    var res = a > b ? a : b;
    res = res > c ? res : c;
    return res;
}

// Function

var func = new Function( 'a',
    'b',
    'c',
    'var res = a > b ? a : b;res = res > c ? res : c;return res;' )
解决代码太长的问题

1.利用+连接字符串

var func = new Function( 'a', 'b', 'c',

 'var res = a > b ? a : b;' +

 'res = res > c ? res : c;' +

 'return res;' );

1 . 利用字符串特性

function foo ( a, b, c ) {
    var res = a > b ? a : b;
    res = res > c ? res : c;
    return res;
}
var func = new Function( 'a', 'b', 'c', 'return foo( a, b, c );');

1 . ES6 语法(很少有浏览器实现) 使用键盘左上角的` 表示可换行字符串的界定符,之前我们用的是单引号或者双引号来表示一个字符串字面量,在ES6中可以用反引号来表示该字符串可换行。

2. (最终)利用 DOM 的特性完成该方法

<div id="code" style="display:none">
var res = a > b ? a : b;
res = res > c ? res : c;
return res;
</div>


<script>

var txt = document.getElementbyId("code).innerHtml + ' ';

var func = new Function('a', 'b', 'c', txt);

</script>
静态成员与实例成员的概念

静态成员和实例成员这两个概念其实也是从面相对象的编程语言中引入的,对应到JavaScript中的理解为:

静态成员

静态成员是指静态属性和静态方法,所谓静态,就是有构造函数提供的。

实例成员

实例成员是值实例属性和实例方法,所谓实例,就是由构造函数创建出来的对象。

举例说明:

function Person(){
    this.name = "zs",
    this.sayHello = function(){
        console.log("Hello World");
    }
}

//下面这个sayHi方法就是构造函数自己的方法,也就是静态方法
Person.sayHi = function(){
    console.log("I'm a Person");
}

//原型属性属于构造函数,所以原型属性是静态属性
Person.prototype = {};
var p = new Person();

//这里的name是构造函数创建出来的实例对象的属性,所以是实例属性
p.name = "李四";


//这里的sayHello也是构造函数创建出来的实例对象的方法,所以是实例方法
p.sayHello();

提示:

一般工具型方法都有静态成员提供, 一般与实例对象有关的方法由实例成员表示.

工具方法:比如jQuery.Ajax()、jQuery.trim()、jQuery.Each()

arguments对象

在每一个函数调用的过程中, 函数代码体内有一个默认的对象arguments, 它存储着实际传入的所有参数。

arguments是一个伪数组对象. 它表示在函数调用的过程中传入的所有参数的集合。在函数调用过程中不规定参数的个数与类型, 可以使得函数调用变得非常灵活性。

JavaScript中的函数并没有规定必须如何传参:

1 定义函数的时候不写参数, 一样可以调用时传递参数
2 定义的时候写了参数, 调用的时候可以不传参
3 定义的时候写了一个参数, 调用的时候可以随意的传递多个而参数
在代码设计中, 如果需要函数带有任意个参数的时候, 一般就不带任何参数, 所有的参数利用arguments对象来获取. 一般的函数定义语法, 可以写成:

function foo ( /* ... */ ) {
}

练习:

利用 Function 创建一个函数, 要求允许函数调用时传入任意个数参数, 并且函数返回这些数字中最大的数字.

function foo ( ) {
// 所有的参数都在 arguments 中. 将其当做数组使用
// 问题已转换成在有一个数组中求最大值
var args = arguments;
var max = args[ 0 ];
    for ( var i = 1; i < args.length; i++ ) {
        if ( max < args[ i ] ) {
            max = args[ i ];
        }
    }
    return max;
}

练习:

利用 Function 写一个函数, 要求传入任意个数字 求和

3,JavaScript的高级知识

递归

什么是递归
在程序中,所谓的递归,就是函数自己直接或间接的调用自己。调用自己分两种:

1 .直接调用自己

2 .间接调用自己

就递归而言最重要的就是跳出结构,因为跳出了才可以有结果.
化归思想
化归思想:将一个问题由难化易,由繁化简,由复杂化简单的过程称为化归,它是转化和归结的简称。

递归思想就是将一个问题转换为一个已解决的问题来实现

假如有一个函数f, 如果它是递归函数的话, 那么也就是说函数体内的问题还是转换为 f的形式.

function f() {
    ... f( ... ) ...
}

例子
1, 2, 3, 4, 5, ..., 100 求和
1 首先假定递归函数已经写好, 假设是foo. 即foo(100)就是求1到100的和

2 寻找递推关系. 就是n与n-1, 或n-2之间的关系:foo( n ) == n + foo( n - 1 )

var res = foo(100);
var res = foo(99) + 100;

将递推结构转化为递归体

function foo(n){
    return n + foo( n - 1 );
}

上面就是利用了化归思想:

  • 将 求 100 转换为 求 99

  • 将 求 99 转换为 求 98

  • ...

  • 将求 2 转换为 求 1

  • 求 1 结果就是 1

  • 即: foo( 1 ) 是 1
    **将临界条件加到递归体中(求1的结果为1)

function foo( n ) {
    if ( n == 1 ) return 1;
    return n + foo( n - 1 );
}

练习
求 1, 3, 5, 7, 9, ... 第n项的结果与前n项和. 序号从0开始
先看求第n项
1 首先假定递归函数已经写好, 假设是fn. 那么第n项就是fn(n)

2 找递推关系:fn(n) == f(n-1) + 2

3 递归体

function fn(n) {
    return fn(n-1) + 2;
}

1 找临界条件

  • 求 n -> n-1

  • 求 n-1 -> n-2

  • ...

  • 求 1 -> 0

  • 求 第 0 项, 就是 1

  • 加入临界条件

function fn( n ) {
    if ( n == 0 ) return 1;
    return fn( n-1 ) + 2;
}

再看求前n项和
1 假设已完成, sum( n ) 就是前 n 项和

2 找递推关系: 前 n 项和 等于 第 n 项 + 前 n-1 项的和

3 递归体

function sum( n ) {
    return fn( n ) + sum( n - 1 );
}

1 找临界条件

  • n == 1结果为 1

2 加入临界条件

function sum( n ) {
    if (n == 0) return 1;
    return fn(n) + sum(n - 1);
}

练习

2, 4, 6, 8, 10 第 n 项与 前 n 项和

解题方法和上一题一样。

练习

现有数列: 1, 1, 2, 4, 7, 11, 16, … 求 第 n 项, 求前 n 项和.

求第n项`

1 假设已经得到结果 fn, fn( 10 ) 就是第 10 项

2 找递推关系

  • 0, 1 => fn( 0 ) + 0 = fn( 1 )

  • 1, 2 => fn( 1 ) + 1 = fn( 2 )

  • 2, 3 => fn( 2 ) + 2 = fn( 3 )

  • ...

  • n-1, n => fn( n-1 ) + n - 1 = fn( n )

3 递归体也就清楚了

4 临界条件是 n == 0 => 1

function fn( n ) {
    if ( n == 0 ) return 1;
    return fn( n-1 ) + n - 1;
}

前n项和

function sum( n ) {
    if ( n == 0 ) return 1;
    return sum( n - 1 ) + fn( n );
}

练习

Fibonacci 数列: 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, … 求其第 n 项.

递推关系:fn(n) == fn(n-1) + fn(n - 2)

function fib( n ) {
    if ( n == 0 || n == 1 ) return 1;
    return fib( n - 1 ) + fib( n - 2 );
}

练习

阶乘:一个数字的阶乘表示的是从 1 开始 累乘到这个数字. 例如 3! 表示 1 2 3. 5! 就是 1 2 3 4 5. 规定 0 没有阶乘, 阶乘从1开始。

求n的阶乘

function foo ( n ) {
    if ( n == 1 ) return 1;
    return foo( n - 1 ) * n;
}

练习

求幂

  • 求幂就是求 某一个数 几次方

  • 2*2 2 的 平方, 2 的 2 次方

  • 求 n 的 m 次方

  • 最终要得到一个函数 power( n, m )

  • n 的 m 次方就是 m 个 n 相乘 即 n 乘以 (m-1) 个 n 相乘

function power ( n, m ) {
    if ( m == 1 ) return n;
    return power( n, m - 1 ) * n;
}

词法作用域

,表示的是一个范围,作用域,就是作用范围。

作用域说明的是一个变量可以在什么地方被使用,什么地方不能被使用。

块级作用域
JavaScript中没有块级作用域

{
    var num = 123;
    {
        console.log( num );
    }
}
console.log( num );

上面这段代码在JavaScript中是不会报错的,但是在其他的编程语言中(C#、C、JAVA)会报错。

这是因为,在JavaScript中没有块级作用域,使用{}标记出来的代码块中声明的变量num,是可以被{}外面访问到的。

但是在其他的编程语言中,有块级作用域,那么{}中声明的变量num,是不能在代码块外部访问的,所以报错。

词法作用域
什么是词法作用域?
词法( 代码 )作用域, 就是代码在编写过程中体现出来的作用范围. 代码一旦写好, 不用执行, 作用范围就已经确定好了. 这个就是所谓词法作用域.
在 js 中词法作用域规则:

  • 函数允许访问函数外的数据.

  • 整个代码结构中只有函数可以限定作用域.

  • 作用域规则首先使用提升规则分析

  • 如果当前作用规则中有名字了, 就不考虑外面的名字

例子1:

var num = 123;
function foo() { 
console.log( num );
}
foo();

例子2:

if ( false ) {
 var num = 123;
}
console.log( num ); // undefiend

例子3:

var num = 123;
function foo() { 
var num = 456;
 function func() {
 console.log( num );
 }
 func();
}
foo();

练习:

var num1 = 123;
function foo1() {
 var num1 = 456;
 function foo2() {
 num1 = 789;
 function foo3 () {
 console.log( num1 );
 } 
foo3();
 }
 foo2();
}
foo1();
console.log( num1 );

面试题

var num = 123;
function func1(){ 
console.log(num);
}
function func2(){
 var num = 456; 
func1();
}

变量名提升

JavaScript是解释型的语言,但是他并不是真的在运行的时候逐句的往下解析执行。

我们来看下面这个例子:

func();

function func(){
     alert("Funciton has been called");
}

在上面这段代码中,函数func的调用是在其声明之前,如果说JavaScript代码真的是逐句的解析执行,那么在第一句调用的时候就会出错,然而事实并非如此,上面的代码可以正常执行,并且alert出来Function has been called。

所以,可以得出结论,JavaScript并非仅在运行时简简单单的逐句解析执行!

JavaScript 预解析

JavaScript引擎在对JavaScript代码进行解释执行之前,会对JavaScript代码进行预解析,在预解析阶段,会将以关键字var和function开头的语句块提前进行处理。

关键问题是怎么处理呢?

当变量和函数的声明处在作用域比较靠后的位置的时候,变量和函数的声明会被提升到作用域的开头。

重新来看上面的那段代码

func();
function func(){
     alert("Funciton has been called");
}

由于JavaScript的预解析机制,上面的代码就等效于:


function func(){
    alert("Funciton has been called");
}
func();

看完函数声明的提升,再来看一个变量声明提升的例子:

alert(a);
var a = 1;

由于JavaScript的预解析机制,上面这段代码,alert出来的值是undefined,如果没有预解析,代码应该会直接报错a is not defined,而不是输出值。

Wait a minute,不是说要提前的吗?那不是应该alert出来1,为什么是undefined?

行为 说明
声明 告诉编译器/解析器有这个变量存在,这个行为是不分配内存空间的,在JavaScript中,声明一个变量的操作为:var a;
定义 为变量分配内存空间,在C语言中,一般声明就包含了定义,比如:int a;,但是在JavaScript中,var a;这种形式就只是声明了。
初始化 在定义变量之后,系统为变量分配的空间内存储的值是不确定的,所以需要对这个空间进行初始化,以确保程序的安全性和确定性
赋值 赋值就是变量在分配空间之后的某个时间里,对变量的值进行的刷新操作(修改存储空间内的数据)

那么在这里有必要说一下声明、定义、初始化的区别。其实这几个概念是C系语言的人应该都比较了解的。

行为 说明
声明 告诉编译器/解析器有这个变量存在,这个行为是不分配内存空间的,在JavaScript中,声明一个变量的操作为:var a;
定义 为变量分配内存空间,在C语言中,一般声明就包含了定义,比如:int a;,但是在JavaScript中,var a;这种形式就只是声明了。
初始化 在定义变量之后,系统为变量分配的空间内存储的值是不确定的,所以需要对这个空间进行初始化,以确保程序的安全性和确定性
赋值 赋值就是变量在分配空间之后的某个时间里,对变量的值进行的刷新操作(修改存储空间内的数据)

所以我们说的提升,是声明的提升。

那么再回过头看,上面的代码就等效于:

var a; //这里是声明
alert(a);//变量声明之后并未有初始化和赋值操作,所以这里是 undefined
a = 1;

复杂点的情况分析
通过上一小节的内容,我们对变量、函数声明提升已经有了一个最基本的理解。那么接下来,我们就来分析一些略复杂的情况。

函数同名

观察下面这段代码:

func1();
function func1(){
     console.log('This is func1');
}

func1();
function func1(){
     console.log('This is last func1');
}

输出结果为:

This is last func1
This is last func1

原因分析:由于预解析机制,func1的声明会被提升,提升之后的代码为:

function func1(){
     console.log('This is func1');
}

function func1(){
     console.log('This is last func1');
}

func1();
func1();

同名的函数,后面的会覆盖前面的,所以两次输出结果都是This is last func1。

变量和函数同名

alert(foo);
function foo(){}
var foo = 2;

当出现变量声明和函数同名的时候,只会对函数声明进行提升,变量会被忽略。所以上面的代码的输出结果为

function foo(){}

我们还是来吧预解析之后的代码展现出来:

function foo(){};
alert(foo);
foo = 2;

再来看一种

var num = 1;
function num () {
     alert( num );
}
num();

代码执行结果为:

Uncaught TypeError: num is not a function

直接上预解析后的代码:

function num(){
     alert(num);
}

num = 1;
num();

预解析是分作用域的

声明提升并不是将所有的声明都提升到window对象下面,提升原则是提升到变量运行的环境(作用域)中去。

function showMsg()
{
    var msg = 'This is message';
}
alert(msg); // msg未定义

还是直接把预解析之后的代码写出来:

function showMsg()
{
    var msg;
    msg = 'This is message';
}
alert(msg); // msg未定义
预解析是分段的

分段,其实就分script标签的

<script>
func(); // 输出 AA2;
function func(){
    console.log('AA1');
}

function func(){
    console.log('AA2');
}
</script>

<script>
function func(){
    console.log('AA3');
}
</script>

在上面代码中,第一个script标签中的两个func进行了提升,第二个func覆盖了第一个func,但是第二个script标签中的func并没有覆盖上面的第二个func。所以说预解析是分段的。

tip:但是要注意,分段只是单纯的针对函数,变量并不会分段预解析。

函数表达式并不会被提升
func();
var func = function(){
    alert("我被提升了");
};

这里会直接报错,func is not a function,原因就是函数表达式,并不会被提升。只是简单地当做变量声明进行了处理,如下:

var func;
func();
func = function(){
    alert("我被提升了");
}
条件式函数声明
console.log(typeof func);
if(true){
    function(){
        return 1;
    }
}
console.log(typeof func);

上面这段代码,就是所谓的条件式函数声明,这段代码在Gecko引擎中打印"undefined""function";而在其他浏览器中则打印"function""function"

原因在于Gecko加入了ECMAScript以外的一个feature:条件式函数声明。

Conditionally created functions Functions can be conditionally declared, that is, a function declaration can be nested within an if statement.

Note: Although this kind of function looks like a function declaration, it is actually an expression (or statement), since it is nested within another statement. See differences between function declarations and function expressions.

Note中的文字说明,条件式函数声明的处理和函数表达式的处理方式一样,所以条件式函数声明没有声明提升的特性。

作用域链

什么是作用域链

只有函数可以制造作用域结构, 那么只要是代码,就至少有一个作用域, 即全局作用域。

凡是代码中有函数,那么这个函数就构成另一个作用域。如果函数中还有函数,那么在这个作用域中就又可以诞生一个作用域。

将这样的所有的作用域列出来,可以有一个结构: 函数内指向函数外的链式结构。就称作作用域链。
例如:

function f1() {
    function f2() {
    }
}

var num = 456;
function f3() {
    function f4() {    
    }
}
作用域链.jpg

绘制作用域链的步骤:
1 看整个全局是一条链, 即顶级链, 记为 0 级链

2 看全局作用域中, 有什么成员声明, 就以方格的形式绘制到 0 级练上

3 再找函数, 只有函数可以限制作用域, 因此从函数中引入新链, 标记为 1 级链

4 然后在每一个 1 级链中再次往复刚才的行为
变量的访问规则

  • 首先看变量在第几条链上, 在该链上看是否有变量的定义与赋值, 如果有直接使用

  • 如果没有到上一级链上找( n - 1 级链 ), 如果有直接用, 停止继续查找.

  • 如果还没有再次往上刚找... 直到全局链( 0 级 ), 还没有就是 is not defined

  • 注意,同级的链不可混合查找
    练习:绘制作用域链

function f1() {
    var num = 123;
    function f2() {
        console.log( num );
    }
    f2();
}

var num = 456;
f1();
作用域链02.jpg

如何分析代码

  • 在分析代码的时候切记从代码的运行进度上来分析, 如果代码给变量赋值了, 一定要标记到图中
  • 如果代码比较复杂, 可以在图中描述代码的内容, 有事甚至需要将原型图与作用域图合并分析
    练习
var num = 123;
function f1() {
    console.log( num );
}

function f2() {
    var num = 456;
    f1();
}
f2();
作用域链03.jpg

练习

var num = 123;

function f1() {
    console.log( num );
}

function f2() {
    num = 456;
    f1();
}

f2();

补充
声明变量使用var, 如果不使用var声明的变量就是全局变量( 禁用 )

因为在任何代码结构中都可以使用该语法. 那么再代码维护的时候会有问题. 所以除非特殊原因不要这么用.

下面的代码的错误

function foo () {
    var i1 = 1 // 局部
    i2 = 2, // 全局
    i3 = 3; // 全局
}
此时注意

var arr = [];
for ( var i = 0; i < 10; i++ ) {
    arr.push( i );
}

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

// 一般都是将变量的声明全部放到开始的位置, 避免出现因为提升而造成的错误
var arr = [],
i = 0;

for ( ; i < 10; i++ ) {
    arr.push( i );
}

for ( i = 0; i < 10; i++ ) {
    console.log( arr[ i ] );
}

闭包

闭包的概念

闭包从字面意思理解就是闭合, 包起来.

简单的来说闭包就是,一个具有封闭的对外不公开的, 包裹结构, 或空间.

在JavaScript中函数可以构成闭包. 一般函数是一个代码结构的封闭结构, 即包裹的特性, 同时根据作用域规则, 只允许函数访问外部的数据, 外部无法访问函数内部的数据, 即封闭的对外不公开的特性. 因此说函数可以构成闭包.

闭包要解决什么问题?
  • 闭包内的数据不允许外界访问
  • 要解决的问题就是间接访问该数据

函数就可以构成闭包, 要解决的问题就是访问到函数内部的数据

我们观察下面的函数foo,在foo内部有一个变量num,能否在函数外部访问到这个变量num呢?

function foo () {
    var num = 123;
    return num;
}

var res = foo();
console.log( res ); // => 123

分析:

在上面的代码中,确实可以访问到num这个函数内部的变量。但是能不能多次访问呢?

不能,因为每次访问都得重新调用一次foo函数,每次调用都会重新创建一个num = 123,然后返回。

解决思路

函数内的数据不能直接在函数外被访问,是因为作用域的关系,上级作用域不能直接访问下级作用域中的数据。

但是如果反过来,下级作用域可以直接访问上级作用域中的数据。那么如果在函数foo内定义一个函数,那么在这个内部函数中是可以直接访问foo中的num的。

function foo() {
    var num = Math.random();    
    function func() {
        return num;    
    }
    return func;
}


var f = foo();
// f可以直接访问num,而且多次访问,访问的也是同一个,并不会返回新的num
var res1 = f();
var res2 = f();

如何获得超过一个数据

函数的返回值只能有一个,那按照上面的方法,我们只能对函数内部的一个数据进行操作。怎么操作函数内的多个数据呢?

可以使用对象,代码如下:

function foo () {
    var num1 = Math.random();
    var num2 = Math.random();
    //可以将多个函数包含在一个对象内进行返回,这样就能在函数外部操作当前函数内的多个变量
    return {
        num1: function () {
            return num1;
        },
        num2: function () {
            return num2;
        }
    }
}
如何完成读取一个数据和修改这个数据

前面讲的都是如何去获取函数内部的数据,接下来我们考虑如何修改函数内部的数据。

同样,也是使用内部的函数进行操作。

function foo() {
    var num = Math.random();
    //分别定义get和set函数,使用对象进行返回
    return {
        //get_num负责获取数据
        get_num: function() {    
            return num;
        },
        //set_num负责设置数据
        set_num: function(value) {
            num = value;
        }
    }
}
闭包的基本结构

一般闭包要解决的的问题就是要想办法间接的获得函数内数据的使用权. 那么我们的可以总结出一个基本的使用模型.

  • 写一个函数, 函数内定义一个新函数, 返回新函数, 用新函数获得函数内的数据
  • 写一个函数, 函数内定义一个对象, 对象中绑定多个函数( 方法 ), 返回对象, 利用对象的方法访问函数内的数据

函数的四种调用模式

函数模式

特征:就是一个简单的函数调用,函数名前面没有任何的引导内容

function foo(){}
var func = function(){}

foo();
func();
(function(){})();

this在函数模式中的含义: this在函数中表示全局对象,在浏览器中是window对象

方法模式

特征: 方法一定是依附于一个对象, 将函数赋值给对象的一个属性, 那么就成为了方法.

function f() {
    this.method = function () {};
}

var o = {
    method: function () {}
}

this在方法模式调用中的含义:表示函数所依附的这个对象

构造器调用模式

由于构造函数只是给 this 添加成员. 没有做其他事情. 而方法也可以完成这个操作, 就 this 而言, 构造函数与方法没有本质区别.

特征:使用 new 关键字, 来引导构造函数.

function Person(){
    this.name = "zhangsan";
    this.age = 19;
    this.sayHello = function(){
    };
}

var p = new Person();

构造函数中发this与方法中一样, 表示对象, 但是构造函数中的对象是刚刚创建出来的对象

关于构造函数中return关键字的补充说明
  • 构造函数中不需要return, 就会默认的return this

  • 如果手动的添加return, 就相当于 return this

  • 如果手动的添加return 基本类型; 无效, 还是保留原来 返回this

  • 如果手动添加return null; 或return undefiend, 无效

  • 如果手动添加return 对象类型; 那么原来创建的this就会被丢掉, 返回的是 return后面的对象

创建对象的模式

工厂方法


// 工厂就是用来生产的, 因此如果函数创建对象并返回, 就称该函数为工厂函数
function createPerson( name, age, gender ) {
    var o = {};
    o.name = name;
    o.age = age;
    o.gender = gender;
    return o;
}
// document.createElement()

构造方法


function Person(name, age, gender){
    this.name = name;
    this.age = age;
    this.gender = gender;
}

var p = new Person("zhangsan", 19, "男");

寄生式创建对象

function Person(name, age, gender){
    var o = {};
    o.name = name;
    o.age = age;
    o.gender = gender;
    return o;
}

var p = new Person("Jack", 18, "male");

混合式创建

混合式继承就是讲所有的属性放在构造方法里面,然后讲所有的方法放在原型里面,使用构造方法和原型配合起来创建对象。

上下文调用模式
上下文(Context),就是函数调用所处的环境。

上下文调用,也就是自定义设置this的含义。

在其他三种调用模式中,函数/方法在调用的时候,this的值都是指定好了的,我们没办法自己进行设置,如果尝试去给this赋值,会报错。

上下文调用的语法
//第一种, apply

函数名.apply(对象, [参数]);

//第二种, call

函数名.call(对象, 参数);

//上面两种方式的功能一模一样,只是在传递参数的时候有差异。

功能描述:

  • 语法中的函数名表示的就是函数本身,使用函数调用模式的时候,this默认是全局对象

  • 语法中的函数名也可以是方法(如:obj.method),在使用方法模式调用的时候,this默认是指当前对象

  • 在使用apply和call的时候,默认的this都会失效,this的值由apply和call的第一个参数决定

补充说明

  • 如果函数或方法中没有this的操作, 那么无论什么调用其实都一样.

  • 如果是函数调用foo(), 那么有点像foo.apply( window ).

  • 如果是方法调用o.method(), 那么有点像o.method.apply( o ).

参数问题

callapply在没有后面的参数的情况下(函数无参数, 方法无参数) 是完全一样的.

如下:


function foo() {

 console.log( this );

}



foo.apply( obj );

foo.call( obj );

第一个参数的使用规则:

1 如果传入的是一个对象, 那么就相当于设置该函数中的 this 为参数

2 如果不传入参数, 或传入 null. undefiend 等, 那么相当于 this 默认为 window

foo();

foo.apply();

foo.apply( null );

foo.call( undefined );

1 如果传入的是基本类型, 那么 this 就是基本类型对应的包装类型的引用

  • number -> Number

  • boolean -> Boolean

  • string -> String

第二个参数的使用规则

在使用上下文调用的时候, 原函数(方法)可能会带有参数, 那么这个参数在上下文调用中使用第二个( 第 n 个 )参数来表示


function foo( num ) {

 console.log( num );

}

foo.apply( null, [ 123 ] );


// 等价于

foo( 123 );
上下文调用模式的应用

上下文调用只是能修改this, 但是使用的最多的地方上是函数借用.

1. 将伪数组转换为数组

传统的做法:

var a = {};
a[ 0 ] = 'a';
a[ 1 ] = 'b';
a.length = 2;

// 使用数组自带的方法 concat
// 如果参数中有数组会把参数数组展开
// 语法: arr.concat( 1, 2, 3, [ 4, [ 5 ] ] );
// 特点:不修改原数组
var arr = [];
var newArr = arr.concat( a );

由于a是伪数组, 只是长得像数组. 所以上面的代码不能成功,不能使用concat方法。

但是apply方法有一个特性, 可以将数组或伪数组作为参数。(IE8不支持伪数组操作)

foo.apply( obj, 伪数组 ); // IE8 不支持

利用apply方法,可以写出以下

//将伪数组 a 作为 apply 的第二个参数
var newArr = Array.prototype.concat.apply( [], a )

处理数组转换, 实际上就是将元素一个一个的取出来构成一个新数组, 凡是涉及到该操作的方法理论上都可以。

push方法

//用法:
arr.push( 1 ); //将这个元素加到数组中, 并返回所加元素的个数
arr.push( 1, 2, 3 ); //将这三个元素依次加到数组中, 返回所加个数

var a = { length: 0 }; // 伪数组
a[ a.length++ ] = 'abc'; // a[ 0 ] = 'abc'; a.length++;
a[ a.length++ ] = 'def';

// 使用一个空数组, 将元素一个个放到数组中即可
var arr = [];
arr.push( a ); // 此时不会将元素展开, 而是将这个伪数组作为一个元素加到数组中
// 再次利用 apply 可以展开伪数组的特征
arr.push.apply( arr, a );
// 利用 apply 可以展开伪数组的特性, 这里就相当于 arr.push( a[0], a[1] )

2. 求数组中的最大值

传统的做法

var max = arr[ 0 ];
for ( var i = 1; i < arr.length; i++ ) {
    if ( arr[ i ] > max ) {
        ...
    }
}
在 js 中的Math对象中提供了很多数学函数Math.max( 1,2,3 )

还是利用 apply 可以展开数组的特性

var arr = [ 123456,12345,1234,345345,234,5 ];
Math.max.apply( null, arr );

3.借用构造函数继承

function Person ( name, age, gender ) {
    this.name = name;
    this.age = age;
    this.gender = gender;
}

// 需要提供一个 Student 的构造函数创建学生对象
// 学生也应该有 name, age, gender, 同时还需要有 course 课程
function Student ( name, age, gender, course ) {
    Person.call( this, name, age, gender );
    this.course = course;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容