javascript是目前web领域中实用最为广泛的语言,不管是在前端还是在后端都能看到它的影子,可以说web从业者不论怎样都绕不开它。在前端领域,各种框架层出不穷,最火的时候几乎每个月都有新的框架诞生,如angularjs,vuejs等。在后端领域,nodejs可谓如火如荼,打破了人们对javascript只能作为前端语言的认知。按照此势头下去,javascript会越来越流行,会随着web的发展越来越重要。现在基本没有第二种语言可以挑战js在web前端中的地位,至少10年以内不可能。
所以不论你是想学各种前端框架还是nodejs,都需要深入理解javascript的工作原理以及特性,只有这样才能以不变应万变。
特别是,现在ES6语法的面世(ES7未来几年可能面试),原生语法的支持,极大的便利了前端语言的编写,至少,相对ES5来说,可以像服务端一样,编写出优雅的代码来。
javascript的实现原理
Javascript诞生与1995年,是Netscape(网景)公司推出的浏览器端语言。虽然含有Java关键字,却与java没有半点关系。随后Javascript越来越火,网景公司想将其标准化,这样更加利于网络的发展,遂将其提交给了ECMA(欧洲计算机制造商协会)管理,负责对其的标准化。ECMA机构以JS为原型,推出了一个ECMAScript的脚步语言,规定各大浏览器厂商都必须依照ECMAScript标准实现各种的JS,保证JS具有良好的跨平台性。所以可以将ECMAScript看成是标准化的JS,一个意思。
ECMAScript本质上是一种语言规范,其与平台没有关系,比如浏览器等。web浏览器只是ES的宿主环境之一,负责实现ES以及提供ES与环境交互的手段。宿主环境还有Node以及Flash等。
JS的实现要比ES规定的要复杂的多,ES只规定了基本语言特性。浏览器下的JS实现可以由下面三部分组成:
- 语言部分(ES)
- 文档对象模型(DOM)
-
浏览器对象模型(BOM)
DOM是负责操作由XML编写的应用程序的API,负责将整个页面映射成多层节点结构。如下所示:
image
DOM可以提供JS对节点结构的任何操作,增删改查等。根据DOM提供的功能多样性,将DOM分为DOM1,DOM2,DOM3这几个级别。DOM1由DOM Core与DOM HTML组成。DOM2在DOM1的基础上提供了更多的操作与功能。DOM3则更进一步的扩展了DOM。
BOM是浏览器对象模型,负责提供浏览器与JS的交互接口,提供JS操作浏览器的窗口与框架等。这个没啥好说的!
总之要实现一个完整的浏览器端JS,这个三个部分缺一不可。
javascript与java,C/C++的区别和联系。
javascript是一门动态语言,即在编写好代码后不用编译,由js解释器解释执行,同时变量不用显式的写出类型,统一用var类型表示,具体的变量类型由JS解释器推测,与python和ruby一样。说到js,大家经常听到面向函数式编程,这是js的一大设计特性。强大的function。其实在js中,函数本质上也是对象,也继承自Object类,也有属性等。js中也很多地方需要我们注意,它与java和C++很不一样。
- js中没有类继承关键字,和java与C++不一样。js的类继承需要自己动手实现,这也衍生出了多种类继承的编写范式。
- 同时js中没有函数重载特性,这个需要特别注意。因为在js中函数只是普通对象,没有函数签名(函数名+参数)。而在java和C++中,用函数签名唯一标示一个函数。不过在js中我们也可以有多种方式模拟出函数重载的效果。
- js中的作用域与java也不一样,js中有作用域链,在函数执行中,解释器会根据执行函数的作用域链一层层的往上寻找变量,一直找到位于末端的window作用域中。
- js中有原型的概念,每个类都有对于的原型,包括函数等。类对象中有引用指向原型对象,所以同一类的原型对象被所有类对象共享。由此衍生出很多有意思的特性。
- js中有闭包,这个闭包特性是由作用域链的设计衍生出来的,特别值得注意。根据闭包特性,结合匿名函数,我们可以模拟块级作用域效果,甚至可以模拟出单例模式以及私有变量等。
- js中的继承与多态,需要程序员自己实现,与java和C++不一样。利用js的原型链,可以写出很多不同的继承效果,各有特点。写js中的继承远比java中有技术含量,哈哈!
- js有垃圾回收机制,但是比较简单,没有jvm中的有意思。
以下仅为个人学习心得体会乃本人觉得Javascrpt较为基础、必要部分内容分享
一、Javascript垃圾回收方式
Javascript具有自动垃圾回收机制,开发人员不需要关心内存的使用与释放。垃圾回收方式主要有下述两种
- 1、标记清除法
Javascript中最常用的就是标记清除。当变量进入环境(例如,在函数中声明的变量)时,就将这个变量标记为“进入环境”;当变量离开环境时,标记为“离开环境”。最后垃圾收集器会清除带有“离开环境”的对象,释放内存。 - 2、引用计数法
另一种不太常见的垃圾回收方法叫引用计数法。引用计数的含义是跟踪记录每个值的引用次数。当声明一个变量并将一个引用类型赋给该变量时候,则这个值的引用次数就+1,如果同一个值又被赋给另一个变量时候,该值的引用次数再+1;反之,引用该对象的变量取得了另外的值,则引用变量的引用次数-1。当引用的次数为0时候,则对应的内存将在垃圾回收时候被释放。
问题:在循环引用中内存得不到释放。如下述demo
function problem() {
var obj1 = new Object()
var obj2 = new Object()
obj1.a = obj2
obj2.b = obj1
demo中,obj1,obj2通过属性各自相互引用,也就是这两个对应的引用次数都是2,在引用计数法下永不会被垃圾回收;但在标记清除策略中,该方法执行后就离开了环境不再使用,可以被垃圾回收。在ie中,Javascript访问COM对象是基于这个引用计数策略的,也就是会存在这种垃圾无法回收的情况。为了避免这种情况,在对应不再使用时候,最好手动赋值null释放对象。
二、理解对象
创建对象的最简单方式就是创建Object的实例,然后再为它添加属性、方法,如下所示:
var person = new Object()
person.name = 'Jack'
person.age = 29
person.jbo = 'Software Engineer'
person.sayName = function () {
console.log(this.name)
}
上面的例子创建了一个名为person的对象,并为它添加了三个属性(name、age、job)和一个sayName的方法。这些属性、方法在创建时候带有一些特征值,Javascript通过这些特征值来定义它们的行为。
- 属性类型:js中有两种属性,数据属性 和** 访问器属性**
1、数据属性
数据属性包括一个数据值的位置。在这个位置可以读取和写入值。它包含有4个描述其行为的特性:configurable、Enumerable、writable、value。
var person = {
name = 'Jack'
}
// 该对象创建了一个name的数据,value值为‘Jack’。其他configurable、Enumerable、writable特征值为默认值true。
要修改它的特征值,需要使用ES5中的Object.defineProperty()方法。 如下:
var person()
Object.property(person,'name',{
value: 'Jack',
writabe: false
})
// 将name属性的可写特性设置为false
person.name = 'tom'
person.name // 'Jack' 上述赋值语句并不能修改它的值
2、访问器属性
访问器属性不包含数据值,它们包含一堆getter和setter函数。在读取访问器属性时候,会调用getter函数,这个函数负责返回有效的值;在写入访问器属性时候,会调用setter函数并传入新的值,这个函数负责决定如何处理数据。
访问器属性值不能直接定义,必须通过**Object.defineProperty()方法,如下:
var book = {
_year: 2014,
edition: 1
}
Object.defineProperty(book, '_year', {
get: function() {
return this._year
},
set: function(newVal) {
this._year = newVal
}
})
创建对象的方式:
- 工厂模式
function createPerson(name, age, job){
var person = new Object()
person.name = name
person.age = age
person.job = job
return person
}
var person1 = createPerson('Jack', 29, 'teacher')
- 构造函数模式
function createPerson(name, age, job) {
this.name = name
this.age = age
this.job = job
this.sayName = function () {
console.log( this.name)
}
}
var person1 = new createPerson('Jack', 29, 'teacher')
相比于工厂模式,没有显示的创建对象,没有retrun语句
- 原型模式
我们创建的每一个函数都有一个propertype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性与方法。
function Person = {}
person.propertype.name = 'Jack'
person.propertype.age = 29
var person1 = new Person()
person.name // 'Jack'
person.age // 29
三、原型链
原型链是JS中实现继承的主要方式。每一个JS类中都有一个指向该类原型对象的引用。该原型对象有一个constructor属性,指向构造函数,如图所示:
image
由该类生成的对象中,也有个隐含的prototype属性,指向该类的原型对象!设想一下,我们如何将另一个类的对象实例作为某个类的原型对象会怎么样呢?如下所示:
image
可以看到对于instance对象而言,prototype指向的是SubType类的原型,而SubType类的原型是SuperType的类实例,其中的prototype指向SuperType类的原型对象,这样就形成了一个3层的原型链(包含Object原型)。当解析器在instance中寻找变量时,它会先在实例对象中寻找,然后沿着原型链一直向上寻找,直到找到为止!最终如果在Object类的原型中都没有找到,那么会产生错误!
如图所示:
image
根据原型链,我们可以写出JS中的继承代码,一般推荐用混合方式实现,即构造函数与原型链混合方式:
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue"];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
}
function SubType(name, age) {
this.age = age;
SuperType.call(this, name);
}
//继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
console.log(this.Age);
}
SuperType A = new SuperType("hh");
SubType B = new SubType("MM", 26);
这样就完成了继承的编写,混合方式的好处,可以避免只用原型链方式的一些缺点,比如不能向构造函数中传递参数,或者对于引用类型的值,不能做到对象独有一份!但是上式方式依然有缺点,可以发现,每次创建SubType类时,会调用SuperType的构造函数,创建两个变量,name与colors。但是我们会发现这两个变量其实在SubType的原型中已经存在了,只是SubType的对象实例中的变量屏蔽了其原型对象中的两个变量!这样一来,造成空间浪费,同时也耗费了在形成SubType原型对象中调用SuperType的构造函数的时间。
那么如何解决上述问题呢,我们可以用寄生混合式继承方法。代码如下:
function inheritPrototype(subType, superType) {
var prototype = Object(superType.prototype);
prototype.constructor = subType;
subType.prototype = prototype;
}
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue"];
}
SuperType.prototype.sayName = function() {
alert(this.name);
}
function SubType(name, age) {
this.age = age;
SuperType.call(this, name);
}
SubType.prototype = inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function() {
alert(this.Age);
}
SuperType A = new SuperType("hh");
SubType B = new SubType("MM", 26);
采用这种寄生混合式的继承方法,使用寄生式继承父类的prototype对象,将结果做子类的prototype,这样就可以避免调用父类的构造函数,同时需要将prototype对象的constructor属性指向子类构造函数即可。
这种方法是目前公认的最理想的继承范式,能正常使用instanceof和isPrototypeOf()。
思考:这种方式就没有缺点吗?如果后续我们在SuperType的原型对象中增加一个方法,但是SubType的原型是复制品,所以后续的SubType对象实例中不能得到该方法。但是如果采用原型式继承+混合式继承呢?能不能得到更好的效果呢?思考下面这段代码:
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue"];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
}
function SubType(name, age) {
this.age = age;
SuperType.call(this, name);
}
//原型继承SuperType的原型对象
SubType.prototype = Object.create(SuperType.prototype);
SubType.prototype.sayAge = function() {
console.log(this.Age);
}
SuperType A = new SuperType("hh");
SubType B = new SubType("MM", 26);
//增加这段代码
SuperType.prototype.sayColors = function() {
console.log(this.colors);
}
B.sayColors();
四、闭包
闭包是JS中的一个非常重要的概念,不少开发人员搞不清匿名函数跟闭包两个概念,经常混着用。闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包常见的方式是在一个函数内部建立另一个函数,例如:
function createCompare(propertyName) {
return function(object1, object2) {
var value1 = object1[propertyName];
var value2 = object2[propertyName];
return value2 - value1;
}
}
var compare = createCompare("name");
var result = compare({name: "haha"}, {name: "hehe"});
该函数返回一个匿名函数,在该匿名函数中可以访问外面函数的活动变量propertyName。该原理是:在匿名函数返回以后,匿名函数的作用域链被初始化为包含createCompare()函数的活动对象以及全局变量对象。这样,在匿名函数执行时就可以访问createCompare函数的活动变量了。值得注意的是,createCompare函数在执行完毕后,它的变量对象并没有被销毁,因为有匿名函数的作用域链依然在引用这个活动对象。换句话说,createCompare函数执行完后,它的作用域链会被销毁,但是它的活动变量却保留在了内存中,直到匿名函数被销毁后,它的活动对象才会被销毁。用下图表示:
image
java中有匿名对象,js中有匿名函数,其实本质都差不多。js中函数对象都有一个name属性。对于name属性,其实是指向函数声明时跟在function后面的名字。但是在匿名函数中name为空字符串。这里需要正确理解函数声明与函数表达式。
函数声明:
functionName(a, b); //由于函数声明提升,所以可以执行
function functionName(arg0, arg1) {
}
函数表达式:
a(); //函数表示式,,没有函数声明提升,所以不能执行,报错!
var a = function(arg0, arg1) {
};
在递归情况下,可以用匿名函数很好的书写,即使在严格模式下,依然可以使用:
var factorial = (function f(num) {
if (num <= 1)
return 1;
else
return num * f(num-1);
});
用命名函数表达式,可以将f()函数赋值给factorial变量,但是函数的名字依然是f,可以测试factorial.name依然是f。
有了匿名函数,可以模拟块级作用域:
(function(){
//模拟的块级作用域
})();
在括号内用函数声明,表示这个是函数表达式,后面紧接括号,表示立刻调用这个匿名函数。
匿名函数配合闭包特性,可以实现单例模式:
var singleton = (function() {
//设置私有变量与私有函数
var privateVariable = 10;
function privateFunction(){
alert("hello world");
}
//创建对象,可以是任意类型的对象
var object = new Object();
//添加特权/公有属性与方法
object.publicProperty = true;
object.publicMethod = function(){
privateVariable++;
return privateFunction();
};
return object;
})();
五、关于this对象
在闭包中使用this对可能导致一些问题。我们知道this对象是在运行时基于函数的执行环境绑定的:在全局中,this等于window;而当函数被作为某个对象的方法调用时候,this等于调用的那个对象。不过,匿名函数的执行环境具有局部性,因此其对象通常指向window。但有时候由于编写闭包的的方式不同,这一点可能不那么明显。如下demo:
var name = 'This window'
var Obj = {
name: 'My Object',
getName: function () {
return function () {
return this.name
}
}
}
console.log(Obj.getName()()) // 'This window'
上诉代码先创建了一个全局变量name,又创建了包含name属性的对象。这个对象还包含方法getName,它返回一个匿名函数而匿名函数又返回this.name。由于getName()返回的是一个函数,因此调用Obj.getName()会立即调用它返回的函数,返回结果就是一个字符串‘This window’,即全局变量name的值。为什么匿名函数没有取得其闭包作用域的this对象呢?
事实上,每个函数在被调用时,其活动对象会都会自动取得两个特殊变量:this和arguments。内部函数在搜索这两个变量时候,只会搜索到其活动对象为止因此永远访问不肯直接访问外部环境中的这两个变量。不过,如果外部作用域中的this对象保存在一个闭包能改访问到的变量里面,就可以让闭包访问到该对象了,如下所述:
var name = 'this window'
var Object = {
name: 'my Object',
getName: function () {
var self = this
return function () {
return self.name
}
}
}
console.log(Object.getName()()) // 'my Object'
this对应在函数中指向的对象是否就是固定的不能被改变呢?
答: 不是的。可通过下述几个方法改变this的指向:bind,apply,call,至于这几个方法的区别,有兴趣的再自己去研究一番。
六、作用域链
执行环境是js中一个重要的概念。执行环境定义了对象或函数可以访问到的数据。每一个执行环境都有一个与之关联的变量对象,环境定义的所以变量和函数都保存在这个对象中。程序编写者无法正常访问该对象,但是后台的解析器会访问到它。
全局执行环境是最外围的一个执行环境,在web浏览器中是window对象。当某个执行环境执行完后,环境会被销毁,与之相关联的变量对象中的所有变量与函数也可能会被销毁。为什么用可能呢?有一个值得注意的地方,该变量对象销不销毁最本质的是看有没有其他引用指向它,如果有别的引用指向该变量对象,那么该变量对象不会被销毁,比如在闭包中(函数中的函数情况)。
当代码在一个环境中执行时,解析器会创建变量对象的一个作用域链。作用域链的最顶端始终是该执行环境的变量对象。对于函数而言,变量对象是其活动对象!当在执行环境中遇到一个变量时,解析器会从作用域链的最顶端变量对象中找相应的变量,没有找到,则会顺着作用域链一直找下去,最后找到全局执行环境的变量对象!最后还是没有,则会报错!
看下来一个函数:
function compare(value1, value2) {
return value2 - value1;
}
上面的函数执行在全局环境中,调用compare()时,会创建一个包含arguments、value1以及value2的对象,this特殊的变量不能在变量对象中找到。所以,全局执行环境中的变量对象则处在compare函数的作用域链中的第二位。如下图所示:
image
七、高级技巧
- 定时器
setTimeOut、setInterval
如何编写一个精准的定时器?
// 伪代码 参考
let delay = 0
setTimeout(doSomething, delay)
doSomething=()=>{
const start = Date.now().getTime()
do()
const end = Date.now().getTime()
delay = 1000 - (end - start)
}
do = ()=>{
// 重复执行逻辑
}
-
节流防抖
参考之前写的节流防抖文章https://www.jianshu.com/p/990d2a40e81d -
防止对象被篡改
// 不可扩展对象
var person = { name: 'jack'}
person.age = 29
即使第一行代码已经完整的定义了person对象,但第二行代码仍然可以给它添加属性,。现在用Object.preventExtenions()方法可以改变这个行为让你不能再给对象添加任何属性和方法。
var person = { name: 'jack'}
Object.preventExtenions(person)
person.age = 29
concole.log(person.age) //undefined
以上为所有的心得体会分享。
by 刘荣杰
网友评论