本文系统梳理JavaScript核心机制,涵盖7种数据类型及typeof判断陷阱、var/let/const声明与变量提升、函数定义与参数处理(arguments/rest/default)、对象与构造函数、this指向与原型链、以及闭包、柯里化、尾递归等高级特性。通过经典代码实例解析JavaScript语言精粹,帮助开发者深入理解JS底层原理与最佳实践。
JavaScript 数据类型与判断
数据类型
JavaScript 属于弱类型语言,但这并不意味着它没有类型系统。JavaScript 能够识别以下 7 种不同类型的值:
基本数据类型
1. Boolean(布尔值)
2. Number(数值)
3. String(字符串)
4. null(空值)
5. undefined(未定义)
6. Symbol(符号)
对象类型
1. Array(数组)
2. RegExp(正则表达式)
3. Date(日期)
4. Math(数学对象)
5. ...(其他内置对象)
可以使用 typeof 操作符来判断数据类型,该操作符返回一个字符串。需要注意的是,并非所有返回结果都完全符合预期,例如 null 会被判断为 object。
typeof false // "boolean"
typeof .2 // "number"
typeof NaN // "number"
typeof '' // "string"
typeof undefined // "undefined"
typeof Symbol() // "symbol"
typeof new Date() // "object"
typeof [] // "object"
typeof alert // "function"
typeof null // "object"
typeof not_defined_var // "undefined"
变量声明与提升机制
变量
在应用程序中,使用变量来为值命名。变量的名称在 JavaScript 中被称为标识符(identifiers)。
声明
1. 使用关键字 var:具有函数作用域。
2. 使用关键字 let:具有块级作用域(block scope local variable)。
3. 直接赋值(不使用关键字):具有全局作用域。
var global_var = 1;
function fn () {
var fn_var = 2;
if(fn_var > 10){
let block_var = 3;
global_var2 = 4;
}
}
如果只声明变量而不赋值,该变量的默认值为 undefined。
const 关键字用于声明不可变变量,同样具备块级作用域。需要注意的是,对于对象类型的不可变性理解要格外小心,const 仅保证引用地址不变,不保证对象内部属性不可变。
const num = 1;
const obj = {
prop: 'value'
};
num = 2; // Uncaught TypeError: Assignment to constant variable.
obj['prop'] = 'value2';
obj = []; // Uncaught TypeError: Assignment to constant variable.
变量提升
JavaScript 允许引用稍后声明的变量而不会立即引发错误,这一概念称为变量声明提升(hoisting)。
console.log(a); // undefined
var a = 2;
其执行机制等同于将变量声明提升到代码块顶部,但赋值保留在原位。
var a;
console.log(a);
a = 2;
函数定义与参数处理
函数
一个函数是一个可以被外部代码调用(或者函数本身递归调用)的子程序。
定义函数
1. 函数声明(Function Declaration)
2. 函数表达式(Function Expression)
3. Function 构造函数
4. 箭头函数(Arrow Function)
function fn(){}
var fn = function(){}
var fn = new Function(arg1, arg2, ... argN, funcBody)
var fn = (param) => {}
arguments 对象
1. arguments:一个包含了传递给当前执行函数参数的类似于数组的对象。
2. arguments.length:传给函数的参数的数目。
3. arguments.caller:调用当前执行函数的函数(在非严格模式下,ES5 已废弃此属性)。
4. arguments.callee:当前正在执行的函数(在非严格模式下,ES5 已废弃此属性)。
function foo() {
return arguments;
}
foo(1, 2, 3); // Arguments[3]
// { "0": 1, "1": 2, "2": 3 }
rest 参数
function foo(...args) {
return args;
}
foo(1, 2, 3); // Array[3]
// [1, 2, 3]
function fn(a, b, ...args){
return args;
}
fn(1, 2, 3, 4, 5); // Array[3]
// [3, 4, 5]
默认参数
函数的参数可以在定义的时候约定默认值,如果调用时未提供该参数,则使用默认值。
function fn (a = 2, b = 3) {
return a + b;
}
fn(2, 3); // 5
fn(2); // 5
fn(); // 5
对象与构造函数原理
对象
JavaScript 中对象是可变的键控集合(keyed collections)。
定义对象
1. 字面量方式
2. 构造函数方式
var obj = {
prop: 'value',
fn: function(){}
};
var date = new Date();
构造函数
构造函数和普通函数在语法上没有区别,区别在于调用方式。使用 new 关键字调用即为构造函数,通过构造函数可以实例化一个对象。
函数的返回值有两种可能
1. 显式调用 return 返回 return 后表达式的求值结果。
2. 没有调用 return 则返回 undefined。
function People(name, age) {
this.name = name;
this.age = age;
}
var people = new People('Byron', 26);
构造函数返回值
1. 没有返回值(隐式返回)。
2. 返回简单数据类型。
3. 返回对象类型。
前两种情况构造函数返回构造对象的实例,实例化对象正是利用了这个特性。
第三种情况构造函数和普通函数表现一致,返回 return 后表达式的结果。
prototype 原型
1. 每个函数都有一个 prototype 的对象属性,该对象内有一个 constructor 属性,默认指向函数本身。
2. 每个对象都有一个 __proto__ 的属性,该属性指向其父类型的 prototype。
function Person(name) {
this.name = name;
}
Person.prototype.print = function () {
console.log(this.name);
};
var p1 = new Person('Byron');
var p2 = new Person('Casper');
p1.print();
p2.print();
this 指向与作用域
this 和作用域
作用域可以通俗地理解。
1. 我是谁(this 指向)。
2. 我有哪些马仔(局部变量)。
其中“我是谁”的回答就是 this。
“马仔”就是我的局部变量。
this 场景
普通函数
1. 严格模式:this 为 undefined。
2. 非严格模式:this 指向全局对象。
3. Node.js 环境中:this 指向 global。
4. 浏览器环境中:this 指向 window。
构造函数中:this 指向对象的实例。
对象方法中:this 指向对象本身。
call & apply
1. fn.call(context, arg1, arg2, …, argn):立即执行函数,参数列表展开。
2. fn.apply(context, args):立即执行函数,参数以数组形式传递。
function isNumber(obj) {
return Object.prototype.toString.call(obj) === '[object Number]';
}
Function.prototype.bind
bind 返回一个新函数,该函数的作用域(this 指向)被绑定为 bind 参数指定的对象。
function fn() {
this.i = 0;
setInterval(function () {
console.log(this.i++);
}.bind(this), 500)
}
fn();
() => {}
箭头函数是 ES6 提供的新特性,是简写的函数表达式,拥有词法作用域和固定的 this 值(继承自定义时的外层作用域)。
function fn() {
this.i = 0;
setInterval(() => {
console.log(this.i++);
}, 500)
}
fn();
继承与 ES6 Class
继承
在 JavaScript 的场景中,继承有两个目标,子类需要得到父类的:
1. 对象的属性。
2. 对象的方法。
function inherits(child, parent) {
var _proptotype = Object.create(parent.prototype);
_proptotype.constructor = child.prototype.constructor;
child.prototype = _proptotype;
}
function People(name, age) {
this.name = name;
this.age = age;
}
People.prototype.getName = function () {
return this.name;
}
function English(name, age, language) {
People.call(this, name, age);
this.language = language;
}
inherits(English, People);
English.prototype.introduce = function () {
console.log('Hi, I am ' + this.getName());
console.log('I speak ' + this.language);
}
function Chinese(name, age, language) {
People.call(this, name, age);
this.language = language;
}
inherits(Chinese, People);
Chinese.prototype.introduce = function () {
console.log('你好,我是' + this.getName());
console.log('我说' + this.language);
}
var en = new English('Byron', 26, 'English');
var cn = new Chinese('色拉油', 27, '汉语');
en.introduce();
cn.introduce();
ES6 class 与继承
"use strict";
class People{
constructor(name, age){
this.name = name;
this.age = age;
}
getName(){
return this.name;
}
}
class English extends People{
constructor(name, age, language){
super(name, age);
this.language = language;
}
introduce(){
console.log('Hi, I am ' + this.getName());
console.log('I speak ' + this.language);
}
}
let en = new English('Byron', 26, 'English');
en.introduce();
语法糖形式的类定义与继承机制。
label statement 标签语句
loop: 用于标记循环语句,配合 break 或 continue 使用。
for (var i = 0; i < 10; i++) {
for (var j = 0; j < 5; j++) {
console.log(j);
if (j === 1) {
break loop;
}
}
}
console.log(i);
语句与表达式
var x = { a:1 };
{ a:1 }
{ a:1, b:2 }
立即执行函数(IIFE)
( function() {}() );
( function() {} )();
[ function() {}() ];
~ function() {}();
! function() {}();
+ function() {}();
- function() {}();
delete function() {}();
typeof function() {}();
void function() {}();
new function() {}();
new function() {};
var f = function() {}();
1, function() {}();
1 ^ function() {}();
1 > function() {}();
高阶函数与闭包
高阶函数
高阶函数是把函数当做参数或者返回值是函数的函数。
回调函数
[1, 2, 3, 4].forEach(function(item){
console.log(item);
});
闭包
闭包由两部分组成
1. 函数本身。
2. 环境:函数创建时作用域内的局部变量集合。
function makeCounter(init) {
var init = init || 0;
return function(){
return ++init;
}
}
var counter = makeCounter(10);
console.log(counter());
console.log(counter());
典型错误
for (var i = 0; i < doms.length; i++) {
doms.eq(i).on('click', function (ev) {
console.log(i);
});
}
for (var i = 0; i < doms.length; i++) {
(function (i) {
doms.eq(i).on('click', function (ev) {
console.log(i);
});
})(i);
}
惰性函数
function eventBinderGenerator() {
if (window.addEventListener) {
return function (element, type, handler) {
element.addEventListener(type, hanlder, false);
}
} else {
return function (element, type, handler) {
element.attachEvent('on' + type, handler.bind(element, window.event));
}
}
}
var addEvent = eventBinderGenerator();
柯里化
一种允许使用部分参数生成新函数的方式,常用于函数复用和参数预填充。
function isType(type) {
return function(obj){
return Object.prototype.toString.call(obj) === '[object '+ type +']';
}
}
var isNumber = isType('Number');
console.log(isNumber(1));
console.log(isNumber('s'));
var isArray = isType('Array');
console.log(isArray(1));
console.log(isArray([1, 2, 3]));
function f(n) {
return n * n;
}
function g(n) {
return n * 2;
}
console.log(f(g(5)));
function pipe(f, g) {
return function () {
return f.call(null, g.apply(null, arguments));
}
}
var fn = pipe(f, g);
console.log(fn(5));
尾递归与函数技巧
尾递归
1. 尾调用是指某个函数的最后一步是调用另一个函数。
2. 函数调用自身,称为递归。
3. 如果尾调用自身,就称为尾递归。
普通递归很容易发生“栈溢出”错误(stack overflow)。
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}
factorial(5) // 120
但对于尾递归来说,由于只存在一个调用记录,所以永远不会发生“栈溢出”错误(前提是引擎支持尾调用优化)。
function factorial(n, total) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
factorial(5, 1) // 120
柯里化减少参数
function currying(fn, n) {
return function (m) {
return fn.call(this, m, n);
};
}
function tailFactorial(n, total) {
if (n === 1) return total;
return tailFactorial(n - 1, n * total);
}
const factorial = currying(tailFactorial, 1);
factorial(5) // 120
反柯里化
Function.prototype.uncurry = function () {
return this.call.bind(this);
};
push 通用化
var push = Array.prototype.push.uncurry();
var arr = [];
push(arr, 1);
push(arr, 2);
push(arr, 3);
console.log(arr);
以上内容是小编给大家介绍的JavaScript语言精粹经典实例(整理篇)的全部叙述,希望对大家有所帮助!