继承
一、混入式继承
for in
使用for in遍历对象1的属性,将所有的属性添加到另外一个对象2上
这时候就可以称 对象2 继承自 对象1
二、原型继承
- 利用原型中的成员可以被和其相关的对象共享这一特性,可以实现继承,这种实现继承的方式就是原型继承:
- 一、利用对象的动态特性,为原型对象添加成员,不是严格意义上的继承;如下例中的p对象继承自原型对象。
function Person(name,age){
this.name = name;
this.age = age;
}
Person.prototype.sayHello = function(){
console.log("我想死你啦");
}
var p = new Person("冯巩",50);
p.sayHello();
- 二、直接替换原型对象(原型的使用方式):
- 有风险,在替换之后,原有的成员都会丢失;
- 替换原型对象的时候,需要手动去指定原型对象的construtor属性;
function Person(name,age){
this.name = name;
this.age = age;
}
var parent = {
sayHello:function(){
console.log("我想死你啦");
}
}
//让p继承自parent,替换原型对象即可
Person.prototype = parent;
var p = new Person("冯巩",50);
p.sayHello();
- 三、利用混入给原型对象添加成员
function Person(name,age){
this.name = name;
this.age = age;
}
var parent = {
sayHello:function(){
console.log("朋友们还好吗?");
}
}
//混入式继承
for(k in parent){
Person.prototype[k] = parent[k];
}
var p = new Person("冯巩",50);
p.sayHello();
继承的应用
- 对内置对象进行扩展:
var arr = [1,2,3];
Array.prototype.sayHello = function(){
console.log("我是数组);
}
arr.sayHello();
- 但是,如果直接修改内置对象的原型,会影响到整个开发团队。
- 如何安全的扩展一个内置对象:
function MyArray(){
//添加自己的属性
this.name = "我是一个数组";
this.sayHello = function(){
console.log("你好,我是数组");
}
}
var arr = new Array();
//继承之后(替换原型,我的数组中就有了原生数组的所有属性和方法)
MyArray.prototype = arr;
var myArr = new MyArray();
三、经典继承
var 对象1 = Object.create(对象2);
这个时候,创建出来的对象1继承自对象2
Object.create方法是ES5中提出来的,存在兼容性问题
如何解决?
1.检测浏览器是否支持Object.create方法,如果不支持,直接手动给Object添加create方法
2.自定义函数,在函数内部判断浏览器是否支持Object.create方法,如果不支持,则手动创建对象返回,否则直接调用
function create(obj){
if(Object.create){
return Object.create(obj);
}else{
function F(){
}
F.prototype = obj;
return new F();
}
}
原型链
什么是原型链
每个构造函数都有原型对象,每个对象都有构造函数,每个构造函数的原型对象都是对象,那么这个原型对象也会有构造函数,那么这个原型对象的构造函数也会有原型对象,这样就会形成一个链式的结构,我们称之为原型链。
原型结构的基本形式
function Person(){}
var p = new Person();
p ---> Person.prototype ---> Object.prototype ---> null
- 属性搜索原则:
- 当访问一个对象的成员的时候,会先在自身查找,如果找到直接使用;
- 如果没有找到,就去当前对象的原型对象中去查找,如果找到就直接使用;
- 如果没有找到,继续在原型对象的原型对象中查找,如果找到了就直接使用;
- 如果还没有找到,就继续向上查找,直到查找到Object.prototype,如果还没有找到:是属性的话就返回undefined,是方法的话就报错。
原型继承是什么?
通过修改原型链的结构,实现的继承方式就是原型继承
对象和原型的成员关系
function Person(){};
var p = new Person();
- p对象中包含的成员有:Person.prototype中的成员和自身拥有成员;
- Person.prototype中的成员有:Object.prototype的成员和自身的成员
- p对象可以访问Person.prototype和Object.prototype中的所有成员
Object.prototype的成员
- constructor:指向和该原型相关的构造函数;
- hasOwnProperty方法:判断对象本身是否拥有某个属性;
- isPrototypeOf方法:判断是否是对象的原型对象;
- properIsEnumerable方法:1.判断属性是否属于对象本身,2.判断属性是否可以被遍历;返回的结果为:1&&2
- toString:将对象转换成字符串; toLocalString转换成字符串的时候应用的本地的设置格式;
- valueOf 方法:在对象参与运算的时候,首先调用valueOf方法获取对象的值,如果该值无法参与运算,将会调用toString方法;
-
__proto__
属性:指向当前对象的原型对象的属性,可以使用对象.__proto__
去访问对象的原型对象。
Function
3种创建函数的方式
* 直接声明函数:function fn(){}
* 函数表达式:var fn = function(){}
* new Function:var fn = new Function();
可以用Function来创建函数:
语法:
//创建一个空的函数
var 函数名 = new Function();
//创建一个没有参数的函数
var 函数名 = new Function("函数体")
//当给Fucntion传多个参数的时候,最后一个参数为函数体,前面的参数为创建出来的函数的形参
var 函数名 = new Function("参数1","参数2", "参数3",..."函数体")
//Function接收的所有的参数都是字符串类型的!!!
- 创建一个没有参数的函数:
var fn = new Function("console.log('我可是函数体哟!!!')");
fn();
- 创建一个带有参数的函数:
var fn = new Function("a","b","console.log(a+b);");
fn(2,3); //5
arguments对象
- arguments对象是函数内部的一个对象,在函数调用的时候,系统会默认的将所有传入的实参存入该对象;
注意:不管有没有形参,实参都会被存入该对象。
是一个伪数组,arguments.length可以用来表示传入的实参的个数;
arguments.callee,指向函数本身。
例. 不管输入多少个数,总是输出最大的数:
直接声明函数:
function max() {
var maxValue = arguments[0];
for(var i=1; i<arguments.length; i++){
if (maxValue < arguments[i]){
maxValue = arguments[i];
}
}
return maxValue;
};
console.log(max(1, 3, 7, 9, 5));
- 函数表达式:
var max = function() {
var maxValue = arguments[0];
for(var i=1; i<arguments.length; i++){
if (maxValue < arguments[i]){
maxValue = arguments[i];
}
}
return maxValue;
};
console.log(max(1, 3, 7, 9, 5));
- new Function:
var max = new Function("a","b","var maxValue=arguments[0];" +
"for(i=1;i<arguments.length;i++){" +
"if(maxValue<arguments[i]){" +
"maxValue=arguments[i];" +
"}}" +
"return maxValue;"
);
console.log(max(1,2,3,4,59));
eval
可以将字符串转换成js代码并执行,但是不推荐使用,存在安全性问题。
var str = "var a = 10;";
eval(str);
console.log(a); //10
- 注意:当使用eval解析JSON格式字符串的时候,要注意,会将{}解析为代码块
- 1.可以在JSON格式字符串前面拼接
"var 变量名 ="
:
var obj = "{name:'yijiang',age:10};";
eval("var o = "+obj);
console.log(o);
- 2.用
()
把JSON格式字符串括起来,eval("("+JSON格式的字符串+")")
:
Function和eval的区别:
- 共同点:都可以将字符串转换成js代码;
- 不同点:
- Function创建出来的是函数,并不会直接调用,只用手动去调用才会执行;
- eval把字符串转成代码之后,直接就执行了。
静态成员和实例成员
静态成员
是指构造函数的属性和方法。通过构造函数去访问的属性和方法就是静态成员
实例成员
是指实例的属性和方法。通过对象(实例)去访问的属性和方法就是实例成员
function Person(){
this.age = 28;
this.brother = "yijiang";
}
Person.prototype = {};
//其中age、brother是实例成员;
//prototype是静态成员
//__proto__是实例成员
通常情况下:
- 把工具方法作为静态成员;
- 把跟对象相关的方法作为实例成员。
补充:
instanceof关键字:
对象 instanceof 构造函数;
:判断该构造函数的原型是否存在于该对象的原型链上。Function的原型链:
Function也可以被当做一个构造函数;
通过Function new出来的函数可以被当做是实例化的对象;
那么Function这个构造函数也有原型对象,Function的原型对象是一个空的函数;
Function的原型对象的原型对象是Object.prototype。
- Object构造函数 是通过 Function构造函数 实例化出来的;
- Function构造函数 也是通过 Function构造函数 实例化出来的(不要强行去理解,知道就好)
面向对象总复习
1.什么叫面向对象
-
面向对象是一种思想
- 把解决问题的关注点放到解决问题所需要的一系列对象上
-
面向过程是一种思想
- 把解决问题的关注点放到解决问题的每一个详细步骤上
2.面向对象的三大特性
封装
继承
多态
3.什么是对象
万物接对象
4.什么是js对象
键值对儿的集合(无序)
5.名词提炼法
一句话中的所有的名词都可以被当做对象
6.如何用js对象模拟现实生活中的对象
属性对应特征
方法对应行为
7.创建对象的方式
使用对象字面量
使用内置的构造函数Object
使用简单工厂函数(不推荐使用)
自定义构造函数
8.传统构造函数中存在的问题
如果把方法定义在构造函数中,每创建一个对象,都会新建一个方法,这样同样的代码会在内存中存在多分,造成资源浪费
9.如何解决问题8
把方法提取出来定义在全局中,在构造函数中引用该函数
使用原型来解决,原型中的所有成员都可以被所有跟其关联的对象访问
10.原型是什么
在构造函数创建的时候,系统默认的会为这个构造函数创建并关联一个对象,这个对象就是原型对象
这个原型对象默认是一个空的对象,该对象中的所有成员可以被所有通过该构造函数实例化出来的对象访问
11.原型的作用
该对象中的所有成员可以被所有通过该构造函数实例化出来的对象访问
12.原型的使用方式(实现原型继承的方式)
1.利用对象的动态特性给原型对象添加成员
2.直接替换原型对象
3.通过混入的方式给原型对象添加成员
13.原型链
每一个构造函数都有原型对象,每一个原型对象都有构造函数,这样就形成一个链式的结构,称之为原型链
14.继承的实现方式
1.混入式继承 for-in
2.原型继承 通过更改原型链的结构,实现的继承,就是原型继承
3.经典继承 Object.creat() 有兼容性问题
//var 对象名 = Object.create(要继承的对象)
15.Object.prototype的成员
constructor 属性 指向该原型相关的构造函数
hasOwnProperty 方法 判断对象本身是否拥有某个属性 obj.hasOwnProperty("属性名")
isPrototypeOf 方法 判断一个对象是不是另一个对象的原型对象 obj1.isPrototypeOf(obj2)
propertyIsEnumerable 方法 先判断属性是否属于对象本身,如果不是,返回false,如果是,就继续判断属性是否可以被遍历,如果是才返回ture 反之则false
toString toLocaleString 方法 转换成字符串 toLocaleString转换成本地格式的字符串
valueOf 方法 当对象参加运算的时候,会首先调用valueOf方法获取对象的值,如果获取的值不能参与运算,则调用toString方法
proto 属性 指向对象关联的原型对象
16.Function eval
都可以将字符串转换成代码
不同点
Function 创建出来的是函数,不会直接调用,除非手动调用
eval 直接可以将字符串转换成代码,并执行
17.arguments
函数内部的一个对象,在函数调用的时候,系统会默认的将所有传入的实参存入该对象
arguments.length 表示传入实参的个数
arguments.callee 指向当前函数 (匿名函数中使用,因为他没有名字)
案例:歌曲管理器
- 在当前对象的方法中,调用当前对象中的另一个方法,需要使用this
递归
什么是递归
在程序中,所谓的递归,就是函数自己直接或间接的调用自己。调用自己分两种:
直接调用自己;
间接调用自己。
就递归而言最重要的就是
跳出结构,
因为跳出了才可以有结果。
化归思想
化归思想:将一个问题由难化易,由繁化简,由复杂化简单的过程称为化归,它是转化和归结的简称。
递归思想就是将一个问题转换为一个已解决的问题来实现的。
假如有一个函数f,如果它是递归函数的话,,那么也就是说函数体内的问题还是转换为f的形式。
function f() {
... f( ... ) ...
}
例子:
求1,2,3,4,5...100的和。
- 首先假定递归函数已经写好,假设是foo。即foo(100)就是求1到100的和;
- 寻找递推关系,就是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项:
- 首先假定递归函数已经写好,假设是fn。 那么第n项就是fn(n)
- 找递推关系:fn(n) == f(n-1) + 2
- 递归体:
function fn(n) {
return fn(n-1) + 2;
}
- 找临界条件
求 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项和
- 假设已完成:sum( n ) 就是前 n 项和;
- 找递推关系:前n项和等于第n项 + 前n-1项的和;
- 递归体
function sum( n ) {
return fn( n ) + sum( n - 1 );
}
- 找临界条件:
- n == 1结果为 1;
- 加入临界条件:
function sum( n ) {
if (n == 0) return 1;
return fn(n) + sum(n - 1);
}
二、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!表示123。规定 0 没有阶乘, 阶乘从1开始。
- 求n的阶乘
function foo ( n ) {
if ( n == 1 ) return 1;
return foo( n - 1 ) * n;
}
案例:使用递归遍历所有的后代元素:
- DOM没有提供直接获取后代元素的API;
- 但是可以通过childNode来获取所有的子元素;
作用域
域,表示的是一个范围,作用域,就是作用范围。
作用域说明的是一个变量可以在什么地方被使用,什么地方不能被使用。
块级作用域
- 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(); //456
- 例子4:
var num1 = 123;
function foo1() {
var num1 = 456;
function foo2() {
num1 = 789;
function foo3 () {
console.log( num1 );
}
foo3();
console.log( num1 );
}
foo2();
console.log( num1 );
}
foo1();
//789
//789
//789
- 面试题
var num = 123;
function func1(){
console.log(num);
}
function func2(){
var num = 456;
func1();
}
func2(); //123[词法作用域]
变量提升
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?
- 那么在这里有必要说一下声明、定义、初始化的区别。其实这几个概念是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未定义
- 分作用域:
var msg = "aaa";
function showMsg()
{
alert(msg); // msg未定义
var msg = 'This is message';
}
var aaa = 10;
function f1() {
console.log(aaa);
aaa = 20;
}
console.log(aaa);
f1();
console.log(aaa);
//10
//10
//20
var aaa = 10;
function f1() {
console.log(aaa);
var aaa = 20;
}
console.log(aaa);
f1();
console.log(aaa);
//10
//undefined
//10
预解析是分段的
- 分段,其实就分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("我被提升了");
}
练习:
if("a" in window){
var a = 10;
}
alert(a); //10
- 上述代码预解析:
var a;
if("a" in window){
a = 10;
}
alert(a);
function fn(){
if("a" in window){
var a = 10;
}
alert(a);
}
fn(); //undefined
var foo = 1;
function bar(){
if(!foo){
var foo = 10;
}
alert(foo); //10
}
bar();
条件式函数声明
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中的文字说明,条件式函数声明的处理和函数表达式的处理方式一样,所以条件式函数声明没有声明提升的特性。
复习:
作用域:变量的作用范围;
js中的作用域是词法作用域:代码在写好之后,变量的作用域已经确定;
js中没有块级作用域。
js中只有函数可以创建作用域;
变量提升:在分析代码的时候,首先将以var声明的变量和function声明的函数进行提升;再去执行代码的具体执行过程:
变量的提升是分作用域的;
当函数和变量名相同的时候,只提升函数,不提升变量;
函数名相同,全部都会被提升,后面的函数会覆盖前面的函数;
函数表达式中函数不会被提升,但是变量会被提升。
func();
var func = function () {
console.log(11111);
}
- 上述代码执行会报错,变量提升如下:
var func;
func();
func = function () {
console.log(11111);
}
- 如下代码就不会报错:
var func = function () {
console.log(11111);
};
func();
- 并不是函数内部写了变量,这个变量就属于这个函数的作用域,而是必须使用var来声明的变量才属于这个函数作用域。
作用域链
什么是作用域链
只有函数可以制造作用域结构,那么只要是代码,就至少有一个作用域,即全局作用域。
凡是代码中有函数,那么这个函数就构成另一个作用域。如果函数中还有函数,那么在这个作用域中就又可以诞生一个作用域。
将这样的所有的作用域列出来,可以有一个结构:函数内指向函数外的链式结构。就称作作用域链。
函数内部的作用域可以访问函数外部的作用域;
如果有多个函数嵌套,那么就会构成作用域链。
- 例如:
function f1() {
function f2() {
}
}
//f2-->f1-->全局
var num = 456;
function f3() {
function f4() {
}
}
//f4-->f3-->全局
- 绘制作用域链的步骤:
- 看整个全局是一条链,即顶级链,记为 0 级链;
- 看全局作用域中,有什么成员声明,就以方格的形式绘制到 0 级练上;
- 再找函数,只有函数可以限制作用域,因此从函数中引入新链,标记为 1 级链;
- 然后在每一个 1 级链中再次往复刚才的行为。
变量的访问规则
首先看变量在第几条链上;在当前链上看是否有变量的定义与赋值,如果有直接使用;
如果没有到上一级链上找( n - 1 级链 ), 如果有直接用,停止继续查找;
如果还没有再次往上刚找... 直到全局链( 0 级 ),还没有就是 is not defined。
注意,同级的链不可混合查找。
练习1:绘制作用域链
function f1() {
var num = 123;
function f2() {
console.log( num );
}
f2();
}
var num = 456;
f1(); //123
- 练习2:
var num = 456;
function f() {
num = 678;
function foo() {
var num = 999;
console.log(num);
}
foo();
console.log(num);
}
f(); //999 678
- 练习3:变量提升会提升到函数前面
function fff() {
console.log(num);
}
fff(); //undefined
var num = 123;
function fff() {
console.log(num);
}
var num = 123;
fff(); //123
如何分析代码
- 在分析代码的时候切记从代码的运行进度上来分析,如果代码给变量赋值了,一定要标记到图中;
- 如果代码比较复杂,可以在图中描述代码的内容,有事甚至需要将原型图与作用域图合并分析。
练习
- 第一题:
var num = 123;
function f1() {
console.log( num );
}
function f2() {
var num = 456;
f1();
}
f2(); //123
- 第二题:
var num = 123;
function f1() {
console.log( num );
}
function f2() {
num = 456;
f1();
}
f2(); //456
补充
声明变量使用
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 ] );
} //0 1 2 3 4 5 6 7 8 9
// 一般都是将变量的声明全部放到开始的位置,避免出现因为提升而造成的错误
var arr = [],
i = 0;
for ( ; i < 10; i++ ) {
arr.push( i );
}
for ( i = 0; i < 10; i++ ) {
console.log( arr[ i ] );
} //0 1 2 3 4 5 6 7 8 9
闭包
闭包的概念
- 闭包从字面意思理解就是闭合,包起来。
- 简单的来说闭包就是,一个具有封闭的对外不公开的包裹结构或空间。
- 在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;
}
}
}
闭包的基本结构
- 一般闭包要解决的的问题就是要想办法间接的获得函数内数据的使用权。那么我们就可以总结出一个基本的使用模型:
- 写一个函数,函数内定义一个新函数,返回新函数,用新函数获得函数内的数据;
- 写一个函数,函数内定义一个对象,对象中绑定多个函数( 方法 ),返回对象,利用对象的方法访问函数内的数据。
闭包的作用
闭包的基本作用:可以通过闭包返回的函数或者方法,来修改函数内部的数据。
- 在函数外部想要修改数据,只能通过函数内部的方法;
- 我们可以在函数内部定义的这个方法里,设置安全措施,比如检验之类的操作,可以保证系统的安全性和稳定性。
复习
使用递归获取后代元素
作用域
变量起作用的范围;
什么是块级作用域:
JS中没有块级作用域,使用代码块限定的作用域就是块级作用域。
JS中的作用域叫做词法作用域:
在代码写好的时候,就能确定变量的作用域叫词法作用域。
动态作用域(是词法作用域就不可能是动态作用域)。
在JS中,只有函数能创造作用域。
变量提升
JS代码的运行分两个阶段:
预解析阶段:变量名和函数名提升(将var声明的变量和function声明的函数提升到当前作用域的最上方);
执行阶段。
注:
变量名和函数名相同的时候,只提升函数名,不提升变量名;
函数名相同的时候,都提升,但是后面的函数会覆盖前面的函数;
函数表达式,只会提升变量名,不会提升后面的函数;
变量提升只会将变量和函数提升到当前作用域的最上方。
变量提升是分块
<script></script>
的。条件式函数声明是否会被提升,取决于浏览器,不推荐去写
foo(); //报错
if(true){
function foo(){
console.log("123");
}
}
foo(); //123
作用域链
- 只要是函数都有作用域,函数内部的作用域都可以访问函数外部的作用域,当多个函数嵌套的时候,就会形成一个链式的结构,这个结构就是作用域链。
绘制作用域链图的步骤
- 先绘制0级作用域链;
- 在全局作用域中查找变量和函数的声明,找到之后将所有的变量和函数用小方格放在0级作用域上;
- 在0级作用域链上的函数引出1级作用域链;
- 再去每一个1级作用域链中查找变量和函数的声明,找到之后...
- 以此重复,就画好了整个作用域链。
变量搜索规则
- 首先在访问变量的作用域中查找该变量,如果找到就直接使用;
- 如果没有找到,就去上一级作用域中继续查找,如果找到就直接使用;
- 如果没有找到,就继续去上一级作用于中查找,知道找到0级为止;
- 如果找到了就用,如果没有找到就undefined(变量)或者报错(函数)。
闭包
- 闭包是一个封闭的对外不公开的包裹结构或者空间;
- JS中的闭包是函数;
- 闭包要解决的问题:在函数外部访问不到函数内部的数据;要解决的问题就是需要在函数外部间接的访问函数内部的数据。
闭包的基本结构
- 返回一个数据:
function outer(){
var data = "数据";
return function(){
return data;
}
}
- 返回多个数据:
function outer(){
var data1 = "数据1";
var data2 = "数据2";
return {
getData1:function(){
return data1;
},
setData1:function(value){
data1 = value;
return data1;
},
getData2:function(){
return data2;
},
setData2:function(value){
data2 = value;
return data2;
}
}
}
闭包的作用
- 如果把数据放在全局作用域内,那么所有人都可以随意修改,这样数据就不再可靠。
- 闭包可以创建一个私有的空间,在这个空间内部的数据,外部无法直接访问;
- 外部空间想要访问函数内部的数据,只能通过闭包提供的指定方法,在这个指定方法内部可以设置一些校验规则,让数据变得更加安全;
函数模式
特征:就是一个简单的函数调用,函数名前面没有任何的引导内容
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 ).
参数问题
call和apply在没有后面的参数的情况下(函数无参数, 方法无参数) 是完全一样的.
如下:
function foo() {
console.log( this );
}
foo.apply( obj );
foo.call( obj );
第一个参数的使用规则:
如果传入的是一个对象, 那么就相当于设置该函数中的 this 为参数
如果不传入参数, 或传入 null. undefiend 等, 那么相当于 this 默认为 window
foo();
foo.apply();
foo.apply( null );
foo.call( undefined );
如果传入的是基本类型, 那么 this 就是基本类型对应的包装类型的引用
number -> Number
boolean -> Boolean
string -> String
第二个参数的使用规则
在使用上下文调用的时候, 原函数(方法)可能会带有参数, 那么这个参数在上下文调用中使用第二个( 第 n 个 )参数来表示
function foo( num ) {
console.log( num );
}
foo.apply( null, [ 123 ] );
// 等价于
foo( 123 );
上下文调用模式的应用
上下文调用只是能修改this, 但是使用的最多的地方上是函数借用.
- 将伪数组转换为数组
传统的做法:
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] )
- 求数组中的最大值
传统的做法
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;
}