重学JS(一)----- JS基础

216 阅读24分钟

JS基础

很多时候我们编写JS代码,但是可能我们并不了解JS的内部实现原理。在这里想总结平时可能会忘记的一些比较基础的知识点和一些面试的问题。用于基础知识的巩固和之后面试的复习

文章内容比较知识点比较散乱,后续有其他关于js基础的内容会继续往上添加。

数据类型

JavaScript 语言的每一个值,都属于某一种数据类型。JavaScript 的数据类型,共有七种:

  • Number:数值:包括整数和小数,比如3,3.14
  • String:字符串: 比如'hello world'
  • undefined:表示“未定义”或不存在
  • null: 表示空值,即此处的值为空
  • Object:对象:表示各种值的集合
  • Symbol:用于表示独一无二的值(ES6引入,本次内容不讲)
  • Boolean:布尔值,它的值仅能为true(真)和false(假)

对象是最复杂的数据类型,又可以分为三个子类型

  • 狭义的对象(object)
  • 数组(array)
  • 函数(function)

nullundefined

nullundefined都可以表示没有,含义非常相似。将一个变量赋值为两者时,从效果上来看基本没有任何区别。 在两者转化为boolean值时,都会被转化为false。两者使用相等运算符时为true

console.log(Boolean(undefined)) // false
console.log(Boolean(null)) // false
console.log(null == undefined) // true

面试题目:

nullundefined的区别:
在javascript中,将一个变量赋值为undefinednull几乎没区别

var a = undefined;
var a = null;
console.log(null == undefined) // true

两者几乎等价,在最开始两者通过如下方法区分:null表示一个"无"的对象,转化为数值类型时为0;undefined表示一个无的原生值,转为数值时为NaN

Number(undefined) //NaN
Number(null) // 0

目前来说:null的用法如下:

  1. 作为函数的参数,表示该函数的参数不是对象
  2. 作为对象原型的重点

undefined更想要表达的意思:就是这个地方应该有一个值,但是还没有定义。典型用法:

  1. 变量被声明,没有被赋值,则变量值为undefined
  2. 调用函数,应该提供的参数没有提供,该参数值为undefined
  3. 对象没有赋值的属性,该属性值为undefined
  4. 函数没有返回值时,默认返回undefined

在浏览器中,undefined其实是挂载在window对象上的一个属性它的值为undefined,浏览器window上的undefined属性是不可写的,因此给window.undefined赋值是无效的操作,不会改变undefined属性的值。

console.log(undefined  in window) // true
console.log(window.undefined) // undefined
window.undefined = 3;
console.log(window.undefined)

boolean

布尔值只有两个选择:truefalsetrue表示真,false表示假。

在数据类型转化时,有如下的值会被转化为false,其他值全部都转化为true

  • undefined
  • null
  • false
  • 0
  • NaN
  • "" 或者 ''

Number

Javascript内部,所有的数字都是以64位浮点数存储。即使是整数也是如此。所以,11.0是等同的。由此可以看出,Javascirpt中没有整数的概念。

面试题目

在javascript中,0.1 + 0.2 === 0.3 的输出结果,为什么?

答: 输出结果为false。在javascript中,Number类型的数值以二进制浮点数存储。0.1和0.2转化成二进制时,小数点后面是无限循环的,因此在计算机中采用近似值表示 0.1 和0.2。当0.1 + 0.2 时,在计算机内部是将两者二进制表示的数值进行相加,相加的结果和0.3在javascript中表示的二进制值相差过大,因此0.1 + 0.2 != 0.3

console.log(0.1 + 0.2 === 0.3) // false
console.log(0.1 + 0.2) // 0.30000000000000004

数值的进制:

  • 十进制: 开头不是0的数值
  • 八进制: 以0或者0o或者0O,然后接八进制数值
  • 十六进制: 以0x或者0X开头,然后接十六进制数值
  • 二进制: 以0b开头的数值然后接二进制数值

默认情况下javascript内部会将八进制,十六进制,二进制转化为十进制

console.log(012) // 10
console.log(0x12) // 18
console.log(0b1000) // 8

tips: 通常情况下,如果表示的相应进制里面出现了不属于该进制的数值会报错,但是如果是八进制中以0开头并出现了89,则被当成十进制处理。

0xzz  //  Uncaught SyntaxError: Invalid or unexpected token
0o88 // Uncaught SyntaxError: Invalid or unexpected token
ob 12 // Uncaught SyntaxError: Invalid or unexpected token

NaN是javascript的特殊值,表示"非数值类型"。它不是独立的数字类型而实一个特殊的值。它的类型为number

NaN的特点:

  • NaN任何的数值运算结果都是NaN
  • NaN不等于任何值,也不等于自身
  • NaN在布尔运算时被当作false
NaN + 1 // NaN
NaN - 2 // NaN
NaN * 3 // NaN
NaN / 4 // NaN

数值类型常用方法:

  1. parseInt()Number.parseInt()

以上两个方法功能相同。 该方法会将字符串转化为整数,如果传入的参数不是字符串则会将参数隐式转化为字符串,然后进行运算。

在字符串转化为整数的过程中,该函数从左往右一个一个转化,遇到非数字的字符则会停止转换并返回已转换过的部分。如果该字符串第一个字符无法转化,那么返回NaN

parseInt('112') // 112
parseInt('  12') // 12
parseInt(1.23) // 1
parseInt('8a') // 8
parseINt('11.23') // 11
parseInt(15zz) // 15
parseInt(ww) // NaN

该函数还有第二个参数,用于解析数值时采用的值的进制,不传默认10进制。

parseInt('1000') // 1000
parseInt('1000', 2) // 8
parseInt('1000', 4) // 64
parseInt('1000', 5) // 125
  1. Number.prototype.toFixed()

将数值四舍五入为指定小数位数的数字,返回值为字符串

(2.123).toFixed(1) // 2.1
(1.23).toFixed(0) // 1 (取整操作)
  1. Number.isNaNisNaN

两者方法有一些区别。isNaN会将参数转化为Number类型。 Number.isNaN则不会转换参数的类型。

函数作用:判断数值类型是否是NaN,如果是NaN类型则为true。否则为false

isNaN(NaN);       // true
isNaN(undefined); // true
isNaN({});        // true

isNaN(true);      // false
isNaN(null);      // false
isNaN(37);        // false

// strings
isNaN("37");      // false: 可以被转换成数值37
isNaN("37.37");   // false: 可以被转换成数值37.37
isNaN("37,5");    // true
isNaN('123ABC');  // true:  Number("123ABC")结果是 NaN
isNaN("");        // false: 空字符串被转换成0
isNaN(" ");       // false: 包含空格的字符串被转换成0

// dates
isNaN(new Date());                // false
isNaN(new Date().toString());     // true

isNaN("blabla")   // true: "blabla"不能转换成数值
                  // 转换成数值失败, 返回NaN

实现一个isNaN函数的Polyfill

function isNaN(val){
	let number = Number(val);
    return number !== number;
}

Number.isNaN就远比这个简单了,该函数不会进行类型的转化。当传入的数值是number类型,并且是NaN时才会返回true

Number.isNaN(NaN);        // true
Number.isNaN(Number.NaN); // true
Number.isNaN(0 / 0)       // true

Number.isNaN("NaN");      // false,字符串 "NaN" 不会被隐式转换成数字 NaN。
Number.isNaN(undefined);  // false
Number.isNaN({});         // false
Number.isNaN("blabla");   // false

其实利用NaN不等于自身的特性,我们可以简单实现该函数

Number.isNaN = function isNaN(v){
	return typeof v === 'number' &&  v!==v
}

String

字符串就是多个排放在一起的字符,放在单引号或者双引号之中。单引号的内部可以双引号,双引号之中也可以使用单引号。

'abc',"www"'key = "value"',"It's look good"

字符串可以被看做时字符数组,可以使用下标访问字符串的某一个字符串。但是读取的单个字符无法被修改。

var a = 'test'
a[1] // e
a[3] // t
a[4] // undefined
a[1] = 'd'
a // test
delete a[1] 
a // test

字符串可是使用length属性,该属性可以返回字符串的长度。该属性也是无法被修改的。

var a = 'test'
a.length  // 4
a.length = 5 
a.length // 4

Object

对象是Javascript的核心概念。什么是对象?简单来说的话,就是key,value键值对的集合。每一个key(必须是字符串类型,ES6又引入Symbol可以作为key)对应一个value(任意类型)。如果我们设置的key不是字符串,则会被自动转化为字符串。

var obj = {
	name: 'Evan',
	age: 22,
    123: 'a',
}
obj['name'] // 'Evan'
obj.age  // 22
obj['123'] // 'a'

如果键名不符合标识名的条件(第一个字符为数字,含有空格或者运算符)则必须加上引号。如果我们在通过一个根据一个变量作为key去访问obj的内容,则必须使用[]去获取相应的对象的值。

var obj = {
	1p: "hello world" //Uncaught SyntaxError: Invalid or unexpected token
} 
var obj = {
    '1p': "hello world" // good
} 
var obj1 = {
    test: 1,
    foo: 2
}
var key = 'test'
obj1[key] // 1
key = 'foo'
obj1[key] // 2

Function

函数可以通过三种方式声明

function foo(v){
    console.log(v)
}

var foo1 = function(v){
    console.log(v)
}

var foo2 = new Function('v', 'console.log(v)') // Function 前面n个参数都是函数内部的参数名称,最后一个参数为函数体内部执行的代码。

函数可以被多次声明,后面的声明会覆盖前面的声明

function f(){
	console.log(1)
}

function f(){
	console.log(2)
}
f() // 2

变量提升

在javascript中存在变量提升,即我们在声明变量并进行初始化时,我们会将变量的声明放在最前面去执行。

var a = 1
// 等价于
var a ;
a = 1;

var f = function(){}
// 等价于:
var f;
f = function(){}

因此我们在使用定义变量之前可以使用该变量,其值为undefined

console.log(a) // undefined
var a = 1;
// 等价于 
var a;
console.log(a)
a = 1;

f()
var f = function(){}
// 等价于
var f;
f() // TypeError: undefined is not a function
f = function(){}
console.log(a)
var a = 1;
var a = function(){
	console.log(123)
}
console.log(a)

扩展:

函数作为JavaScript中的一等公民,它的地位和其他值(Number, String, Boolean)地位相同。凡是可以使用值的地方就可以使用函数。比如我们可以把函数赋值给某个变量。我们可以在函数中返回一个函数。我们可以把函数作为参数传递给一个函数。函数作为一个可以执行的值,没有什么特别的地方。

函数作用域

在ES5中,JavaScript中只存在两种作用域。1. 全局作用域 2. 函数作用域

在浏览器中,全局作用域就是window对象。我们在全局作用域中定义的变量实际上都会挂在到window对象上,都可以在window上找到对应的属性。而我们在函数内部的定义的变量,只能在函数内部访问,函数外部无法访问(包括我们传入函数的参数也是如此)。

var a = 1;
window.a // 1

function foo(b, c){
	console.log(a, b, c)
}
foo(2, 3) // 1 2 3
console.log(b) // ReferenceError: b is not defined

然后我们接下来在看一个栗子:

// --------最外层作用域--------
var a = 1;
var x = 3
function foo1(){
	// --------foo1作用域--------
	var a = 2;
    var b = 4;
    console.log("a in foo1", a) // a in foo1 2
    return function foo2(){
    	// --------foo2作用域--------
    	var a = 3;
        console.log("a in foo2", a) // a in foo2 3
        console.log("b in foo2", b) // a in foo2 4
        console.log("x in foo2", x) // x in foo2 3
        console.log("v in foo2", v) // ReferenceError
        // --------foo2作用域--------
    }
    // --------foo1作用域--------
}
var func = foo1()
func()

// --------最外层作用域--------

在javascript中,我们可以把全局作用域看成最外层的作用域。其中,最外层作用域中包含了一个foo1作用域,foo1可以访问全局作用域,但是全局作用域,无法访问foo1作用域。那么如果,我们在foo1作用域中在定义一个函数foo2,那么foo2函数内部就会存在foo2作用域。那么按照规则,foo1作用域在foo2作用域的外层,那么我们foo2应该也可以访问foo1的作用域。

最后我们可以得到如下的结果 全局作用域 包含 foo1作用域, foo1作用域包含 foo2作用域。在某个作用域中访问某个变量时,我们从内往外查找,遵循就近原则。如果找到最外层的全局作用域都没找到变量,则报错。

因此,我们在foo2中访问变量,查找该变量的作用优先级为 foo2 > foo1 > 全局作用域。某个函数外层的作用域取决于在哪里声明该函数。

闭包

在上面我们知道,JavaScript中有两个作用域。并且,我们函数作用可以访问全局作用域的变量。 正常情况下,我们函数外部无法读取函数内部的变量,但是某些情况下,我们确实需要函数的外部访问函数内部的变量,那么在JavaScript中我们可以编写如下的函数

function f1(){
	var a = 1;
    return function(){
    	console.log(a)
        return a
    }
}

var foo = f1()
foo() // 1

所谓的闭包就是函数f2,我们在f2中可以读取f1中定义的变量。这个时候,就有一个很有意思的现象。 假设我们将返回的函数改成如下的内容

function f1(){
	var a = 1;
    return function f2(){
    	a++;
        return a
    }
}

var foo = f1()
foo() // 2
foo() // 3
a++ //  Uncaught ReferenceError: a is not defined

我们在f1函数内部定义了一个变量a,然后返回一个函数,每次执行该函数会让f1函数内部的a加一。根据我们上面讲述的原则,我们在外部作用域是无法获取内部作用的变量。但是,我们的函数f2的外部作用域是f1的函数作用域。我们可以在f1中访问a变量。

当我们在全局作用域执行f1 函数,该函数会将内部定义的函数f2暴露给全局作用域。这时,我们就有了访问和修改a变量的方法:调用f2函数。与此同时,在全局作用域中,除了f2我们没有任何办法改变f1函数内部的a变量。我们根据闭包,相当于将a变量从全局作用隐藏起来,并且提供一种可以修改和访问a变量的方法。

tips: 当我们每次允许外层函数f1时,我们都会生成一个闭包。这个闭包会保存f1函数内部定义的变量a。该变量不会被浏览器回收。因此,我们不能滥用闭包,产生大量的闭包会占用大量的内存,从而导致网页性能问题。

函数柯里化

假设我们想实现一个函数,该函数可以实现相乘的操作。 一般情况下,我们的实现如下:

function mul(a, b){
	return (a, b);
}
mul(34) // 12

那么还没有其他的方法呢,假设有函数curryingMul,我们想实现如下效果:curryingMul(3)(4) // 12

根据上面所说的方法实现该方法:

function curryingMul(a){
	return function(b){
    	return a * b;
    }
}
curryingMul(3)(4) // 12

这就是函数柯里化最简单的一个例子。我们将原来一次传入直接传入两个参数的函数。变成了先用一个函数接受a,然后返回另外一个函数,该函数接受b。并在返回的函数内部处理剩下的参数。那么问题来了,我们为什么费这么事情写这么麻烦的东西呢?

函数柯里化的好处:

  1. 参数的复用 比如,现在有同学,一个小明,一个小红。两个人比赛吃东西。

按照正常的写法,我们编写eatApple函数。

function eatFood(people, food){
	console.log( people + ' eat ' + food);
}

// 使用
eatApple('小明', '苹果');
eatApple('小明', '梨子');
eatApple('小红', '橙子');

我们可以使用函数柯里化避免其中参数的重复执行。因此我们产生编写如下的函数

function curryingEatFood(people){
	return function(food){
    	console.log( people + ' eat ' + food);
    }
}

// 使用
var mingEat = curryingEatFood('小明');
var hongEat = curryingEatFood('小红');

mingEat('苹果')
mingEat('梨子')
hongEat('橙子')

这里的话,如果小明或者小红需要吃其他的东西,我们可以避免重复传入'小红'和'小明'。实现参数的复用。

  1. 提前返回

提前返回的主要意思,我们可以在进行函数柯里化时,对参数进行提前的处理。在这里我们可以很方便的进行兼容性的处理。

举个栗子: 还是刚才的小红或者小明,假设目前只有这两个人在比赛,其他人比如小波的也想吃东西。但是小波吃的东西不能算在小红和小明的比分里面。因此,我们对刚才的函数再次进行扩展。

function curryingEatFood(people){
	if(people === '小明' || people === '小红'){
    	people = "比赛人员" + people
    } else {
    	people = "比赛无关人员" + people
    }
	return function(food){
    	console.log( people + ' eat ' + food);
    }
}

// 使用
var mingEat = curryingEatFood('小明');
var hongEat = curryingEatFood('小红');
var boEat = curryingEatFood('小波')

mingEat('苹果')
mingEat('梨子')
hongEat('橙子')
boEat('榴莲')
  1. 延迟执行

还是上面那个栗子,我们继续讲述。假设现在小红已经吃饱饱了。然后她想先休息一会,过了几秒钟在继续吃个苹果。而我们的小明想犯规,他想把喝水算到比赛里面。这个时候,我们便可以继续修改原来的代码。

function curryingEatFood(people){
	if(people === '小明' || people === '小红'){
    	people = "比赛人员" + people
    } else {
    	people = "比赛无关人员" + people
    }
	return function(food){
    	console.log( people + ' eat ' + food);
    }
}

// 使用
var mingEat = curryingEatFood('小明');
var hongEat = curryingEatFood('小红');
var boEat = curryingEatFood('小波')

var food = ['苹果', '梨子', '橙子', '榴莲', '西瓜']

function isFood(eatFood){
	for(let i = 0; i < food.length; i++ ){
    	if(food[i] === eatFood){
        	return true;
        }
    }
    return false;
}
var mingFood = '喝水'
if(isFood(mingFood)){
	mingEat(mingFood)
}

setTimeout(() =>{ hongEat('苹果') }, 2000)

讲完这个栗子,我们来看待函数柯里化的应用:

栗子1: 我们可以使用函数柯里化实现bind函数

var slice = Array.prototype.silice;
Function.prototype.bind = function() {
  var thatFunc = this, thatArg = arguments[0];
  var args = slice.call(arguments, 1);
  return function(){
    var funcArgs = args.concat(slice.call(arguments))
    return thatFunc.apply(thatArg, funcArgs);
  };
};

栗子2: 函数柯里化实现防抖功能

防抖: 比如我们在百度上搜索问题,如果我们一直输入文字,我们不可能每次键盘按下之后,都去触发相关函数,给我们显示下拉框的相关提示信息。那么防抖的作用就来了,如果我们一直敲击键盘,我们则不去触发下拉框的提示信息,而当我们某次敲击键盘,并且在n秒之后没有再次敲击键盘,则我们去触发相应函数,显示提示信息。

var debounce = function(fn, delay){
	var id;
    return function(){
    	var args = Array.prototype.slice.call(arguments),
        	context = this;
        var newFn = function(){
        	id = null;
            fn.apply(context, args);
        }
        
    	if(id){
        	clearTimeout(id);
        }else{
        	timer = setTimeout(newFn, delay);
        }
    }
}
function add(x, y){
    return x + y;
}
var a = add;
a(1, 2) // 3
function test(){
    // doSomeThing
    return add
}

function addExtend(add){
    var c = 3;
    return c + add(1, 2);
}

复合函数

根据上面我们所描述的内容,我们设想以下我们如何实现一个如下功能的函数

var add = function(v){
	return v + 10;
}
var mul = function(v){
	return v * 2;
}

var sub = function(v){
	return v - 5
}

// 假设我们传入一个值value,我们需要先让value + 10, 然后将得到的值 (value + 10) * 2,最后将(value + 10) * 2 得到的值 减去 5

常规的做法是这样的:
var value = 5;
var res = add(value);
res = mul(res);
res = sub(res);

或者:
var value = 5;
var res = sub(mul(add(value)));

我们尝试编写compose函数。用于组合两个函数,返回一个新的函数

var add = function(v){
	return v + 10;
}
var mul = function(v){
	return v * 2;
}
function compose(a, b){
	return function(){
    	var args = Array.prototype.slice.call(arguments)
    	return b(a.apply(null, args))
    }
}
compose(add, mul)(3) // 26 (3 + 10) * 2

这就是所谓的复合函数,我们接受多个函数,将多个函数组合,返回一个新的函数。然后由于这里我们只能接受两个函数:a和b。然后我们尝试扩展compose函数接收多个函数,并且将这些函数组合。

var add = function(v){
    return v + 10;
}
var mul = function(v){
    return v * 2;
}
var sub = function(v){
    return v - 5
}
function compose(){
	var fns = Array.prototype.slice.call(arguments);
    function composeFn(a, b){
        return function(){
            var args = Array.prototype.slice.call(arguments)
            return b(a.apply(null, args))
        }
    }
    return fns.reduce(composeFn)
}
compose(sub, add, mul)(6) // 22 ((6 -5) + 10) * 2

高阶函数

高阶函数本身肯定是一个函数。之所以有了高阶两个词,就是该函数的参数是函数 或者返回的是一个函数,则我们可以将其称之为高阶函数。在上面的代码中我们已经编写了许多的高阶函数。

举个栗子,我们平时使用的Array.prototype.mapArray.prototype.forEach 就是高阶函数。

在react中,我们通过高阶函数的思想,便可以编写高阶组件。同时,我们通过compose组合函数,可以完成react中间件的实现(后面有空的话写一些关于react的内容)


看到我们上面写法,我们总归感觉有一些不优雅,我们在想,我们能不能实现一个函数,合并上面的函数,返回一个最终的函数完成所有的结果。

举个栗子:
```javascript
let compose = function(a, b){
	return function(){
    	a(b(value))
    }
}

类型转换

强制类型转换

Number: 将任意类型转化为数值

  1. 原始类型值
Number(324) // 324

Number('324') // 324

Number('324abc') // NaN

Number('') // 0

Number(true) // 1
Number(false) // 0

Number(undefined) // NaN

Number(null) // 0
  1. 对象 Number将对象转化为数值时,规则会稍显不同。 首先会调用Object.valueOf()方法获取原始之值,如果原始值是原始类型,则调用Number函数;如果原始值是对象类型,则将对象自身的toString方法。该方法返回的是原始类型则调用Number,否则报错。

tips: js的数据类型可划分为原始类型和引用数据类型。原始数据类型(Undefined, Null, Boolean, Number, String) 引用类型(对象,数组,函数)

Number({}) // NaN
Number([1,23]) // NaN
Number([5]) // 5

var obj1 = {
	valueOf: function(){
    	return {};
    },
    toString(): function(){
    	return {};
    }
};

Number(obj1) // TypeError: Cannot convert object to primitive value

var obj2 = {
	valueOf: function(){
    	return 2;
    }
}
Number(obj2) // 2

var obj3 = {
	toString: function(){
    	return 3
    }
}
Number(obj3) // 3

var obj4 = {
	valueOf: function(){
    	return 2;
    },
    toString: function(){
    	return 3;
    }
}
Number(obj4) // 2

String

  1. 原始值类型 数值(Number):转化为对于字符串 字符串(String):字符串 布尔值: true => 'false', false => "false"。 undefined: 转为字符串"undefined" null: 转为字符串"null"
String(123) // "123"
String('abc') // "abc"
String(true) // "true"
String(undefined) // "undefined"
String(null) "null"
  1. 对象 String方法的规则和Number方法基本相同。只是将valueOftoString方法先后顺序调换。

  2. 先调用对象自身的toString方法。如果返回原始值类型,则对该值使用String函数,不再进行以下步骤。

  3. 如果toString方法返回的是对象,再调用原对象的valueof方法。如果valueof方法返回原始值类型的值,则对该值使用String函数,不再进行以下步骤。

  4. 如果valueOf方法返回的是对象,就报错。

String({a: 1}) 
// "[object Object]"

String([1]) // 1
String([1, 2]) // 1,2

var obj1 = {
	valueOf: function(){
    	return {};
    },
    toString(): function(){
    	return {};
    }
};

String(obj1) // Uncaught SyntaxError: Unexpected token ':'

var obj2 = {
	valueOf: function(){
    	return 2;
    }
}
String(obj2) // "[object Object]"

var obj3 = {
	toString: function(){
    	return 3;
    }
}
String(obj3) // 3

var obj4 = {
	valueOf: function(){
    	return 2;
    },
    toString: function(){
    	return 3;
    }
}
String(obj4) // 3

Boolean()

Boolean()的转化规则相当的简单:除了以下五个值的转换结果为false,其他的值全部为true

  • undefined
  • null
  • 0 (包含-0+0)
  • NaN
  • ''(空字符串)
// 以下为false的情况
Boolean(undefined) // false
Boolean(null) // false
Boolean(NaN) // false
Boolean('') // false
Boolean(false) // false

隐式数据类型转换

在JavaScript中我们经常编写如下的函数:

123 + 'abc'
if(3 > 1){
	// todo
}
+ {foo: 'bar'} //NaN
!! {} // true

在我们编写如上的内容时,其实javascript内部自动帮我们调用上述我们讲解的NumberString,和Boolean方法。

  1. 自动转化为布尔值

在JavaScript在预期为boolean值的地方(比如if条件),会自动调用Boolean()将其他类型转化为boolean值。

  1. 自动转化为字符串 在Javascript在预期为String值的地方,会自动调用Boolean()将非字符串转化为字符串。

字符串的自动转换主要发生在字符串的加法运算时。

'5' + 1 // "51"
'5' + true // "5true"
'5' + false // "5false"
'5' + {} // "5[object Object]"
'5' + function(){} // "5function (){}"
'5' + undefined // "5undefined"
'5' + null // "5null"
  1. 自动转化为数值 javascript在预期为数值的地方,就会将参数自动转化为数值。系统内部会自动调用Number()函数。

除了加法运算法(+)有可能会将运算结果变为字符串,其他运算符都会将运算结果变为数值。

'5' - '2' // 3
'5' * '2' // 10
true - 1 // 0
false - 1 // -1
'1' - 1 // 0
'5' * [] // 0
false / '5' // 0
'abc' - 1 // NaN
null + 1 // 1
undefined + 1 // NaN

确定值类型的方法

javascript有三种方法可以确定值的类型:

  1. typeof运算符
  2. instanceof运算符
  3. Object.prototype.toString方法

typeof

typeof 123 // number
typeof '123' // string
typeof true // boolean
function func(){}
typeof func // function
typeof [] // object
typeof {} // object
typeof null // object
typeof undefined // undefined
typeof Symbol(12) // symbol

其中,可以发现null的类型为object,这时由于历史原因造成的。当初设计js数据类型时,没有考虑将null作为数据类型,而是将null作为object的一种特殊的值。

instanceof

instanceof运算符返回一个布尔值,表示对象是否是某个构造函数的实例。instanceof左边是实例对象,右边是构造函数。它会检查右边的构造函数的原型对象是否在左边对象的原型链上。

var v = new Vehicle()
v instanceof Vehicle // true

由于instanceof检查整个原型链,一个对象的原型链上可能会有多个对象,因此可能对多个构造函数返回true

var d = new Date();
d instanceof Date() // true
d instanceof Object // true

根据这个特性,我们可以使用instanceof来判断一个对象的值类型(不适用原始类型)。

var x = [1, 2, 3];
var y = {};
x instanceof Array // true
y instanceof Object // true

toString

每一个对象都有一个toString方法。默认情况下,toString方法被每个Object对象继承。如果此方法在自定义对象中未被覆盖,toString()返回"[object type]"。其中type则是对象的类型。上面的代码中,我们在强制转换的时候,尝试过给对象加入toString方法,当对象定义了该方法,则不能直接调用该方法,因此可以使用Object.prototype.toString.call来安全的判断数据类型。

var toString = Object.prototype.toString;
toString.call(new Date) // [object Date]
toString.call(new String) // [object String]
toString.call(Math) // [object Math]
toString.call({}) // [object Object]

this

this关键字javascript是一个非常重要的语法点。this可以用在各个场合,this总是返回一个对象。简单来说,this是用来返回属性或者方法"当前"所处的对象。

var obj = {
	name: '张三',
    describe: function(){
    	return 'name: ' + this.name;
    }
}

var obj2 = {
	name: '李四'
}

obj2.describe = A.describe;
obj2.describe() "name: 李四"


// 等价于
function func(){
	return "name: " + this.name;
}

var obj = {
	name: "张三",
    describe: func
}

var obj2 = {
	name: "李四",
    describe: func
}

obj1.describe() // "name: 张三"
obj2.describe() // "name: 李四"

this设计的目的是在函数体内部,指代函数当前的运行环境。

var f = function() {
	console.log(this.x);
}

var x = 1;
var obj = {
	f: f,
    x: 2
}

var obj2 = {
	x: 3,
    obj3: {
    	f: f,
        x: 4
    }
}

f() // 1 当前环境为window

obj.f() // 2 当前环境为 obj

obj2.obj3.f() // 4

function createObj(p){
	this.p = p;
}

var obj = new createObj('Hello World');
obj.p // Hello World

简单来说,this的指向取决于使用它是的上下文环境,this指向的一定是一个对象。

Function.prototype.call()

函数原型上的call方法可以指定函数内部的this指向(即函数执行时所在的作用域),然后在所指定的作用域,调用该函数。

var obj = {};

var f = function(){
	return this;
}

f() === window // true
f.call(obj)  === obj // true

// call的参数应该是一个对象,如果参数是空,null以及undefined,则默认传入全局对象。
var n = 123;
function func(){
	console.log(this.n)
}
func.call() // 123
func.call(null) // 123
func.call(undefined) // 123
func.call(window) // 123

apply方法与call方法类似。只是apply参数是接受一个数组作为函数执行的参数。

原型

在JavaScript中,我们通过构造函数生成新的对象,因此构造函数可以视为对象的模版。实例对象的属性和方法可以定义在函数内部。

function Cat(name, color) {
	this.name = name;
    this.color = color;
    this.eat = function(){
    	console.log(eat fish)
    }
}

var cat1 = new Cat('大毛', '白色');
var cat2 = new Cat('二毛', '黑色');

cat1.eat === cat2.eat // false 

cat1.name // 大毛
cat1.color // 白色

cat2.name // 二毛
cat2.color // 白色

但是以上代码存在一个问题,当我们试图为同一个构造函数创建多个实例时,无法共享属性,从而造成对系统资源的浪费。

在JavaScript中,原型对象的所有属性和方法都能被实例对象共享。也就是说,如果属性和方法定义在原型上,那么所有实例对象就能共享,不仅节省了内存,还体现了实例对象之间的联系。并且如果你在实例出来的对象中,已经存在了某个原型上也存在的属性。则优先使用实例对象上的属性内容。

function Animal(name){
	this.name = name;
}

Animal.prototype.color = 'white';

var cat1 = new Animal('cat1');
var cat2 = new Animal('cat2')

cat1.color // 'white'
cat2.color // 'white'

原型链 JavaScript中规定,所有对象都有自己的原型对象。一方面,任何一个对象,都可以充当其他对象的原型;另一个方面,由于原型也是对象,所以它也有自己的原型对象。因此,就会形成一个原型链:对象到原型,再到原型的原型。

如果一层层地上溯,所有对象的原型最终都可以上溯到Object.prototype,即Object构造函数的prototype属性。也就是。最后的结果就是所有的对象都会有valueOftoString方法,因为这就是从Object.prototype继承的。

Object.prototype的原型对象是nullnull没有任何属性和方法,也没有自己的原型。因此,原型链的尽头就是null