前端技术语言篇

认识JavaScript

你应该了解的事情

ECMAScript是ECMA国际定义的一套脚本语言实现标准,而JavaScript、JScript以及ActionScript则是它的具体实现和扩展。
现阶段JavaScript的ECMAScript实现基本上基于ES5,浏览器厂商也在努力实现2015年发布的ES2015,即ECMAScript6,也有称之为ECMAScript. Next。
webpack的加载器babel能够实现ES6到ES5的转化,建议小心谨慎地去使用。
参考资料:《ECMAScript 6入门》

JavaScript是基于对象的编程语言,而不是面向对象。基于对象与面向对象最大不同之处在于基于对象的编程语言无法从新设计的类中生成新对象,并且无法做相应的封装与继承。
JavaScript的继承是使用了原型链的方式来模拟实现,并不是真正的继承,也没有所谓的OOP。

JavaScript很多时候是在宿主环境中运行,由宿主环境提供各种对象供JavaScript调用,所以你无法在node.js中访问到window对象,而只能在浏览器中访问到,因为浏览器宿主环境才提供了这个对象。

请直接访问Mozilla或者webkit的开发者资料库,你可以在这些资料库中找到最新的资料,不要迷信微软的资料库或者网上下载的jscript.chm(其实也是微软的)。
参考资料:《Mozilla Web技术文档》

请尽量为你的代码开启JavaScript严格模式,并且仅在函数级别的代码块中开启它。

JavaScript运行环境

  • 宿主环境

    JavaScript一般来说必须在宿主环境中才能够运行,而不像C/C++一样编译成exe文件直接执行。与之类似的是Java编译成目标代码之后,还需要在JVM这个环境下才能执行。简单来说,宿主环境就是一个软件,由它加载JavaScript解析器执行JavaScript代码,并且提供相应的配套服务,比如安全权限。

  • 解析器

    跟PHP之类的脚本语言一样,JavaScript在执行之前并不编译,而是在运行期间一边解析一边执行,这需要一个解析器,也称为脚本引擎。
    常用的解析器有用于chrome的V8、用于Firefox的SpiderMonkey、用于Internet Explorer的Chakra以及用于Safari的JavaScriptCore。
    解析器提供了基础的ECMAScript特性以及基础对象。

  • 上下文环境/对象

    宿主环境调用解析器执行JavaScript代码的时候为JavaScript运行环境建立起一个封闭的作用域与宿主环境隔离开来,并且提供一个全局对象。所有内置对象都以属性或者方法的方式挂载在该对象上。在没有明确指定作用域、所属对象的变量也会挂载在该对象上。

这里写图片描述

JavaScript的语言特性

对象的哈希特性

JavaScript对象的属性、方法访问存在两种方式:

  1. 使用对象.属性名,如 obj.name
  2. 使用属性名作为下标,如obj[“name”]

    从使用上看,如果对哈希表有了解很容易感觉出JavaScript的对象仿佛就是一个Hash表,事实上内部实现也确实是类似于Hash表的存储结构。

Hash表的数据结构:

  1. 索引区,一个足够大的指针数组,每个元素存储着指向实体项的内存地址
  2. 实体区,一个链表,存储着key、value、next(解决碰撞问题)

Hash表的查找原理:

  1. 根据一个定义好的散列算法将key转换成一个对应着索引区下标的数字
  2. 找到索引区对应的元素,取出该元素的值,如果为0即表示没有对应的value
  3. 根据索引区取出的值找到实体区对应的链表的头部
  4. 根据key的原始值遍历链表,找到对应的value
    这里写图片描述

作用域-scope

  • 作用域是使用JavaScript要特别小心的概念,在ECMAScript中规定只有函数才拥有作用域,也就是说代码块是不存在作用域,这是与许多编程语言不一样的地方。

  • 全局作用域是宿主环境加载JavaScript源码开始执行时创建,每个作用域中使用var声明的局部变量仅在该作用域以及该作用域中的子作用域可见,并且不影响该作用域的上层作用域。

  • 当使用一个变量时,会从本作用域开始往上层查找,直到全局作用域。如果全局作用域中不存在,则抛出错误(作用域链)。

  • 声明变量时如果不使用var关键词,则相当于在全局作用域下声明,即声明了一个全局变量,并且被挂载在window对象中。因此,子作用域声明的局部变量会在该作用域下覆盖上层作用域的同名变量,这时候上层作用域的同名变量不可见。

  • 函数在执行完毕后,一般来说作用域下的变量会被释放,除非外部的某个操作会读写该作用域下的数据(即闭包)。

  • 作用域下的变量可以在任意位置定义,并且在任意位置可见,这意味着你使用了未声明的变量并且该变量随后被声明,哪怕外部作用域已经存在同名变量,该变量依然会等于undefined,也就是所谓的变量声明提前。

  • 在一个作用域中,使用function
    fn(){}声明的函数允许在未声明时就被调用。但要注意的是,如果是全局范围内声明的函数,只能由同处同一个script标签的任意位置提前调用。
    这是因为页面是一边加载一边执行,当一个script标签内容被加载完就会被马上执行,如果提前调用函数的代码跟函数不处地同一个script标签,调用函数时该函数的声明还未被加载。

这里写图片描述

闭包-closure

正如上页所言,如果作用域外的操作将会导致作用域内的数据发生读写操作,那么解析器将会保持住该作用域的数据以供再次访问。

闭包的应用场景:

  • 实现作用域内私有数据封装,防止外部直接访问
  • 实现匿名自执行函数,防止污染全局作用域以及数据干扰
  • 实现数据长期驻存内存

特别注意:

  • 如非必要不要建立存在大量数据的闭包,造成内存浪费
  • 闭包是最容易发生内存泄露的地方
  • 由于浏览器更新非常快,许多关于内存泄露的资料都随时过时,本PPT就不针对这方面做分享,但需要了解的是内存泄露基本上都是因为在DOM对象跟JavaScript标准对象的互相引用。
    由于DOM对象跟JavaScript的标准对象不是一个系统的,导致资源回收器无法准确计算对象的引用计数。
    内存泄露导致的后果就是除非关闭浏览器进程,否则泄露的内存不会被释放,哪怕刷新页面。

最简单的闭包:

function fn() {
    var x = 1;
    return function() {
        return x++;
    }
}
var myFn = fn();
myFn();

this的指向

  • 如果函数作为某个对象的方法被调用,this就是该对象
  • 如果函数直接运行,this是window
  • apply/call可以改变函数的this指向,哪怕它作为某个对象的方法被执行
  • bind方法可以为函数生成一个新的函数,该新函数不管怎么执行,this都是指向bind方法指定的对象。
  • 使用addEventListener为DOM元素绑定事件,回调函数中的this是指该DOM对象,除非使用了bind函数生成新的函数改变了this指向。
    注:虽然绑定事件时并没有为事件回调函数指定对象,表面上看应该是window对象,但在addEventListener的内部实现依然使用了类似apply/call将this改为当前DOM对象。

示例代码:

function method() {
    console.log(this);
}
method(); // window


// 作为 obj的方法执行,this为obj
var obj = {};
obj.method = method;
obj.mehod();


// 使用call为method指定了obj对象并且执行,this为obj。apply同理。
var obj = {};
method.call(obj);


// 使用bind方法生成新的函数,直接执行newMethod,this指向obj
var obj = {};
var newMethod = method.bind(obj);
newMethod();

对象构建函数

  • JavaScript是基于对象的语言,不存在真正的类,但提供了一种方法允许我们定义一个对象构建函数(也就是大家说的类),模拟类创建出多个相似的对象,并且通过原型链来建立起这些对象跟构建函数的关系。
  • 尽管ECMAScript6引入了class关键词,但它依然是基于原型的类实现。
  • 跟其它语言的类完全不一样的是:同一个类的实例的属性和方法都是可以随意增删的,因此不要指望同一个类的实例拥有同样的属性和方法,但在编码的过程中应该避免这种情况。
  • 类的方法与属性都是公开的,ECMAScript5并没有提供私有或者保护等权限。
  • 尽管new关键词看起来很像其它语言的实例化对象的关键词,但实际上它仅是额外做了点小动作罢了,完全可以不使用new关键词即可模拟出这种实例化的过程。
function People(name) {
    var createTime = new Date();
    this.name = name;
    this.showName = function() {
        console.log(this.name);
    }
    this.getCreateTime = function() {
        return createTime.getTime();
    }
}

var obj = new People(”obj1”);
obj.showName();

实例化过程解析

  • 创建一个新对象,并赋值给this
  • 为新对象增加一个隐藏属性proto,并且它的值等于构建函数的prototype属性(proto在ES6已经弃用)
  • 为新对象增加一个constructor属性,它的值为构建函数本身。值得注意的是:构建函数默认的prototype属性的constructor是构建函数本身。
function People(name) {
    var createTime = new Date();
    this.name = name;
    this.getCreateTime = function() {
        return createTime.getTime();
    }
}
People.prototype.showName = function() {
    console.log(this.name);
}
实例化方式:
var people = new People(“ft”);

等同于:
var people = {};
people.__proto__ = People.prototype;
people.constructor = People;
People.call(people, “ft”);

另外在没有改动People.prototype的情况下,以下表达式成立
people.__proto__.constructor == People

原型链

  • 对象构建函数的prototype属性被称为原型对象,类的所有实例都能够访问到该原型对象的所有属性或方法,并且该原型对象所属的构建函数的prototype也同样能够访问到,对象构建函数的prototype属性以及对象的proto属性共同构成了一条属性、方法访问链,也就是原型链。
    注意:原型链查找实质上效率不高。
  • 如果要了解原型链,最重要的就是要了解解析器如何查找一个对象的属性。
  • 1.查找对象本身是否拥有该属性
  • 2.如果对象拥有proto属性,则在proto属性上找
  • 3.如果proto属性也拥有proto属性,继续找
function findProp(obj, prop) {
    if (obj.hasOwnProperty(prop)) {
        return obj[prop];
    } else if (obj.__proto__ != null) {
        return findProp(obj.__proto__, prop);
    } else {
        return undefined;
    }
}

这里写图片描述

原型继承

  • 由于原型链的存在,我们可以扩展对象构建函数的原型对象,让实例化后的所有对象拥有同一个proto对象,这也是JavaScript的类继承另一个类的传统方式。
  • 从表现上看,这种继承方式其实问题不少。
    NewPeople.prototype实际上是People的一个实例,这并不是一个类继承另一个类,而是利用一个类的实例为某个类做扩展。这意味着在做扩展的同时,父类的初始化参数已经确定,无法在子类的实例化过程中往父类构建函数传参。
function People(gender) {
    this.gender = gender;
    this.getGender = function() {
        return this.gender;
    }
}

function NewPeople(name, gender) {
    this.name = name;
    this.gender = gender;
    this.getName = function() {
        return this.name;
    }
}

NewPeople.prototype = new People();
var people = new NewPeople(”benny”, “male”);

改进的原型继承:

function People(gender) {
    this.gender = gender;
    this.getGender = function() {
        return this.gender;
    }
}

function NewPeople(name, gender) {
    People.call(this, gender);

    this.name = name;
    this.getName = function() {
        return this.name;
    }
}

(function() {
    var op = People.prototype;
    NewPeople.prototype = Object.create(op);
    NewPeople.prototype.constructor = NewPeople;
})();

var people = new NewPeople(”benny”, “male”);

以扩展的思路构造对象

原型扩展与继承的不足之处:

  1. 原型链较为复杂,如果不熟悉细节很容易在编码时出错
  2. 原型链相对真正的类虽然更适合解析器解析,但性能极其低下(chrome采用了hidden classes技术有较大改善)
  3. 由于JavaScript对象允许随意增删改属性,原型链并没有想象中那么安全。
  4. 由于个人对this对象的指向必须谨慎,否则容易出问题

由于JavaScript语言本身的灵活性导致并不存在真正的类与实例的关系,许多模块化开发方式都放弃了原型继承,而采用对象扩展的方式去生成一个对象。

  1. 所有定义方法属性皆直接作为对象本身的属性方法,性能高
  2. 抛弃了没什么用的类与实例关系,也不存在this对象,更易于理解
function createPeople(gender) {
    var that = {};
    that.gender = gender;
    return that;
}

var people = createNewPeople(“benny”, “name”);

function createNewPeople(name, gender) {
    var that = createPeople(gender);
    that.name = name;
    return that;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值