资讯专栏INFORMATION COLUMN

JavaScript 进阶知识 - 高级篇

LiuRhoRamen / 2848人阅读

摘要:汪汪汪哈士奇大黄狗输出结果为这样写依然存在问题全局变量增多,会增加引入框架命名冲突的风险代码结构混乱,会变得难以维护想要解决上面的问题就需要用到构造函数的原型概念

JS高级 前言
经过前面几篇文章的学习,相信大家已经对js有了大部分的理解了,但是要想真正的掌握好js,本篇才是关键。由于js高级阶段的知识点比较难理解,所以本篇文章花了大量的时间去理思路,有可能有一些知识点遗漏了,也有可能有部分知识点写的不对,欢迎大家留言纠正。
1.异常处理

常见的异常分类

运行环境的多样性导致的异常(浏览器)

语法错误,代码错误

异常最大的特征,就是一旦代码出现异常,后面的代码就不会执行。

1.1异常捕获
捕获异常,使用try-catch语句:
try{
    // 这里写可能出现异常的代码
}catch(e){
    // e-捕获的异常对象
    // 可以在此处书写出现异常后的处理代码
}
异常捕获语句执行的过程为:

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

catch中处理错误信息

然后继续执行后面的代码

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

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

示例代码:

console.log("代码开始执行");
try{
    console.log(num); // num 在外部是没有定义的
}catch(e){
    console.log(e);
    console.log("我已经把错误处理了");
}
console.log("代码结束执行");

效果图:

从效果图中我们可以看到,num是一个没有定义的变量,如果没有放在try-catch代码块中,后面的‘代码结束执行’就不会被打印。通过把try-catch放在代码块中,出现错误后,就不会影响后面代码的运行了,他会把错误信息打印出来。

注意:

语法错误异常用try-catch语句无法捕获,因为在预解析阶段,语法错误会直接检测出来,而不会等到运行的时候才报错。

try-catch在一般日常开发中基本用不到,但是如果要写框架什么的,用的会非常多。因为这个会让框架变得健壮

异常捕获语句的完整模式

异常捕获语句的完整模式为try-catch-finally
try {
    //可能出现错误的代码
} catch ( e ) {
    //如果出现错误就执行
} finally {
    //结束 try 这个代码块之前执行, 即最后执行
}

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

1.2抛出异常

如何手动的抛出异常呢?

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

抛出异常使用throw关键字,语法如下:

throw 异常对象;

异常对象一般是用new Error("异常消息"), 也可以使用任意对象

示例代码:

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

try{
    test();
}catch(e){
    console.log(e);
}

效果图:

1.3异常的传递机制
function f1 () {
    f2(); 
}

function f2 () {
    f3();
}

function f3() {
    throw new Error( "error" );
}
f1();  // f1 称为调用者, 或主调函数, f2 称为被调用者, 或被调函数

当在被调函数内发生异常的时候,异常会一级一级往上抛出。

2.面向对象编程
在了解面向对象编程之前,我们先来了解下什么是面向过程,什么是面向对象,他们之间的区别是什么。
2.1 面向过程和面向对象的的对比

举个例子:

日常洗衣服

1.面向过程的思维方式:

面向过程编程:将解决问题的关注点放在解决问题的具体细节上,关注如何一步一步实现代码细节;
step 1:收拾脏衣服
step 2:打开洗衣机盖
step 3:将脏衣服放进去
step 4:设定洗衣程序
step 5:开始洗衣服
step 6:打开洗衣机盖子
step 7:晒衣服

2.面向对象的思维方式:

面向对象编程:将解决问题的关注点放在解决问题所需的对象上,我们重点找对象;
人(对象)
洗衣机(对象)

在面向对象的思维方式中:我们只关心要完成事情需要的对象,面向对象其实就是对面向过程的封装;

示例代码:

在页面上动态创建一个元素
//面向过程
//1-创建一个div
var  div=document.createElement("div");
//2-div设置内容
div.innerHTML="我是div";
//3-添加到页面中
document.body.appendChild(div);

//面向对象
$("body").append("
我也是div
");

我们可以看出,jQ封装的其实就是对面向过程的封装。

总结: 面向对象是一种解决问题的思路,一种编程思想。

2.2 面向对象编程举例
设置页面中的divp的边框为"1px solid red"

1、传统的处理办法

// 1> 获取div标签
var divs = document.getElementsByTagName( "div" );
// 2> 遍历获取到的div标签
for(var i = 0; i < divs.length; i++) {
    //3> 获取到每一个div元素,设置div的样式
    divs[i].style.border = "1px dotted black";
}

// 4> 获取p标签
var ps = document.getElementsByTagName("p");
// 5> 遍历获取到的p标签
for(var j = 0; j < ps.length; j++) { 
    // 获取到每一个p元素 设置p标签的样式
    ps[j].style.border = "1px dotted black"; 
}

2、使用函数进行封装优化

// 通过标签名字来获取页面中的元素 
function tag(tagName) { 
    return document.getElementsByTagName(tagName); 
}

// 封装一个设置样式的函数 
function setStyle(arr) { 
    for(var i = 0; i < arr.length; i++) { 
        // 获取到每一个div或者p元素 
        arr[i].style.border = "1px solid #abc"; 
    } 
}
var dvs = tag("div");
var ps = tag("p");
setStyle(dvs); 
setStyle(ps);

3、使用面向对象的方式

// 更好的做法:是将功能相近的代码放到一起 
var obj = {     // 命名空间
    getEle: { 
        tag: function (tagName) { 
            return document.getElementsByTagName(tagName); 
        }, 
        id: function (idName) { 
            return document.getElementById(idName); 
        } 
        // ...
    },    
    setCss: { 
        setStyle: function (arr) { 
            for(var i = 0; i < arr.length; i++) { 
                arr[i].style.border = "1px solid #abc"; 
            } 
        }, 
        css: function() {}, 
        addClass: function() {}, 
        removeClass: function() {} 
        // ... 
    } 
    // 属性操作模块 
    // 动画模块 
    // 事件模块 
    // ... 
};

var divs = obj.getEle.tag("div");
obj.setCss.setStyle(divs);
2.3 面向对象的三大特性
面向对象的三大特性分别是:"封装""继承""多态"

1、封装性

对象就是对属性和方法的封装,要实现一个功能,对外暴露一些接口,调用者只需通过接口调用即可,不需要关注接口内部实现原理。

js对象就是“键值对”的集合

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

如果键值是函数, 那么就称为方法

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

方法是将过程封装起来

2、继承性

所谓继承就是自己没有, 别人有,拿过来为自己所用, 并成为自己的东西

2.1、传统继承基于模板

子类可以使用从父类继承的属性和方法。

class Person {
 string name;
 int age;
}

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

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

2.2、js 继承基于对象

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

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

// 参数o1和o2是两个对象,其中o1对象继承了所有o2对象的“k”属性或者方法
var o1 = {};
var o2 = {
    name: "Levi",
    age: 18,
    gender: "male"
};
function mix ( o1, o2 ) {
    for ( var k in o2 ) {
        o1[ k ] = o2[ k ];
    }
}
mix(o1, o2);
console.log(o1.name); // "Levi"

3、多态性(基于强类型,js中没有多态)只做了解

同一个类型的变量可以表现出不同形态,用父类的变量指向子类的对象。
动物 animal = new 子类(); // 子类:麻雀、狗、猫、猪、狐狸...
动物 animal = new 狗();
animal.叫();
2.4 创建对象的方式

1、字面量 {}

var student1 = {
    name:"诸葛亮",
    score:100,
    code:1,
}

var student2 = {
    name:"蔡文姬",
    score:98,
    code:2,
}

var student3 = {
    name:"张飞",
    score:68,
    code:3,
}

字面量创建方式,代码复用性太低,每一次都需要重新创建一个对象。

2、Object()构造函数

var student1 = new Object();
    student1.name = "诸葛亮";
    student1.score = 100;
    student1.code = 1;

var student2 = new Object();
    student2.name = "蔡文姬";
    student2.score = 98;
    student2.code = 2;
    
var student3 = new Object();
    student3.name = "张飞";
    student3.score = 68;
    student3.code = 3;

代码复用性太低,字面量创建的方式其实就是代替Object()构造函数创建方式的。

3、自定义构造函数

自定义构造函数,可以快速创建多个对象,并且代码复用性高。
// 一般为了区分构造函数与普通函数,构造函数名首字母大写
function Student(name,score,code){  
    this.name = name;
    this.score = score;
    this.code = code;
}

var stu1 = new Student("诸葛亮",100,1);
var stu2 = new Student("蔡文姬",98,2);
var stu3 = new Student("张飞",68,3);

构造函数语法:

构造函数名首字母大写;

构造函数一般与关键字:new一起使用;

构造函数一般不需要设置return语句,默认返回的是新创建的对象;

this指向的是新创建的对象。

构造函数的执行过程:

new关键字,创建一个新的对象,会在内存中开辟一个新的储存空间;

让构造函数中的this指向新创建的对象;

执行构造函数,给新创建的对象进行初始化(赋值);

构造函数执行(初始化)完成,会将新创建的对象返回。

构造函数的注意点:

构造函数本身也是函数;

构造函数有返回值,默认返回的是新创建的对象;

但是如果手动添加返回值,添加的是值类型数据的时候,构造函数没有影响。如果添加的是引用类型(数组、对象等)值的时候,会替换掉新创建的对象。

function Dog(){
    this.name="哈士奇";
    this.age=0.5;
    this.watch=function(){
        console.log("汪汪汪,禁止入内");
    }
    // return false;          返回值不会改变,还是新创建的对象
    // return 123;            返回值不会改变,还是新创建的对象
    // return [1,2,3,4,5];    返回值发生改变,返回的是这个数组
    return  {aaa:"bbbb"};  // 返回值发生改变,返回的是这个对象
}

var d1=new Dog();  // 新创建一个对象
console.log(d1);

构造函数可以当做普通函数执行,里面的this指向的是全局对象window

 function Dog(){
    this.name="husky";
    this.age=0.5;
    this.watch=function(){
        console.log("汪汪汪,禁止入内");
    }
    console.log(this);  // window对象
    return 1;
}
console.log(Dog());  // 打印 1
2.5 面向对象案例
通过一个案例,我们来了解下面向对象编程(案例中有一个prototype概念,可以学完原型那一章后再来看这个案例)。

需求:

实现一个MP3音乐管理案例;

同种类型的MP3,厂家会生产出成百上千个,但是每个MP3都有各自的样式、使用者、歌曲;

每个MP3都有一样的播放、暂停、增删歌曲的功能(方法);

图解:

示例代码:

    // 每个MP3都有自己的 主人:owner 样式:color 歌曲:list
    function MP3(name,color,list){
        this.owner = name || "Levi";  // 不传值时默认使用者是‘Levi’
        this.color = color || "pink";
        this.musicList = list || [
            {songName:"男人哭吧不是罪",singer:"刘德华"},
            {songName:"吻别",singer:"张学友"},
            {songName:"对你爱不完",singer:"郭富城"},
            {songName:"今夜你会不会来",singer:"黎明"}
        ];
    }
    // 所有的MP3都有 播放 暂停 音乐 增删改查的功能
    MP3.prototype = {
        // 新增
        add:function(songName,singer){
            this.musicList.push({songName:songName,singer:singer});
        },
        // 查找
        select:function(songName){
            for(var i=0;i

打印结果:

3.原型 3.1 传统构造函数存在问题

通过自定义构造函数的方式,创建小狗对象:

两个实例化出来的“小狗”,它们都用的同一个say方法,为什么最后是false呢?
function Dog(name, age) {
    this.name = name;
    this.age = age;
    this.say = function() {
        console.log("汪汪汪");
    }
}
var dog1 = new Dog("哈士奇", 1.5);
var dog2 = new Dog("大黄狗", 0.5);

console.log(dog1);
console.log(dog2);

console.log(dog1.say == dog2.say); //输出结果为false

画个图理解下:

每次创建一个对象的时候,都会开辟一个新的空间,我们从上图可以看出,每只创建的小狗有一个say方法,这个方法都是独立的,但是功能完全相同。随着创建小狗的数量增多,造成内存的浪费就更多,这就是我们需要解决的问题。

为了避免内存的浪费,我们想要的其实是下图的效果:

解决方法:

这里最好的办法就是将函数体放在构造函数之外,在构造函数中只需要引用该函数即可。
function sayFn() {
    console.log("汪汪汪");
}

function Dog(name, age) {
    this.name = name;
    this.age = age;
    this.say = sayFn();
}
var dog1 = new Dog("哈士奇", 1.5);
var dog2 = new Dog("大黄狗", 0.5);

console.log(dog1);
console.log(dog2);

console.log(dog1.say == dog2.say); //输出结果为 true

这样写依然存在问题:

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

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

想要解决上面的问题就需要用到构造函数的原型概念。

3.2 原型的概念
prototype:原型。每个构造函数在创建出来的时候系统会自动给这个构造函数创建并且关联一个空的对象。这个空的对象,就叫做原型。

关键点:

每一个由构造函数创建出来的对象,都会默认的和构造函数的原型关联;

当使用一个方法进行属性或者方法访问的时候,会先在当前对象内查找该属性和方法,如果当前对象内未找到,就会去跟它关联的原型对象内进行查找;

也就是说,在原型中定义的方法跟属性,会被这个构造函数创建出来的对象所共享;

访问原型的方式:构造函数名.prototype

示例图:

示例代码: 给构造函数的原型添加方法

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

// 给构造函数的原型 添加say方法
Dog.prototype.say = function(){
    console.log("汪汪汪");
}

var dog1 = new Dog("哈士奇", 1.5);
var dog2 = new Dog("大黄狗", 0.5);

dog1.say();  // 汪汪汪
dog2.say();  // 汪汪汪

我们可以看到,本身Dog这个构造函数中是没有say这个方法的,我们通过Dog.prototype.say的方式,在构造函数Dog的原型中创建了一个方法,实例化出来的dog1dog2会先在自己的对象先找say方法,找不到的时候,会去他们的原型对象中查找。

如图所示:

在构造函数的原型中可以存放所有对象共享的数据,这样可以避免多次创建对象浪费内存空间的问题。

3.3 原型的使用

1、使用对象的动态特性

使用对象的动态属性,其实就是直接使用prototype为原型添加属性或者方法。
function Person () {}

Person.prototype.say = function () {
    console.log( "讲了一句话" );
};

Person.prototype.age = 18;

var p = new Person();
p.say();  // 讲了一句话
console.log(p.age);  // 18

2、直接替换原型对象

每次构造函数创建出来的时候,都会关联一个空对象,我们可以用一个对象替换掉这个空对象。
function Person () {}

Person.prototype = {
    say : function () {
        console.log( "讲了一句话" );
    },
};

var p = new Person();
p.say();  // 讲了一句话

注意:

使用原型的时候,有几个注意点需要注意一下,我们通过几个案例来了解一下。

使用对象.属性名去获取对象属性的时候,会先在自身中进行查找,如果没有,就去原型中查找;

// 创建一个英雄的构造函数 它有自己的 name 和 age 属性
function Hero(){
    this.name="德玛西亚之力";
    this.age=18;
}
// 给这个构造函数的原型对象添加方法和属性
Hero.prototype.age= 30;
Hero.prototype.say=function(){
    console.log("人在塔在!!!");
}

var h1 = new Hero();
h1.say();   // 先去自身中找 say 方法,没有再去原型中查找  打印:"人在塔在!!!"
console.log(p1.name);  // "德玛西亚之力"
console.log(p1.age);   // 18 先去自身中找 age 属性,有的话就不去原型中找了

使用对象.属性名去设置对象属性的时候,只会在自身进行查找,如果有,就修改,如果没有,就添加;

// 创建一个英雄的构造函数
function Hero(){
    this.name="德玛西亚之力";
}
// 给这个构造函数的原型对象添加方法和属性
Hero.prototype.age = 18;

var h1 = new Hero();
console.log(h1);       // {name:"德玛西亚之力"}
console.log(h1.age);   // 18

h1.age = 30;           // 设置的时候只会在自身中操作,如果有,就修改,如果没有,就添加 不会去原型中操作
console.log(h1);       // {name:"德玛西亚之力",age:30}
console.log(h1.age);   // 30

一般情况下,不会将属性放在原型中,只会将方法放在原型中;

在替换原型的时候,替换之前创建的对象,和替换之后创建的对象的原型不一致!!!

// 创建一个英雄的构造函数 它有自己的 name 属性
function Hero(){
    this.name="德玛西亚之力";
}
// 给这个构造函数的默认原型对象添加 say 方法
Hero.prototype.say = function(){
    console.log("人在塔在!!!");
}

var h1 = new Hero();
console.log(h1);    // {name:"德玛西亚之力"}
h1.say();           // "人在塔在!!!"

// 开辟一个命名空间 obj,里面有个 kill 方法
var obj = {
    kill : function(){
        console.log("大宝剑");
    }
}

// 将创建的 obj 对象替换原本的原型对象
Hero.prototype = obj;

var h2 = new Hero();

h1.say();           // "人在塔在!!!"
h2.say();           // 报错

h1.kill();          // 报错
h2.kill();          // "大宝剑"

画个图理解下:

图中可以看出,实例出来的h1对象指向的原型中,只有say()方法,并没有kill()方法,所以h1.kill()会报错。同理,h2.say()也会报错。

3.4 __proto__属性
js中以_开头的属性名为js的私有属性,以__开头的属性名为非标准属性。__proto__是一个非标准属性,最早由firefox提出来。

1、构造函数的 prototype 属性

之前我们访问构造函数原型对象的时候,使用的是prototype属性:
function Person(){}

//通过构造函数的原型属性prototype可以直接访问原型
Person.prototype;
在之前我们是无法通过构造函数new出来的对象访问原型的:
function Person(){}

var p = new Person();

//以前不能直接通过p来访问原型对象

2、实例对象的 __proto__ 属性

__proto__属性最早是火狐浏览器引入的,用以通过实例对象来访问原型,这个属性在早期是非标准的属性,有了__proto__属性,就可以通过构造函数创建出来的对象直接访问原型。
function Person(){}

var p = new Person();

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

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

如图所示:

3、__proto__属性的用途

可以用来访问原型;

在实际开发中除非有特殊的需求,不要轻易的使用实例对象的__proto__属性去修改原型的属性或方法;

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

由于兼容性问题,不推荐使用。

3.5 constuctor属性
constructor:构造函数,原型的constructor属性指向的是和原型关联的构造函数。

示例代码:

function Dog(){
    this.name="husky";
}

var d=new Dog();

// 获取构造函数
console.log(Dog.prototype.constructor);  // 打印构造函数 Dog
console.log(d.__proto__.constructor);    // 打印构造函数 Dog

如图所示:

获取复杂类型的数据类型:

通过obj.constructor.name的方式,获取当前对象obj的数据类型。

在一个的函数中,有个返回值name,它表示的是当前函数的函数名;

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

var teacher = new Teacher();

// 假使我们只知道一个对象teacher,如何获取它的类型呢?
console.log(teacher.__proto__.constructor.name);  // Teacher

console.log(teacher.constructor.name);  // Teacher

实例化出来的teacher对象,它的数据类型是啥呢?我们可以通过实例对象teacher.__proto__,访问到它的原型对象,再通过.constructor访问它的构造函数,通过.name获取当前函数的函数名,所以就能得到当前对象的数据类型。又因为.__proto__是一个非标准的属性,而且实例出的对象继承原型对象的方法,所以直接可以写成:obj.constructor.name

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

继承的方式有多种,可以一个对象继承另一个对象,也可以通过原型继承的方式进行继承。

1、简单混入继承

直接遍历一个对象,将所有的属性和方法加到另一对象上。
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];
}

console.log(dog);  // 打印的对象与animal一模一样

缺点:只能一个对象继承自另一个对象,代码复用太低了。

2、混入式原型继承

混入式原型继承其实与上面的方法类似,只不过是将遍历的对象添加到构造函数的原型上。
var obj={
     name:"zs",
     age:19,
     sex:"male"
 }

function Person(){
    this.weight=50;
}

for(var k in obj){
    // 将obj里面的所有属性添加到 构造函数 Person 的原型中
    Person.prototype[k] = obj[k];
}

var p1=new Person();
var p2=new Person();
var p3=new Person();

console.log(p1.name);  // "zs"
console.log(p2.age);   // 19
console.log(p3.sex);   // "male"

面向对象思想封装一个原型继承

我们可以利用面向对象的思想,将面向过程进行封装。
function Dog(){
    this.type = "yellow Dog";
}

// 给构造函数 Dog 添加一个方法 extend
Dog.prototype.extend = function(obj){
    // 使用混入式原型继承,给 Dog 构造函数的原型继承 obj 的属性和方法
     for (var k in obj){
        this[k]=obj[k];
    }
}

// 调用 extend 方法
Dog.prototype.extend({
    name:"二哈",
    age:"1.5",
    sex:"公",
    bark:function(){
        console.log("汪汪汪");
    }
});

3、替换式原型继承

替换式原型继承,在上面已经举过例子了,其实就是将一个构造函数的原型对象替换成另一个对象。
function Person(){
    this.weight=50;
}

var obj={
    name:"zs",
    age:19,
    sex:"male"
}
// 将一个构造函数的原型对象替换成另一个对象
Person.prototype = obj;

var p1=new Person();
var p2=new Person();
var p3=new Person();

console.log(p1.name);  // "zs"
console.log(p2.age);   // 19
console.log(p3.sex);   // "male"

之前我们就说过,这样做会产生一个问题,就是替换的对象会重新开辟一个新的空间。

替换式原型继承时的bug

替换原型对象的方式会导致原型的constructor的丢失,constructor属性是默认原型对象指向构造函数的,就算是替换了默认原型对象,这个属性依旧是默认原型对象指向构造函数的,所以新的原型对象是没有这个属性的。

解决方法:手动关联一个constructor属性

function Person() {
    this.weight = 50;
}

var obj = {
    name: "zs",
    age: 19,
    sex: "male"
}
// 在替换原型对象函数之前 给需要替换的对象添加一个 constructor 属性 指向原本的构造函数
obj.constructor = Person;

// 将一个构造函数的原型对象替换成另一个对象
Person.prototype = obj;

var p1 = new Person();

console.log(p1.__proto__.constructor === Person);  // true

4、Object.create()方法实现原型继承

当我们想把对象1作为对象2的原型的时候,就可以实现对象2继承对象1。前面我们了解了一个属性:__proto__,实例出来的对象可以通过这个属性访问到它的原型,但是这个属性只适合开发调试时使用,并不能直接去替换原型对象。所以这里介绍一个新的方法:Object.create()

语法: var obj1 = Object.create(原型对象);

示例代码: 让空对象obj1继承对象obj的属性和方法

var obj = {
    name : "盖伦",
    age : 25,
    skill : function(){
        console.log("大宝剑");
    }
}

// 这个方法会帮我们创建一个原型是 obj 的对象
var obj1 = Object.create(obj);

console.log(obj1.name);     // "盖伦"
obj1.skill();               // "大宝剑"

兼容性:

由于这个属性是ECMAScript5的时候提出来的,所以存在兼容性问题。

利用浏览器的能力检测,如果存在Object.create则使用,如果不存在的话,就创建构造函数来实现原型继承。

// 封装一个能力检测函数
function create(obj){
    // 判断,如果浏览器有 Object.create 方法的时候
    if(Object.create){
        return Object.create(obj);
    }else{
        // 创建构造函数 Fun
        function Fun(){};
        Fun.prototype = obj; 
        return new Fun();
    }
}

var hero = {
    name: "盖伦",
    age: 25,
    skill: function () {
        console.log("大宝剑");
    }
}

var hero1 = create(hero);
console.log(hero1.name);    // "盖伦"
console.log(hero1.__proto__ == hero);   // true
4.原型链
对象有原型,原型本身又是一个对象,所以原型也有原型,这样就会形成一个链式结构的原型链。
4.1 什么是原型链

示例代码: 原型继承练习

// 创建一个 Animal 构造函数
function Animal() {
    this.weight = 50;
    this.eat = function() {
        console.log("蜂蜜蜂蜜");
    }
}

// 实例化一个 animal 对象
var animal = new Animal();

// 创建一个 Preson 构造函数
function Person() {
    this.name = "zs";
    this.tool = function() {
        console.log("菜刀");
    }
}

// 让 Person 继承 animal (替换原型对象)
Person.prototype = animal;

// 实例化一个 p 对象 
var p = new Person();

// 创建一个 Student 构造函数
function Student() {
    this.score = 100;
    this.clickCode = function() {
        console.log("啪啪啪");
    }
}

// 让 Student 继承 p (替换原型对象)
Student.prototype = p;

//实例化一个 student 对象
var student = new Student();


console.log(student);           // 打印 {score:100,clickCode:fn}

// 因为是一级级继承下来的 所以最上层的 Animate 里的属性也是被继承的
console.log(student.weight);    // 50
student.eat();         // 蜂蜜蜂蜜
student.tool();        // 菜刀

如图所示:

我们将上面的案例通过画图的方式展现出来后就一目了然了,实例对象animal直接替换了构造函数Person的原型,以此类推,这样就会形成一个链式结构的原型链。

完整的原型链

结合上图,我们发现,最初的构造函数Animal创建的同时,会创建出一个原型,此时的原型是一个空的对象。结合原型链的概念:“原型本身又是一个对象,所以原型也有原型”,那么这个空对象往上还能找出它的原型或者构造函数吗?

我们如何创建一个空对象? 1、字面量:{};2、构造函数:new Object()。我们可以简单的理解为,这个空的对象就是,构造函数Object的实例对象。所以,这个空对象往上面找是能找到它的原型和构造函数的。

// 创建一个 Animal 构造函数
function Animal() {
    this.weight = 50;
    this.eat = function() {
        console.log("蜂蜜蜂蜜");
    }
}

// 实例化一个 animal 对象
var animal = new Animal();

console.log(animal.__proto__);      // {}
console.log(animal.__proto__.__proto__);  // {}
console.log(animal.__proto__.__proto__.constructor);  // function Object(){}
console.log(animal.__proto__.__proto__.__proto__);  // null

如图所示:

4.2 原型链的拓展

1、描述出数组“[]”的原型链结构

// 创建一个数组
var arr = new Array();

// 我们可以看到这个数组是构造函数 Array 的实例对象,所以他的原型应该是:
console.log(Array.prototype);   // 打印出来还是一个空数组

// 我们可以继续往上找 
console.log(Array.prototype.__proto__);  // 空对象

// 继续
console.log(Array.prototype.__proto__.__proto__)  // null

如图所示:

2、扩展内置对象

js原有的内置对象,添加新的功能。

注意:这里不能直接给内置对象的原型添加方法,因为在开发的时候,大家都会使用到这些内置对象,假如大家都是给内置对象的原型添加方法,就会出现问题。

错误的做法:

// 第一个开发人员给 Array 原型添加了一个 say 方法
Array.prototype.say = function(){
    console.log("哈哈哈");
}

// 第二个开发人员也给 Array 原型添加了一个 say 方法
Array.prototype.say = function(){
    console.log("啪啪啪");
}

var arr = new Array();

arr.say();  // 打印 “啪啪啪”  前面写的会被覆盖

为了避免出现这样的问题,只需自己定义一个构造函数,并且让这个构造函数继承数组的方法即可,再去添加新的方法。

// 创建一个数组对象 这个数组对象继承了所有数组中的方法
var arr = new Array();

// 创建一个属于自己的构造函数
function MyArray(){}

// 只需要将自己创建的构造函数的原型替换成 数组对象,就能继承数组的所有方法
MyArray.prototype = arr;

// 现在可以多带带的给自己创建的构造函数的原型添加自己的方法
MyArray.prototype.say = function(){
    console.log("这是我自己添加的say方法");
}

var arr1 = new MyArray();

arr1.push(1);   // 创建的 arr1 对象可以使用数组的方法
arr1.say();     // 也可以使用自己添加的方法  打印“这是我自己添加的say方法”
console.log(arr1);  // [1]
4.3 属性的搜索原则
当通过对象名.属性名获取属性时,会遵循以下属性搜索的原则:

1-首先去对象自身属性中找,如果找到直接使用,

2-如果没找到,去自己的原型中找,如果找到直接使用,

3-如果没找到,去原型的原型中继续找,找到直接使用,

4-如果没有会沿着原型不断向上查找,直到找到null为止。

5.Object.prototype成员介绍
我们可以看到所有的原型最终都会继承Object的原型:Object.prototype

打印看看Object的原型里面有什么:

// Object的原型
console.log(Object.prototype)

如图所示:

我们可以看到Object的原型里有很多方法,下面就来介绍下这些方法的作用。

5.1 constructor 属性
指向了和原型相关的构造函数
5.2 hasOwnProperty 方法
判断对象自身是否拥有某个属性,返回值:布尔类型

示例代码:

function Hero() {
    this.name = "盖伦";
    this.age = "25";
    this.skill = function () {
        console.log("盖伦使用了大宝剑");
    }
}

var hero = new Hero();
console.log(hero.name); // "盖伦"
hero.skill();           // "盖伦使用了大宝剑"

console.log(hero.hasOwnProperty("name"));       // true
console.log(hero.hasOwnProperty("age"));        // true
console.log(hero.hasOwnProperty("skill"));      // true
console.log(hero.hasOwnProperty("toString"));   // false toString是在原型链当中的方法,并不是这里对象的方法

console.log("toString" in hero); // true in方法 判断对象自身或者原型链中是否有某个属性
5.3 isPrototypeOf 方法
对象1.isPrototypeOf(对象2),判断对象1是否是对象2的原型,或者对象1是否是对象2原型链上的原型。

示例代码:

var obj = {
    age: 18
}
var obj1 = {};

// 创建一个构造函数
function Hero() {
    this.name = "盖伦";
}

// 将这个构造函数的原型替换成 obj
Hero.prototype = obj;

// 实例化一个 hero 对象
var hero = new Hero();

console.log(obj.isPrototypeOf(hero));   // true  判断 obj 是否是 hero 的原型
console.log(obj1.isPrototypeOf(hero));  // false  判断 obj1 是否是 hero 的原型
console.log(Object.prototype.isPrototypeOf(hero));  // true  判断 Object.prototype 是否是 hero 的原型
// 注意 这里的 Object.prototype 是原型链上最上层的原型对象
5.4 propertyIsEnumerable 方法
对象.propertyIsEnumerable("属性或方法名"),判断一个对象是否有该属性,并且这个属性可以被for-in遍历,返回值:布尔类型

示例代码:

// 创建一个构造函数
function Hero (){
    this.name = "盖伦";
    this.age = 25;
    this.skill = function(){
        console.log("盖伦使用了大宝剑");
    }
}

// 创建一个对象
var hero = new Hero();

// for-in 遍历这个对象 我们可以看到分别打印了哪些属性和方法
for(var k in hero){
    console.log(k + "—" + hero[k]); // "name-盖伦" "age-25" "skill-fn()"
}

// 判断一个对象是否有该属性,并且这个属性可以被 for-in 遍历
console.log(hero.propertyIsEnumerable("name"));     // true
console.log(hero.propertyIsEnumerable("age"));      // true
console.log(hero.propertyIsEnumerable("test"));     // false
5.5 toString 和 toLocalString 方法
两种方法都是将对象转成字符串的,只不过toLocalString是按照本地格式进行转换。

示例代码:

// 举个例子,时间的格式可以分为世界时间的格式和电脑本地的时间格式
var date = new Date();

// 直接将创建的时间对象转换成字符串
console.log(date.toString());

// 将创建的时间对象按照本地格式进行转换
console.log(date.toLocaleString());

效果图:

5.6 valueOf 方法
返回指定对象的原始值。

MDN官方文档

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

静态方法: 由构造函数调用的

js中,我们知道有个Math构造函数,他有一个Math.abs()的方法,这个方法由构造函数调用,所以就是静态方法。
Math.abs();

实例方法: 由构造函数创建出来的对象调用的

var arr = new Array();

// 由构造函数 Array 实例化出来的对象 arr 调用的 push 方法,叫做实例方法
arr.push(1);

示例代码:

function Hero(){
    this.name="亚索";
    this.say=function(){
        console.log("哈撒ki");
    }
}
Hero.prototype.skill=function(){
    console.log("吹风");
}

// 直接给构造函数添加一个 run 方法(函数也是对象,可以直接给它加个方法)
Hero.run=function(){
    console.log("死亡如风,常伴吾身");
}

var hero = new Hero();

hero.say();
hero.skill();   //实例方法

Hero.run();     //静态方法

如果这个方法是对象所有的,用实例方法。一般的工具函数,用静态方法,直接给构造函数添加方法,不需要实例化,通过构造函数名直接使用即可;

7.作用域
“域”,表示的是一个范围,“作用域”就是作用范围。作用域说明的是一个变量可以在什么地方被使用,什么地方不能被使用。
7.1 块级作用域
ES5ES5之前,js中是没有块级作用域的。
{
    var num = 123;
    {
        console.log( num ); // 123
    }
}
console.log( num ); // 123

上面这段代码在JavaScript中是不会报错的,但是在其他的编程语言中(C#、C、JAVA)会报错。这是因为,在JavaScript中没有块级作用域,使用{}标记出来的代码块中声明的变量num,是可以被{}外面访问到的。但是在其他的编程语言中,有块级作用域,那么{}中声明的变量num,是不能在代码块外部访问的,所以报错。

注意:块级作用域只在在ES5ES5之前不起作用,但是在ES6开始,js中是存在块级作用域的。

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

js中词法作用域规则:

函数允许访问函数外的数据;

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

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

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

作用域练习:

第一题

var num=250;

function test(){
    // 会现在函数内部查找有没有这个num变量,有的话调用,没有的话会去全局中查找,有就返回,没有就返回undefined
    console.log(num);  // 打印 250
}

function test1(){
   var num=222;
   test();
}

test1();  

第二题

if(false){
    var num = 123;
}

console.log(num); // undefined 
// {}是没有作用域的 但是有判断条件,var num会提升到判断语句外部 所以不会报错 打印的是undefined

第三题

var num = 123;
function foo() {
    var num = 456;
    function func() {
        console.log( num );
    }
    func();
}
foo();  // 456
// 调用foo时,在函数内部调用了func,打印num的时候,会先在func中查找num  没有的时候会去外层作用域找,找到即返回,找不到即再往上找。

第四题

var num1 = 123;
function foo1() {
   var num1 = 456;
   function foo2() {
       num1 = 789;
       function foo3 () {
           console.log( num1 );  // 789  自己的函数作用域中没有就一层层往上找
       }
       foo3();
   }
   foo2();
}
foo1();
console.log( num1 ); // 123 
7.3 变量提升(预解析)
JavaScript是解释型的语言,但是它并不是真的在运行的时候逐句的往下解析执行。

我们来看下面这个例子:

func();

function func(){
    alert("函数被调用了");
}

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

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

JavaScript预解析

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

关键问题是怎么处理呢?

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

示例代码:函数名提升

正常函数书写方式

function func(){
    alert("函数被调用了");
}
func();

预解析之后,函数名提升

func();
function func(){
    alert("函数被调用了");
}

示例代码:变量名提升

正常变量书写方式

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

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

// 变量的时候 提升的只是变量声明的提升,并不包括赋值
var a;      // 这里是声明
alert(a);   // 变量声明之后并未有初始化和赋值操作,所以这里是 undefined
a = 123;    // 这里是赋值

注意:特殊情况

1、函数不能被提升的情况

函数表达式创建的函数不会提升

test();   // 报错 "test is not a function"
var test = function(){
    console.log(123);
}

new Function创建的函数也不会被提升

test();   // 报错 "test is not a function"
var test = new Function(){
    console.log(123);
}

2、出现同名函数

test();  // 打印 "好走的都是下坡路"

// 两个函数重名,这两个函数都会被提升,但是后面的函数会覆盖掉前面的函数
function test(){
   console.log("众里寻她千百度,他正在自助烤肉....");
}

function test(){
   console.log("好走的都是下坡路");
}

3、函数名与变量名同名

// 如果函数和变量重名,只会提升函数,变量不会被提升
console.log(test);  // 打印这个test函数

function test(){
   console.log("我是test");
}
var test=200;

再看一种情况:

var num = 1;
function num () {
    console.log(num); // 报错 “num is not a function”
}
num();

直接上预解析后的代码:

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

num = 1;
num();

4、条件式的函数声明

// 如果是条件式的函数申明, 这个函数不会被预解析
test();  // test is not a function
if(true){
    function test(){
        console.log("只是在人群中多看了我一眼,再也忘不掉我容颜...");
    }
}

预解析是分作用域的

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

示例代码:

function showMsg(){
    var msg = "This is message";
}
alert(msg); // 报错“Uncaught ReferenceError: msg is not defined”

预解析之后:

function showMsg(){
    var msg;    // 因为函数本身就会产生一个作用域,所以变量声明在提升的时候,只会提升在当前作用域下最前面
    msg = "This is message";
}
alert(msg); // 报错“Uncaught ReferenceError: msg is not defined”

预解析是分段的

分段,其实就分script标签的


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

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

函数预解析的时候是分段的,但是执行的时候不分段



7.4 作用域链

什么是作用域链?

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

凡是代码中有函数,那么这个函数就构成另一个作用域。如果函数中还有函数,那么在这个作用域中就又可以诞生一个作用域。将这样的所有的作用域列出来,可以有一个结构: 函数内指向函数外的链式结构。就称作作用域链。

例如:

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

var num = 456;
function f3() {
    function f4() {    
    }
}

示例代码:

var num=200;
function test(){
    var num=100;
    function test1(){
        var num=50;
        function test2(){
            console.log(num);
        }
        test2();
    }
    test1();
}

test();   // 打印 “50”

如图所示:

绘制作用域链的步骤:

看整个全局是一条链, 即顶级链, 记为0级链

看全局作用域中, 有什么变量和函数声明, 就以方格的形式绘制到0级练上

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

然后在每一个1级链中再次往复刚才的行为

变量的访问规则:

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

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

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

注意,同级的链不可混合查找

来点案例练练手

第一题:

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

第二题:

var scope = "global";
function foo() {
    console.log(scope); //  undefined
    var scope = "local";
    console.log(scope); // "local"
}
foo();

// 预解析之后
// var scope = "global";
// function foo() {
//   var scope;
//   console.log(scope); // undefined
//   scope = "local";
//   console.log(scope); // local
// }

第三题:

if("a" in window){
   var a = 10;
}
console.log(a); // 10

// 预解析之后
// var a;
// if("a" in window){
//    a = 10;        // 判断语句不产生作用域
// }
// console.log(a); // 10

第四题:

if(!"a" in window){
   var a = 10;
}
console.log(a); // undefined

// 预解析之后
// var a;
// if(!"a" in window){
//    a = 10;        // 判断语句不产生作用域
// }
// console.log(a); // undefined

第五题

// console.log(num); 报错 虽然num是全局变量 但是不会提升
function test(){
   num = 100;  
}

test();

console.log(num);   // 100

第六题

var foo = 1;
function bar() {
   if(!foo) {
       var foo = 10;
   }
   console.log(foo); // 10
}
bar();

// 预解析之后
// var foo=1;
// function bar(){
//    var foo;
//    if(!foo){
//        foo=10;
//    }
//    console.log(foo); // 10
// }
// bar();
8.Function
Function是函数的构造函数,你可能会有点蒙圈,没错,在js中函数与普通的对象一样,也是一个对象类型,只不过函数是js中的“一等公民”。

这里的Function类似于ArrayObject

8.1 创建函数的几种方式

1、函数字面量(直接声明函数)创建方式

function test(){   
    // 函数体
}   // 类似于对象字面量创建方式:{}

2、函数表达式

var test = function(){
    // 函数体
}

3、Function构造函数创建

// 构造函数创建一个空的函数
var fn = new Function();
fn1();  // 调用函数

函数扩展名

有没有一种可能,函数表达式声明函数时,function 也跟着一个函数名,如:var fn = function fn1(){}? 答案是可以的,不过fn1只能在函数内部使用,并不能在外部调用。
var fn = function fn1(a,b,c,d){
    console.log("当前函数被调用了");
    // 但是,fn1可以在函数的内部使用
    console.log(fn1.name);
    console.log(fn1.length);
    // fn1();  注意,这样调用会引起递归!!!  下面我们会讲到什么是递归。
}
// fn1();   // 报错,fn1是不能在函数外部调用的
fn();   // "当前函数被调用了"

// 函数内部使用时打印:
// "当前函数被调用了"
// console.log(fn1.name); => "fn1"
// console.log(fn1.length); => 4
8.2 Function 构造函数创建函数
上面我们知道了如何通过Function构造函数创建一个空的函数,这里我们对它的传参详细的说明下。

1、不传参数时

// 不传参数时,创建的是一个空的函数
var fn1 = new Function();
fn1();  // 调用函数

2、只传一个参数

// 只传一个参数的时候,这个参数就是函数体
// 语法:var fn = new Function(函数体);
var fn2 = new Function("console.log(2+5)");
f2();   // 7

3、传多个参数

// 传多个参数的时候,最后一个参数为函数体,前面的参数都是函数的形参名
// 语法:var fn = new Function(arg1,arg2,arg3.....argn,metthodBody);
var fn3 = new Function("num1","num2","console.log(num1+num2)");
f3(5,2);   // 7
8.3 Function 的使用

1、用Function创建函数的方式封装一个计算m - n之间所有数字的和的函数

//求 m-n之间所有数字的和
//var sum=0;
//for (var i = m; i <=n; i++) {
//  sum+=i;
//}
var fn = new Function("m","n","var sum=0;for (var i = m; i <=n; i++) {sum+=i;} console.log(sum);");
fn(1,100);  // 5050

函数体参数过长问题:

函数体过长时,可读性很差,所以介绍解决方法:

1)字符串拼接符“+

var fn = new Function(
    "m",
    "n",
    "var sum=0;"+
    "for (var i = m; i <=n; i++) {"+
        "sum += i;"+
    "}"+
    "console.log(sum);"
    );
fn(1,100);  // 5050

2)ES6中新语法“ ` ”,(在esc键下面)

表示可换行字符串的界定符,之前我们用的是单引号或者双引号来表示一个字符串字面量,在ES6中可以用反引号来表示该字符串可换行。
new Function(
    "m",
    "n",
    `var sum=0;
    for (var i = m; i <=n; i++) {
        sum+=i;
    }
    console.log(sum);`
);

3)模板方式




2、eval 函数

eval函数可以直接将把字符串的内容,作为js代码执行,前提是字符串代码符合js代码规范。这里主要是用作跟Function传参比较。

evalFunction 的区别:

Function();中,方法体是字符串,必须调用这个函数才能执行

eval(); 可以直接执行字符串中的js代码

存在的问题:

性能问题

因为eval里面的代码是直接执行的,所以当在里面定义一个变量的时候,这个变量是不会预解析的,所以会影响性能。
// eval 里面的代码可以直接执行,所以下面的打印的 num 可以访问到它
// 但是这里定义的 num 是没有预解析的,所以变量名不会提升,从而性能可能会变慢
eval("var num = 123;");
console.log(num);   // 123

安全问题

主要的安全问题是可能会被利用做XSS攻击(跨站脚本攻击(Cross Site Scripting)),eval也存在一个安全问题,因为它可以执行传给它的任何字符串,所以永远不要传入字符串或者来历不明和不受信任源的参数。

示例代码: 实现一个简单的计算器









效果图:

8.4 Function 的原型链结构
7.2章节中我们知道函数也还可以通过构造函数的方式创建出来,既然可以通过构造函数的方式创建,那么函数本身也是有原型对象的。

示例代码:

// 通过Function构造函数创建一个函数test
var test = new Function();
// 既然是通过构造函数创建的,那么这个函数就有指向的原型
console.log(test.__proto__);  // 打印出来的原型是一个空的函数
console.log(test.__proto__.__proto__);  // 空的函数再往上找原型是一个空的对象
console.log(test.__proto__.__proto__.__proto__);    // 再往上找就是null了

// 函数原型链: test() ---> Function.prototype ---> Object.prototype ---> null

如图所示:

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/100458.html

相关文章

  • 【连载】前端个人文章整理-从基础到入门

    摘要:个人前端文章整理从最开始萌生写文章的想法,到着手开始写,再到现在已经一年的时间了,由于工作比较忙,更新缓慢,后面还是会继更新,现将已经写好的文章整理一个目录,方便更多的小伙伴去学习。 showImg(https://segmentfault.com/img/remote/1460000017490740?w=1920&h=1080); 个人前端文章整理 从最开始萌生写文章的想法,到着手...

    madthumb 评论0 收藏0
  • 个人分享--web前端学习资源分享

    摘要:前言月份开始出没社区,现在差不多月了,按照工作的说法,就是差不多过了三个月的试用期,准备转正了一般来说,差不多到了转正的时候,会进行总结或者分享会议那么今天我就把看过的一些学习资源主要是博客,博文推荐分享给大家。 1.前言 6月份开始出没社区,现在差不多9月了,按照工作的说法,就是差不多过了三个月的试用期,准备转正了!一般来说,差不多到了转正的时候,会进行总结或者分享会议!那么今天我就...

    sherlock221 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<