【语言学习】js语言 - hippowc/hippowc.github.io GitHub Wiki
javascript不优雅之处(坑)
js很多坑都是来自于弱类型语言的本身的缺陷,弱类型语言因为变量的类型可以变化,并且在不同上下文中类型会进行隐式转换,导致程序的执行不符预期。很多弱类型语言譬如:js,php都会有类似问题
隐式转换
js有这么几种类型:
- 基础类型(原始值)undefined、 null、 string、 number、 boolean、 Symbol (es6新出的,本文不讨论这种类型)
- 复杂类型(对象值):Object 另外,js的内置对象:Date, Array, Math, Number, Boolean, String, Array, RegExp, Function。其中Number、Boolean、String是基础值的对象形式
js中一个难点就是js隐形转换,因为js在一些操作符下其类型会做一些变化,所以js灵活,同时造成易出错,并且难以理解。
涉及隐式转换最多的两个运算符 + 和 ==。+运算符即可数字相加,也可以字符串相加。所以转换时很麻烦。== 不同于===,故也存在隐式转换。- * / 这些运算符只会针对number类型,故转换的结果只能是转换成number类型。
隐式转换中主要涉及到三种转换:
- 将值转为原始值,ToPrimitive()。
- 将值转为数字,ToNumber()。
- 将值转为字符串,ToString()。 还有其他ToBoolean等,但是常用的是这几个。这几个方法是js引擎内部的抽象方法。在解释js语言时,引擎通过这几个方法对值进行转换。
ToPrimitive,是将对象值转换为原始值,一般调用对象的valueOf(默认使用valueOf)或者toString方法转换为对应的原始值。
ToNumber,将不同的原始值,都转换为数字。对应关系:
undefined NaN
null +0
布尔值 true转换1,false转换为+0
数字 无须转换
字符串 有字符串解析为数字,例如:‘324’转换为324,‘qwer’转换为NaN
对象(obj) 先进行 ToPrimitive(obj, Number)转换得到原始值,在进行ToNumber转换为数字
ToString,将不同的原始值,都转换为字符串,转换规则:
undefined 'undefined'
null 'null'
布尔值 转换为'true' 或 'false'
数字 数字转换字符串,比如:1.765转为'1.765'
字符串 无须转换
对象(obj) 先进行 ToPrimitive(obj, String)转换得到原始值,在进行ToString转换为字符串
一个例子:
({} + {}) = ?
两个对象的值进行+运算符,肯定要先进行隐式转换为原始类型才能进行计算。
1、进行ToPrimitive转换,由于没有指定PreferredType类型,{}会使默认值为Number,进行ToPrimitive(input, Number)运算。
2、所以会执行valueOf方法,({}).valueOf(),返回的还是{}对象,不是原始值。
3、继续执行toString方法,({}).toString(),返回"[object Object]",是原始值。
故得到最终的结果,"[object Object]" + "[object Object]" = "[object Object][object Object]"
然而,这个规则知道后,对于不同运算符,有不同且很复杂的转换关系。譬如 ==
== 是玄学,这个表达式不具备传递性。0 == "0" 是true, 0 == [] 是true,0 == "\t" 是true,但是"0" [] "\t"互相不等
比较运算 x==y, 其中 x 和 y 是值,返回 true 或者 false。这样的比较按如下方式进行:
1、若 Type(x) 与 Type(y) 相同, 则
1* 若 Type(x) 为 Undefined, 返回 true。
2* 若 Type(x) 为 Null, 返回 true。
3* 若 Type(x) 为 Number, 则
(1)、若 x 为 NaN, 返回 false。
(2)、若 y 为 NaN, 返回 false。
(3)、若 x 与 y 为相等数值, 返回 true。
(4)、若 x 为 +0 且 y 为 −0, 返回 true。
(5)、若 x 为 −0 且 y 为 +0, 返回 true。
(6)、返回 false。
4* 若 Type(x) 为 String, 则当 x 和 y 为完全相同的字符序列(长度相等且相同字符在相同位置)时返回 true。 否则, 返回 false。
5* 若 Type(x) 为 Boolean, 当 x 和 y 为同为 true 或者同为 false 时返回 true。 否则, 返回 false。
6* 当 x 和 y 为引用同一对象时返回 true。否则,返回 false。
2、若 x 为 null 且 y 为 undefined, 返回 true。
3、若 x 为 undefined 且 y 为 null, 返回 true。
4、若 Type(x) 为 Number 且 Type(y) 为 String,返回比较 x == ToNumber(y) 的结果。
5、若 Type(x) 为 String 且 Type(y) 为 Number,返回比较 ToNumber(x) == y 的结果。
6、若 Type(x) 为 Boolean, 返回比较 ToNumber(x) == y 的结果。
7、若 Type(y) 为 Boolean, 返回比较 x == ToNumber(y) 的结果。
8、若 Type(x) 为 String 或 Number,且 Type(y) 为 Object,返回比较 x == ToPrimitive(y) 的结果。
9、若 Type(x) 为 Object 且 Type(y) 为 String 或 Number, 返回比较 ToPrimitive(x) == y 的结果。
10、返回 false。
javascript 变量作用域与函数
1 声明提升
- 变量声明,不管在哪里发生(声明),都会在任意代码执行前处理。
console.log(a); // undefined,这时a已经声明了,但是赋值动作不会提前,所以是undefined
var a = 1; // 要使用var进行声明,否则上一句会报错
ps: 函数定义必须要有名称如:function a() {...},但是如果和操作符一起使用就被看作是函数表达式,可以没有名称,譬如:
var a = function() {};
!function() {};
(function() {});
javascript奇特的模块化实现
这里的模块化是可以解决命名污染的问题,在JavaScript中,最高级别的函数外定义的变量都是全局变量(这意味着所有人都可以访问到它们)。也正因如此,当一些无关的代码碰巧使用到同名变量的时候,程序就乱套了。
我们确实可以将不同的js放在不同文件中,然后需要的时候引用,但是引用的文件中如果使用了相同的变量,就会互相干扰。我们需要的是,在不同的模块中有不同的命名空间。
由于这些特点,javascript使用了一个中奇特的方式,实现了模块化。
- js变量作用域不是以{}区分,而是以函数为作用域,更重要的是,在函数中声明局部变量时一定要使用var进行声明,否则会默认声明为全局变量
- 函数内部可以方便的访问外部的变量。
- 最nb的一点:通常,函数中定义的局部变量,在函数调用完成退出时会被清理掉;在js中,一个函数执行完后,其中的变量仍能留存在内存中,供大家使用。
也就是最奇葩的这点,是js实现模块化的基础。
var MODULE = (function () {
var myProp = {},
myProp.prop = 1;
myProp.moduleMethod = function () {
// ...
};
return myProp;
}());
上个例子,匿名函数已经执行完了,MODULE中还可以使用myProp中的变量,,,就问你棒不棒。
this的含义
this是 JavaScript 语言的一个关键字,它是函数运行时,在函数体内部自动生成的一个对象,只能在函数体内部使用。函数的不同使用场合,this有不同的值。总的来说,this就是函数运行时所在的环境对象
- 纯粹的函数调用。属于全局性调用,因此this就代表全局对象
- 作为对象方法的调用。数还可以作为某个对象的方法调用,这时this就指这个上级对象
function test() {
console.log(this.x);
}
var obj = {};
obj.x = 1;
obj.m = test;
obj.m(); // 1
- 作为构造函数调用。谓构造函数,就是通过这个函数,可以生成一个新对象。这时,this就指这个新对象。
function test() {
this.x = 1;
}
var obj = new test();
obj.x // 1
- apply 调用。apply()是函数的一个方法,作用是改变函数的调用对象。它的第一个参数就表示改变后的调用这个函数的对象。因此,这时this指的就是这第一个参数。
var x = 0;
function test() {
console.log(this.x);
}
var obj = {};
obj.x = 1;
obj.m = test;
obj.m.apply() // 0
apply()的参数为空时,默认调用全局对象。因此,这时的运行结果为0,证明this指的是全局对象。
如果把最后一行代码修改为
obj.m.apply(obj); //1
拓展:apply,call,bind的异同?
apply和call基本功能是类似的,除了传递参数方式有所不同。它们都是为了改变当前函数的运行环境(改变函数体内部的this),apply和call的第一个参数就是函数内部的this。
用法就是:f.apply(foo) 返回f这个函数在foo这个上下文下的执行结果。
bind与它们有所不同。 f.bind(foo) 会返回一个函数,这个函数内部的this不会根据上下文而改变了,无论在哪里执行,都会bind foo这个上下文。
js语言精粹
函数
- 函数对象 JS中的函数就是对象。对象是"名/值"对的集合并拥有一个连到原型对象的隐藏连接。对象字面量产生的对象连接到Object.prototype。函数对象连接到Function.prototype(该原型对象本身连接到Object.prototype)。每个函数在创建时会附加两个隐藏属性:函数的上下文和实现函数行为的代码(类似于句柄)。
- 函数调用 this与调用模式:参数this的值取决于调用的模式。在JS中,一共有4种调用模式:方法调用模式,函数调用模式,构造器调用模式和apply调用模式。这些模式在如何初始化关键参数this上存在差异。
方法调用模式:
当一个函数被保存为对象的一个属性时,我们称之为方法。当方法被调用时,this被绑定到该对象
函数调用模式:
当一个函数并非一个对象的属性时,那么它就是被当做一个函数来调用的。以此模式调用函数时,this被绑定到全局对象。这是语言设计的一个错误!如果设计正确,那么当内部函数被调用时,this应该仍然绑定到外部函数的this变量。这个错误设计的后果是方法不能利用内部函数来帮助它工作,因为内部函数的this被绑定了错误的值,所以不能共享该方法对对象的访问权。
构造器调用模式:
函数创建的目的是结合new前缀来调用,那它就被称为构造器函数。按照约定,它们保存在以大写格式命名的变量里。
var Quo = function(str){ //创建一个名为Quo的构造器函数,它创建一个带有status属性的对象。
this.status = str;
};
//给Quo的所有实例提供一个get_status的公共方法
Quo.prototype.get_status = function(){
return this.status;
};
//构造一个Quo实例
var myQuo = new Quo("confused");
document.writeln(myQuo.get_status()); //打印显示"confused"
Apply调用模式:
apply方法允许我们构建一个参数数组传递给调用函数,同时允许我们选择this的值。apply方法接收两个参数,第1个是要绑定给this的值,第2个是一个参数数组。
- 扩充类型的功能 通过给Object.prototype添加方法,可以让该方法对所有对象可用; 通过给Function.prototype增加方法, 可以使该方法对所有函数可用。由于JS原型继承的动态本质,新的方法立刻被赋予到所有的对象实例上,即使对象实例是在方法增加之前创建的。
//为Function.prototype增加method方法,方便以后创建新的方法
Function.prototype.method = function(name,func){
if(!this.prototype[name]){ //没有该方法时才添加
this.prototype[name] = func;
}
return this;
};
//为Number.prototype增加一个integer方法,用于提取数字中的整数部分
Number.method('integer',function(){
return Math[this<0 ? 'ceil' : 'floor'] (this);
});
//为String添加移除字符串首尾空白的方法
String.method('trim',function(){
return this.replace(/^\s+|\s+$/g,'');
})
js相关知识
类的创建方法
- 构造函数法 经典方法,也是教科书必教的方法。它用构造函数模拟"类",在其内部用this关键字指代实例对象。主要缺点是,比较复杂,用到了this和prototype,编写和阅读都很费力。
function Cat() {
this.name = "大毛";
}
var cat1 = new Cat();
alert(cat1.name); // 大毛
- Object.create()法.更方便地生成对象,Javascript的国际标准ECMAScript第五版(目前通行的是第三版),提出了一个新的方法Object.create()
var Cat = {
name: "大毛",
makeSound: function(){ alert("喵喵喵"); }
};
var cat1 = Object.create(Cat);
alert(cat1.name); // 大毛
cat1.makeSound(); // 喵喵喵
- 极简主义法
var Cat = {
createNew: function(){
var cat = {};
cat.name = "大毛";
cat.makeSound = function(){ alert("喵喵喵"); };
return cat;
}
};
var cat1 = Cat.createNew();
cat1.makeSound(); // 喵喵喵
_proto_和protoType
简单来说:
- 对象有属性__proto__,指向该对象的构造函数的原型对象。
let a = new b();
a.__proto___===b.prototype//true
- 方法除了有属性__proto__,还有属性prototype,prototype指向该方法的原型对象。
进一步来讲:
- protoType:原型对象,proto:原型链指针
- 原型对象的用途是为每个实例对象存储共享的方法和属性,它仅仅是一个普通对象而已。并且所有的实例是共享同一个原型对象,因此有别于实例方法或属性,原型对象仅有一份。
function Person () {
this.name = 'John';
}
var person = new Person();
Person.prototype.say = function() {
console.log('Hello,' + this.name);
};
person.say();
Person原型对象定义了公共的say方法,虽然此举在构造实例之后出现,但因为原型方法在调用之前已经声明,因此之后的每个实例将都拥有该方法。
但是如果这样写:
function Person () {
this.name = 'John';
}
var person = new Person();
Person.prototype = {
say: function() {
console.log('Hello,' + this.name);
}
};
person.say();//person.say is not a function
当var person = new Person()时,Person.prototype为:Person {}(当然了,内部还有constructor属性),即Person.prototype指向一个空的对象{}。而对于实例person而言,其内部有一个原型链指针proto,该指针指向了Person.prototype指向的对象,即{}。接下来重置了Person的原型对象,使其指向了另外一个对象,即 Object {say: function}, 这时person.proto的指向还是没有变,它指向的{}对象里面是没有say方法的
- 在js中,对象在调用一个方法时会首先在自身里寻找是否有该方法,若没有,则去原型链上去寻找,依次层层递进,这里的原型链就是实例对象的__proto__属性
- 原型对象的结构
Function.prototype = {
constructor : Function,
__proto__ : parent prototype,
some prototype properties: ...
};
函数的原型对象constructor默认指向函数本身,原型对象除了有原型属性外,为了实现继承,还有一个原型链指针__proto__,该指针指向上一层的原型对象,而上一层的原型对象的结构依然类似,这样利用__proto__一直指向Object的原型对象上,而Object的原型对象用Object.prototype.proto = null表示原型链的最顶端,如此变形成了javascript的原型链继承,同时也解释了为什么所有的javascript对象都具有Object的基本方法。
顺便提一下,Object其实是个函数(java同学可能会以为是个类)
preventDefault和stopPropagation
javascript中的“事件传播”模式
- 捕获模式(capturing)
- 冒泡模式(bubbling)
这两种模式就是为了一点:决定html中“元素”(比如div, p, button)接收到事件的“顺序”。
捕获模式:当事件发生时,该事件首先被最外层元素接受到,然后依次向内层元素传播。(从上向下)
冒泡模式:当事件发生时,该事件首先被最内层元素接受到,然后依次向外层元素传播。
用哪种事件传播方式完全是我们自己说了算的,我们可以使用:
addEventListener(type, listener, useCapture)
preventDefault:“阻止”元素的“默认特性”,譬如:如果一个a标签的href为空,点击后会刷新页面,那么可以在click事件中使用preventDefault,阻止页面的刷新
stopPropagation:阻止事件的传播,可以将事件的传播到该元素为止,不在继续向外或者向内传播