【语言学习】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:阻止事件的传播,可以将事件的传播到该元素为止,不在继续向外或者向内传播